I didn’t set out to implement currying. I was trying to implement closures but it seemed like it would be easier to implement closures if I added currying first.
But I didn’t set out to implement closures either. I originally wanted to implement asynchronous expressions and futures but that seemed easier to implement if I already had closures.
So now I find myself trying to implement currying for Leema, but I don’t have a good sense of what the syntax should be. I’m hoping the process of writing this blog post will help organize my thoughts and and result in a good plan to move forward.
what is currying?
Currying is when you call a function with a subset of its required parameters, and in the process create a new function that takes the parameters that have not yet been provided.
A simple example in Haskell might look like this:
multiply a b = a * b
main =
let double = multiply 2
answer = double 7
in do print answer
default parameters
In Haskell, currying just looks like a normal function call but without all of the normal parameters.
In other languages, like Python, calling a function without all the parameters is how you use an alternative language feature: default parameters.
In this example, the two calls to foo are equivalent:
def foo(a, b=None):
bar(a)
if b is None:
baz(b)
foo(8)
foo(8, None)
the syntax conflict
The intention for Leema has been to support both of these features, currying and default parameters. However, the problem with supporting both of them is that the standard syntax of calling a function while leaving out some of the parameters is ambiguous. Did the programmer intend to curry the function or call it with its default parameters?
So the question then is what is the best way for the programmer to disambiguate between these two possible intentions? We can think of this question as two smaller questions.
Is there special syntax to indicate a call should be curried?
Is there special syntax to indicate a call should pass default parameters?
Answering them together gives 4 options to consider.
- Neither currying nor default parameters have special syntax
- Default parameters have special syntax, but currying does not
- Currying has special syntax, but default parameters do not
- Both currying and default parameters require special syntax
no special syntax. at all.
I did not consider this case originally because it is so ambiguous but there is a scenario where the ambiguity could be resolved by type inference. Is the result of the call used as the return type of the function or is it used as a function?
I’m not confident that this could be correctly inferred without frequently resorting to type hints. If type hints are usually required then it would kind of be like the fourth option but accidentally.
Another alternative might be to look at whether the function has default parameters. If so, apply them, otherwise curry the function. This could be done consistently but might be too implicit and sneaky for readers of a program to easily interpret.
special syntax for default parameters
What would a special syntax for default parameters look like? Here are a few possible ideas.
## a ++ suffix to the function call parameters let with_defaults := foo(x, y)++ ## maybe move the ++ inside as the last "parameter" let with_defaults := foo(x, y, ++) ## this looks a little like Rust's pattern matching ## to skip the remaining parameters let with_defaults := foo(x, y, ...)
special syntax for currying
Here are two syntax ideas to indicate that a function should be curried. Obviously the ...
syntax should only be used for one or the other, not both.
## In this model, all unpassed arguments are curried by the `...`
func main ->
let curryf1 := foo(x, y, ...)
--
## In this model, each curried parameter must be
## indicated specifically by passing a `?` for that parameter.
## This is also similar to SQL syntax.
func main ->
let curryf2 := foo(x, y, ?)
let curryf3 := foo(?, y, z)
let result2 := curryf2(z)
let result3 := curryf3(x)
--
special syntax for both
The fourth case is to be explicit so that using default parameters or currying are both visually distinct from a normal function call where all parameters are provided.
decision?
When I started this blog post, I was kind of leaning towards having a special syntax for currying and nothing extra for default parameters. In the process of writing this though, there is something appealing the explicitness of having a special syntax for default parameters as well.
In both of those cases, there is a special syntax for currying and between the 2 options, I prefer the ?
syntax.
The question for default parameters is less obvious to me. The most common use case for default parameters is to make it easier to add a new parameter to a function that’s already being used all over the place. This is especially important for dynamic languages where you may not discover some usage until the code is failing in production.
In this case, if there were a special syntax, it would require fixing all usages to the function and adding the special syntax. This kind of makes sense until you consider adding a second default parameter to a function where, at that point, you don’t need to add anything. From here I will say that the only two real options then are to require no special syntax for default parameters or to not support default parameters at all.
Thanks for reading along. Check out Leema if you get a chance sometime and see how this decision for currying syntax played out!