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

Currying and Partial Application

Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each takes a single argument (wikipedia). 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 is x+y+z

as a chain of functions with single arguments

  add(x i32)(y i32)(z i32) i32 is x+y+z

or, using syntax closer to Haskell (but mixing Haskell's declaration and definition),

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

we could even use implicit argument names, e.g., .0, .1, etc.:

  add : i32 -> i32 -> i32 -> i32 is .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 is x+y+z

add(x i32)(y i32)(z i32) i32 is x+y+z

or

add : x i32 -> y i32 -> z i32 -> i32 is 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 is ...
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

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 is x+y+z

Could then have this curried form using a single tuple argument t:

  add(t (i32,i32,i32)) i32 is 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 is arg.0 + arg.1 + arg.2

or

  add (i32,i32,i32) i32 is .0 + .1 + .2

Using a single argument would for sure simplify the language. However, the lack of argument names seems a major drawback.

Partial Application

Using Lambdas

Lambdas permit partial application of features, e.g.,

  add(x,y,z i32) i32 is 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 funcion arguments. However, this form of partial application only works in cases when the first argument or arguments should be paritally 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 parital applicaion of the first argument(s), while we don't have this for later arguments?

Using operators

Turning an infix operator into a function with one argument could be achieved by allowing its use as an 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 only 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) is
    if logLevel >= level
      say "LOG: {msg()}"

This would usually be called using a lambda:

  log ()->"passing lambda"

While a version with an argument to the lambda:

  logLocalized(msg (l language) -> string) is
    if logLevel >= level
      say "LOG: {msg default_language}"

could use a lambda

  logLocalized (l -> logtext.translate l)

or partial application:

  logLocalized 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.