Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

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

add(x i32)(y i32)(z i32) i32 => x+y+z

or

add : x i32 -> y i32 -> z i32 -> i32 => x+y+z

pro
  • traditional look
  • similar to syntax of function type: (i32,i32,i32) -> i32
  • declaration syntax closer to call syntax (no parentheses) and similar to lambda ->
  • nicely maps to partial application syntax: addwh := add w h defines a function that would add w and h from its closure to the argument.
  • Having only a single argument simplifies function composition like g ∘ f which otherwise would not work if g receives several arguments.
cons
  • requires () in declaration, while call is add 1 2 3
  • confusing to newbies
  • declaration longer

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.

last changed: 2024-06-28