Currying and Partial Application
Currying means converting a function that takes multiple arguments into a sequence of functions that each take a single argument. Similarly, partial application takes a function and creates a new function by fixing one or several of its arguments.
Curried form in Function definition
Languages like Haskell use the curried form to define functions with multiple arguments. Applied to Fuzion, the idea would be to declare a function like add
add(x,y,z i32) i32 => x+y+z
as a chain of functions with single arguments
add(x i32)(y i32)(z i32) i32 => x+y+z
or, using syntax closer to Haskell (but mixing Haskell's declaration and definition),
add : x i32 -> y i32 -> z i32 -> i32 => x+y+z
we could even use implicit argument names, e.g., .0
, .1
, etc.:
add : i32 -> i32 -> i32 -> i32 => .0 + .1 + .2
The difference between a multi-argument declaration and a declaration using the curried form is just syntactic. Here is my attempt in collecting pros and cons:
style
aspect
|
multi-argument | curried |
---|---|---|
example | add(x,y,z i32) i32 => x+y+z |
or
|
pro |
|
|
cons |
|
|
The current syntax in Fuzion is as follows
element | syntax |
---|---|
feature declaration | add(x,y,z i32) i32 => ... |
function type |
(i32, i32, i32) -> i32
|
feature call |
a := add x y z
or a := add(x, y, z)
|
lambda |
x,y,z -> x+y+z
or (x, y, z) -> x+y+z
|
Curried form using tuples
As an alternative to currying by creating partial functions for each argument,
one might create a single tuple argument instead. The add
feature
from above
add(x,y,z i32) i32 => x+y+z
could then have this curried form using a single tuple
argument t
:
add(t (i32,i32,i32)) i32 => t.0 + t.1 + t.2
This approach would permit to go one step further and require that each feature has exactly one argument, which might be a tuple or a unit type. Then, we do not even need to specify a name for this argument and use an implicit name instead as in
add (i32,i32,i32) i32 => arg.0 + arg.1 + arg.2
or
add (i32,i32,i32) i32 => .0 + .1 + .2
Using a single argument would for sure simplify the language. However, the lack of argument names seems like a major inconvenience.
Partial Application
Using Lambdas
Lambdas permit partial application of features.
add(x,y,z i32) i32 => x+y+z addwh i32 -> i32 := d -> add w h d
Using Partial Argument Lists
This syntax can be simplified when only the first arguments are fixed:
addwh i32 -> i32 := add w h
Partial argument lists might make type inference easier than using lambdas such that it might be sufficient to write
addwh := add w h
Partial argument lists, in particular giving only one single argument, nicely correspond to the curried declaration of function arguments. However, this form of partial application only works in cases when the first argument or arguments should be partially applied. Partial application of, e.g., only the second argument would require an explicit lambda as in:
add_middle i32, i32 -> i32 := d,e -> add d middle e
Does it make sense to have syntax sugar for partial application of the first argument(s), while not having this for later arguments?
Using operators
Turning an infix operator into a function with one argument could be achieved
by allowing its use as a prefix or postfix operator as follows. The lambdas
assigned to f
and g
in this code
f i32 -> i32 := x -> x - 3 g i32 -> i32 := x -> 3 - x
Could be replaced using the infix operator as prefix- or postfix-operator to
the partially applied argument 3
:
f i32 -> i32 := - 3 g i32 -> i32 := 3 -
Alternatively, we could work with placeholders _
like Scala does
f i32 -> i32 := _ - 3 g i32 -> i32 := 3 - _
Automatic lambdas
When missing parameters automatically turn a call into a lambda using the
partially applied call, it makes sense to automatically create lambdas for
fully applied calls as well. Take the following feature log
that
lazily evaluates its argument msg
:
log(msg () -> String) => if log_level >= level say "LOG: {msg()}"
This would usually be called using a lambda:
log ()->"passing lambda"
While a version with an argument to the lambda
log_localized(msg (l language) -> String) => if log_level >= level say "LOG: {msg default_language}"
could use a lambda
log_localized (l -> logtext.translate l)
or partial application:
log_localized logtext.translate
It would hence be consistent to automatically create the lambda in the first case as well:
log "passing lambda implicitly"
In summary, any expression could be turned into a lambda if the expression is a partially applied call and the arguments expected for the target function type together with the partial actual arguments form a proper actual call or if the expression is not a partially applied call and the target function type expects no arguments.
One effect that this would have is that for the caller, there is no longer any syntactic difference between passing a value directly and passing a lazily evaluated closure.
References
A nice twitter thread Hot take: currying is bad by Ron Minsky and an earlier one Currying (and positionality) are not good by Rahul Goma Phulore.