Auto-Currying vs. Variable-Arity

When designing a programming language, I always reach a few decision points where I have to choose between two options that both sound good to me. One of those is whether to allow variable arguments (a la scheme), or do automatic currying (a la haskell). Other languages have these features, but I pick on scheme and haskell because they are my favorites, and I use both often.

Anyway, I like both of these features, and use them a lot in languages that provide them. Sadly, it doesn’t seem too clean to have both at the same time. To curry you need to know how many arguments are coming, and to accept zero arguments you can’t auto-curry. They just aren’t very compatible features.

I mean, in scheme you can define the + function to take any number of arguments, including no arguments at all:

(+ 1 2)        ; valid
(+ 1 2 3 4 5)  ; valid
(+)            ; valid

That’s really cool. Haskell’s idiom seems to be to define a second function that accepts a list as an argument:

1 + 2   -- or "sum [1,2]" of course
sum [1, 2, 3, 4, 5]
sum []

You might think that the list-accepting function takes care of the issue on haskell’s end. But, it accepting a variable number of arguments turns out to be really useful in a variety of contexts, like map:

(map + '(1 2 3))                    ; valid
(map + '(1 2 3) '(4 5 6))           ; valid
(map + '(1 2 3) '(4 5 6) '(7 8 9))  ; valid

… whereas in haskell we need both map and the zipWith family to handle a similar task:

map id [1,2,3]
zipWith (+) [1,2,3] [4,5,6]
zipWith3 (\a b c-> a+b+c) [1,2,3] [4,5,6] [7,8,9]

Note that in the third case, we can’t use the sum function, since zipWith3 doesn’t pass the 3 arguments as a list. I suppose a version of map could be made which takes a function that operates on lists, along with a list of lists to zip over:

mapVariable sum [[1,2,3],[4,5,6],[7,8,9]]

… and for all I know that function’s already in the GHC libraries somewhere. But, making list versions of everything certainly doesn’t feel very efficient or haskell-like to me.

So, in this example, I’d say the scheme style definitely wins, in terms of using similar-looking code to accomplish similar-looking tasks.

However, I don’t see how I can support scheme-style variable args and also allow familiar auto-currying haskellisms like: map (+1) lst. In scheme, that example would look like:

(map (lambda (x) (+ 1 x)) lst)

It’s just not the same! With srfi-26, you can type:

(map (cut + 1 <>) lst)  ; srfi-26

… which is slightly nicer, I guess. But only slightly.

The automatic currying is also very useful when defining functions. I often write functions like:

fooize = map foo

… which in scheme must be much more explicit about grabbing the parameter and passing it to the function:

(define (fooize lst) (map foo lst))

So, we’ve defintely lost some brevity. However, I usually come down on the scheme side of this argument when designing little extension languages. I think it looks very clean to be able to use the same function name on variable numbers of arguments, and I can still manually curry functions even if it is more verbose.

Are there any valid compromises? Like, say, auto-curry all functions that don’t accopt variable numbers of arguments? Or auto-curry all functions until you have at least as many arguments as you need to fully apply the function? Those options seem confusing at best, and don’t work very well anyway for the toy cases like the + function. If your language has macros, then you can do like some people have done and make macros that allow you to auto-curry individual functions:

(define-curried (foo x y z) (+ x (/ y z)))
((foo 3) 1 2)
((foo 3 1) 2)

But, again, this leads to confusing programs in my opinion… where different functions seem to get different treatment. I haven’t been able to come up with any workable compromises… so when I put together a language, I just pick one approach or the other.

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.