1
votes

I start with this

type int_pair = int * int

and then have these

fun sip1 ((i,j) : int_pair) = (j,i)
fun sip1a (ip : int_pair) = (#2 ip, #1 ip)
fun sip2 (ip : int*int) = (#2 ip, #1 ip)
fun sip3 (i : int, j : int) = (j,i)

with these results

: val sip1 = fn : int_pair -> int * int
: val sip1a = fn : int_pair -> int * int
: val sip2 = fn : int * int -> int * int
: val sip3 = fn : int * int -> int * int

They all work. What puzzles me is how the sip1 and sip1a can be set up to take a single int_pair variable ip or as tuple-like (i,j). How can it handle both just due to the type synonym? I simply assume that any sort of (a,b) in the body will default to type int*int on return, but also, in the expression (j,i) of sip1 it seems to not need the referencing (#1...) of sip1a. Rather peculiar at first glance. sip2 seems straight-on. But then in sip3 it would seem to be simply two ints going in and a result of type int*int coming out; but the function type says it is int * int -> int * int. Why is it not type int -> int -> int*int as I might have seen in other languages? IOW, why does ML treat multiple incoming variables as a tuple?

1
The argument (i, j) is not "tuple-like", it is a tuple. All functions take exactly one argument. - molbdnilo
See also fun sip4 ((i,j) : int * int), and fun sip5 ((i : int, j : int) : int*int) and fun sip6 ((i : int, j : int) : int_pair) - molbdnilo
All your functions have the same type, it's just that when you specifically use your type synonym, so does the interpreter - if you enter fun sip7 (ip: int * int) = ip : int_pair;, you're greeted with val sip7 = fn : int * int -> int_pair. - molbdnilo
Yes, I've seen that statement "All functions take exactly one argument," but not known what to make of it, since I see functions with multiple incoming variables. So technically fun sip3 (i : int, j : int) = (j,i) is not getting two variables, but one packaged as a tuple. So why are clearly two incoming dependent variables considered just one? What is the computer science theory behind this? -- my original question could be worded as. Is this something from the lambda calculus world? I've seen ML tutorials that start you out with lambda calculus. . . . - 147pm
sip1 and sip3 use pattern matching. Pattern-matching is able to unpack the tuple, binding the items to i and j (in this case). That SML function definitions are able to use pattern matching doesn't imply that the functions so defined are functions of more that 1 variable. - John Coleman

1 Answers

1
votes

In ML all functions take exactly one argument. But when we need to send more than one argument, ML interprets the arguments in the traditional parentheses as a single tuple, in keeping with the one argument only rule:

fun swap1 (i,j) = (j,i)
: val swap1 = fn : 'a * 'b -> 'b * 'a

The two arguments above separated by the * means they are a 2-arity tuple.

- swap1 (1,2);
val it = (2,1) : int * int

Even when variables seem separate, they are still considered a single argument, again, a 2-arity tuple:

fun swap1a (i : int, j : int) = (j,i)
: val swap1 = fn : int * int -> int * int

If we create a type synonym with type, e.g.,

type int_pair = int * int

functions using int_pair still are typed as tuples:

fun swap2 ((i,j) : int_pair) = (j,i)
: val swap2 = fn : int_pair -> int * int
fun swap2a (ip : int_pair) = (#2 ip, #1 ip)
: val swap2a = fn : int_pair -> int * int
fun swap2b (ip : int*int) = (#2 ip, #1 ip)
: val swap2b = fn : int * int -> int * int

ML also has curried functions where multiple arguments are curried, basically meaning the arguments are taken in one at a time, which, again, holds to the only-one-argument rule. Currying allows us to partially apply a function to some of its arguments, leaving a residual function which can be further evaluated later. The special ML syntax for a function that employs currying is to simply have the arguments without enclosing parentheses and separated by a space:

fun swap3 i j = (j,i)
: val swap3 = fn : 'a -> 'b -> 'b * 'a

- swap3 1 2;
val it = (2,1) : int * int

Another visualization is to construct swap3 with anonymous functions:

- val swap3a = fn i => fn j => (j,i)
- swap3a 1 2;
val it = (2,1) : int * int

Now, we might envision the incoming 1 2 being resolved right-to-left (ironically called left-associating) a la lambda calculus:

(fn i => fn j => (j,i))(1,2)
(fn i => (2,i))(1)
(2,1)

Notice that fn i =>... is actually accepting a function as its input, in this example, the anonymous function expression fn j =>..., which has the value (2,i). In fact, the spirit of this can be done at the REPL:

- ((fn i => fn j => (j,i)) 1) 2;
val it = (2,1) : int * int

As commenter Andreas Rossberg says (see above comments), in most functional languages, multiple argument functions employ currying. He notes that the use of tuples is cartesian in the sense of a cartesian product. He also notes that this is orthogonal, which in this context might mean that these two approaches to handling multiple arguments, tuple and currying, are orthogonal to each other in that they're completely opposite, non-overlapping approaches that don't semantically interfere with one another.