[Haskell-cafe] Client-extensible heterogeneous types (Duck-typed variadic functions?)

Evan Laforge qdunkan at gmail.com
Thu Oct 14 03:19:08 EDT 2010


>> How
>> are you expecting to call the functions in that container?  "for f in
>> c: try: return f(*misc_args) except: pass"?
>
> to_do = [(call, (AuntMabel,)),
>         (buy,  ([(12*kg, sugar), (6*bushel, wheat)])),
>         (introduce, (Romeo, Juliet))]
>
> for do,it in to_do:
>    do(*it)

As has been pointed out, simply write it like this:

to_do = [call AuntMabel, buy [(12kg, sugar), (6 bushel, weat)], etc.]

If they are monadic actions, you can call 'sequence_' on them when you
want them to "happen".  If not, you really just have a list.

> The thing is, I can arrange for them to be compatible. Python won't be able
> to confirm this statically, but is it too much to ask of Haskell to have it
> figure out (statically) that all of
>
>    (Int -> Bool, Int)
>    (Banana -> Apple -> Orange -> Kiwi -> Bool, (Banana, Apple, Orange,
> Kiwi))
>    (Bool -> Bool -> Bool, (Bool, Bool))
>
> can be combined to give Bool ?

Yes, this sounds like an existential:

data Boolable forall a. = Boolable (a -> Bool)

But despite the fact that I've been keeping them in the back of my
mind for years, I've never once come up with a place where one would
actually be useful.  I guess I just don't think that way.

>> I agree with you
>> that this is sometimes easier in a dynamic language because you can
>> reuse the implementation language at runtime.
>
> I don't think I'm looking for that in this case. I'm just asking to be
> allowed to stick both
>
>    (A -> B -> X, (A, B))
>
> and
>
>    (C -> D -> E -> X, (C, D, E))
>
> etc. in the same container, because, frankly, in the context in which they
> are used, they *are* the same.

Maybe you should focus less on the particular implementation you want
and more on the end result?  If you start off saying "I want
heterogenous lists" then you'll start off with a problem for haskell
:)

>> In the extreme, in python, you can simply call eval() on the input string.
>
> Aaaargh! No! For the love of all that is good, please! Nooooo! :-)

Well, yes, that's the extreme.  My point was that when you call
f(*args) you are also reusing the interpreter at runtime.  The fact
that values carry their types around at runtime is one example of
this.  Haskell doesn't have the interpreter around at runtime.  But if
you know exactly what parts of the interpreter you want, you can
recover them, i.e. with Dynamic or by using 'hint' in the extreme.
But keep in mind you are implementing an interpreter.

BTW, I've used eval(s, {}) on a number of occasions when I wanted to
parse ints or strings and didn't want to write my own parser.  It
doesn't seem that much different from 'read' in haskell.

>> apply1 f [x] = f x
>> apply1 _ _ = throw hissy fit
>> apply2 f [x, y] = f x y
>> etc.
>
> I would hope that the types could be checked statically, as I explained
> above.

They can.  The strings coming in from the user, of course they can't,
because they're not even known statically.  The 'apply1' function, of
course, is statically checked in that 'f' is required to be a function
with a single string argument.  Well, of the same type as the list
passed.

>> Now you can put them all into one container.  Yes, the family of apply
>> functions may be a little tedious, and you may be able to use
>> typeclass magic to automatically select the right apply function, but
>> it doesn't seem like a big deal to me.  If you want to extend this to
>> different types, you just have to extend this in one more direction,
>> and a typeclass definitely helps there.
>
> Except that I now lose the ability to stick them all into the same
> container. (Unless I enable existential quantification.)

I meant to use typeclasses to make it easier to create a typechecking
function, in the same way that 'apply[n]' creates a function that
checks number of args.  Then you can write 'apply1 f [x] = f (parse
x)' and 'parse' will be 'parse_string' or 'parse_int' depending on the
value 'f' expects.  The end result is still a function '[String] ->
Either ParseError Answer' and so can all go into the same container.
You may even be able to overload 'apply' so it will dispatch on applyn
depending on the arity.  Then you just write [apply f, apply g, apply
h] where 'f', 'g', and 'h' can all have different types.

The bottom line is that you have to parse and typecheck the strings
typed by the user at some point.  In the python case, you reuse
python's typechecker when you write f(*args).  In the haskell case you
have to check the types yourself, but typeclasses can probably make it
pretty painless.

If you want to reuse haskell's typechecker, then you can install hint
and just eval the string directly.  If you want to do something in
between like python's f(*args)... well, I'm not aware of libraries
that do that.  You would have to construct an AST, unparse that to
haskell code, and give that to hint.  Sounds like a bother.

> I'm pretty sure that you could never come up with a sufficiently large set
> of primitives. Even if you could, it seems like far too much work, given
> that the ability to store arbitrary (sets of co-operating) functions (which,
> together, always return the same types) in the same container, trivially
> provides you with full generality.

Could you provide a more concrete example?  So far the simple example
of int accepting functions with different arities is pretty easy to
implement with a plain list, so maybe you could provide a bit of
python or something that does what you want and would be harder with
static types?

>> Eventually, some invisible line is crossed and you have
>> an EDSL for writing math tests.
>
> That is *exactly* where I am heading with this.

Well, good, haskell's supposed to be good at EDSLs :)

>> Your Question type could look like
>> 'String -> Answer' and Answer = 'Wrong String | Right | ParseError
>> String'.
>
> Actually, I think it should be more like:
>
> Answer = ParseError String | Wrong String | NeitherWrongNorRightSoTryAgain
> String | Right

Well sure, the point is that neither of these types are even
polymorphic, let alone existential.


More information about the Haskell-Cafe mailing list