Search code examples
smlml

How turn list of pair in list of int, where result int is sum of pair


I try to define function with the following protocol: [(1,2), (6,5), (9,10)] -> [3, 11, 19]

Here is what I have now:

fun sum_pairs (l : (int * int) list) =
  if null l
  then []
  else (#1 hd(l)) + (#2 hd(l))::sum_pairs(tl(l))      

According to type checker I have some type mismatch, but I can't figure out where exactly I'm wrong.


Solution

  • This code runs in PolyML 5.2:

    fun sum_pairs (l : (int * int) list) =
        if null l
        then []
        else ((#1 (hd l)) + (#2 (hd l))) :: sum_pairs(tl l)
    (* ------------^-------------^ *)
    

    The difference from yours is subtle, but significant: (#1 hd(l)) is different from (#1 (hd l)); the former doesn't do what you think - it attempts to extract the first tuple field of hd, which is a function!

    While we're at it, why don't we attempt to rewrite the function to make it a bit more idiomatic? For starters, we can eliminate the if expression and the clunky tuple extraction by matching on the argument in the function head, like so:

    fun sum_pairs [] = []
      | sum_pairs ((a, b)::rest) = (a + b)::sum_pairs(rest)
    

    We've split the function into two clauses, the first one matching the empty list (the recursive base case), and the second one matching a nonempty list. As you can see, this significantly simplified the function and, in my opinion, made it considerably easier to read.

    As it turns out, applying a function to the elements of a list to generate a new list is an incredibly common pattern. The basis library provides a builtin function called map to aid us in this task:

    fun sum_pairs l = map (fn (a, b) => a + b) l
    

    Here I'm using an anonymous function to add the pairs together. But we can do even better! By exploiting currying we can simply define the function as:

    val sum_pairs = map (fn (a, b) => a + b)
    

    The function map is curried so that applying it to a function returns a new function that accepts a list - in this case, a list of integer pairs.

    But wait a minute! It looks like this anonymous function is just applying the addition operator to its arguments! Indeed it is. Let's get rid of that too:

    val sum_pairs = map op+
    

    Here, op+ denotes a builtin function that applies the addition operator, much like our function literal (above) did.


    Edit: Answers to the follow-up questions:

    1. What about arguments types. It looks like you've completely eliminate argument list in the function definition (header). Is it true or I've missed something?

    Usually the compiler is able to infer the types from context. For instance, given the following function:

    fun add (a, b) = a + b
    

    The compiler can easily infer the type int * int -> int, as the arguments are involved in an addition (if you want real, you have to say so).

    1. Could you explain what is happening here sum_pairs ((a, b)::rest) = (a + b)::sum_pairs(rest). Sorry for may be dummy question, but I just want to fully understand it. Especially what = means in this context and what order of evaluation of this expression?

    Here we're defining a function in two clauses. The first clause, sum_pairs [] = [], matches an empty list and returns an empty list. The second one, sum_pairs ((a, b)::rest) = ..., matches a list beginning with a pair. When you're new to functional programming, this might look like magic. But to illustrate what's going on, we could rewrite the clausal definition using case, as follows:

    fun sum_pairs l =
      case l of
        [] => []
      | ((a, b)::rest) => (a + b)::sum_pairs(rest)
    

    The clauses will be tried in order, until one matches. If no clause matches, a Match expression is raised. For example, if you omitted the first clause, the function would always fail because l will eventually be the empty list (either it's empty from the beginning, or we've recursed all the way to the end).

    As for the equals sign, it means the same thing as in any other function definition. It separates the arguments of the function from the function body. As for evaluation order, the most important observation is that sum_pairs(rest) must happen before the cons (::), so the function is not tail recursive.