ml

What is the difference between function declarations `f x y` and `f (x, y)` in Standard ML?


In Standard ML, what is the difference between the following declaration (I omitted the definition starting with =):

fun f x y;

and

fun f (x, y);

As far as I understand, the first takes two arguments while the second takes a tuple. If it so, still, what are the (practical) implications of using one versus another? Can one argue on which is the best style? Assuming, one doesn't actually need to use the tuple as whole, i.e. only x and y, separately, are of relevance.


Solution

  • Yes, as you said, the first takes "two arguments", while the second takes one argument that is a tuple. However, to understand what's really going on, you have to understand currying. In ML, every "function" takes exactly one argument (no more, no less).

    In the second case, this is easy to understand, it takes one argument, which is a tuple, and it does something with the things in the tuple, and returns a result. In the first case, you are defining a function that takes one argument, and then returns a function that takes another argument, and then returns the result.

    Say, for example, this is a function that takes two ints and returns an int. Looking at the types of these two functions is instructive: the second function's type is (int * int) -> int, i.e. a function from a tuple to int. The first function's type is int -> int -> int, which, since -> is right-associative, can be parsed as int -> (int -> int). So you can see, it takes an int and returns a function. The syntax fun f x y = ... is syntactic sugar for the more verbose val f = fn x => fn y => .... When you apply this function, e.g. f 3 4, function application is left-associative, so that is actually (f 3) 4, so you can see how that works: f takes an int, returns a function that is then applied to another int. This is the gist of currying. ML's syntax simply makes it transparent to do.

    The curried version (the first version) allows you to do partial application. This means, although your function conceptually takes two arguments, you don't have to give it that number of arguments. So the f 3 4 we have above (which is (f 3) 4), what if we just took the f 3, and instead of just applying that to 4, just keep it and store it in a variable or something? By "giving the function less arguments than it wants", we automatically get a function that takes the remaining arguments. For example, we can do map (f 3) someList, and it will compute a list of f 3 x for each x in the list, without us having to write some complicated syntax.

    In Standard ML, library functions are mostly in the uncurried form; whereas in OCaml and Haskell, they are mostly in the curried form. (See this question.) However, SML library functions that are higher-order functions (like map and fold) tend to take their function arguments in a separate (curried) argument.