Designing Crux – Function Calls

Tupled vs. Curried Functions

Programming languages specify function argument lists in one of two ways: tuples or currying. Most mainstream languages use the former:

function add(x, y) {
    return x + y;
}

The function add takes two arguments. You can imagine the caller building up a tuple of arguments on the stack before calling the function.

Haskell and ML effect multi-argument functions with currying. For example:

add :: Int -> Int -> Int
add x y = x + y

This is equivalent to:

add :: Int -> (Int -> Int)
add x = \y -> x + y

That is, add is a function that takes an Int and returns another function which also takes an Int and returns an Int.

Currying is quite elegant. All functions take one argument, which makes certain types of metaprogramming easy. Curried function syntax is also extremely convenient: map (add 1) [1, 2, 3] returns [2, 3, 4].

However, there are some downsides to curried functions:

The first has to do with knowing when effects occur. In Haskell, where function application is always pure, this isn’t an issue. But since Crux allows unrestricted effects (more on this later), we want it to be obvious when side effects can happen. Consider this snippet of a hypothetical curried language:

add x y =
  print "adding " + toString x + " and " + toString y
  return x + y

Now consider this other definition:

add x =
  print "adding " + toString x
  return \y ->
    print " and " + toString y
    return x + y

Both functions have the same behavior when called like this:

add 1 2
add 1 3

But they would have different behavior with a partial application:

let addOne = add 1
addOne 2
addOne 3

That’s one argument against curried functions, though you could argue it’s a rare issue not worth worrying much about.

However, a more serious issue has to do with consistent performance. Curried languages don’t actually produce new functions for every argument of every call. That would be way too slow. In practice, functions take multiple arguments at the machine-code level (where the number of arguments is their arity), and when the function’s definition is not known at compile-time, some dynamic arity checks are added to decide whether to create a new partially-applied function, call straight through to the implementation, or perform oversaturation.

These dynamic checks add a bit of runtime overhead, and since we want Crux to have obvious execution semantics, we decided not to use curried functions.

Finally, I want to mention a third downside of curried functions: the error messages. At least in Haskell, if you pass too few or too many arguments, you don’t get a very obvious error message.

Prelude> let f a b = a + b
Prelude> f 1 2
3
Prelude> f 1
<interactive>:31:1:
    No instance for (Show (a0 -> a0))
      (maybe you haven't applied enough arguments to a function?)
      arising from a use of ‘print’
    In the first argument of ‘print’, namely ‘it’
    In a stmt of an interactive GHCi command: print it
Prelude> f 1 2 3
<interactive>:32:1:
    Non type-variable argument in the constraint: Num (a -> t)
    (Use FlexibleContexts to permit this)
    When checking that ‘it’ has the inferred type
      it :: forall a t. (Num a, Num (a -> t)) => t
```

Syntax

Syntax is somewhat tangential to the issue at hand, and it’s certainly possible to use the f x y z syntax with tupled functions, but my experience teaching Haskell is that people get unnecessarily hung up on the "f x y z" call syntax and we decided against spending our strangeness budget here.

Conclusion

That was a long justification for saying that Crux’s function syntax and semantics are just like every other mainstream language. :)

fun add(x, y) {
    x + y
}

let four = add(2, 2)

2 thoughts on “Designing Crux – Function Calls”

  1. It’s definitely been an issue in Standard ML libraries that sometimes it’s not clear when an effect happens when currying is involved, e.g., https://github.com/SMLFamily/BasisLibrary/wiki/2015-009-Policy-for-exceptions-and-partial-applications and https://github.com/SMLFamily/BasisLibrary/issues/10

    That said, I’d like to clarify that you don’t have to write a curried API in ML or Haskell. If you want, you can always write

    (Haskell)
    add :: (Int, Int) -> Int
    add (x, y) = x + y

    or

    (ML)
    fun add (x, y) = x + y

    In fact, most idiomatic Standard ML function definitions are uncurried.

Leave a Reply

Your email address will not be published. Required fields are marked *