0

I found this code on another SO post:

fun number_in_month ([], _) = 0
  | number_in_month ((_,x2,_) :: xs, m) = 
    if x2 = m then
    1 + number_in_month(xs, m)
    else
    number_in_month(xs, m)

and to my surprise it works.

- number_in_month ([(2018,1,1),(2018,2,2),(2018,2,3),(2018,3,4),(2018,2,30)],2);
val it = 3 : int

My confusion is first unfamiliarity with this form of classic mathematical recursive function (I'm a beginner), then how it actually steps through the list. My intuition would have the recursive calls in the if-then-else sending the tail of the list, i.e.,

...
1 + number_in_month((tl xs), m)
...

but that doesn't work. How is it iterating through the list with each recursive call? I can only imagine this is baked-in SML magics of some sort.

sshine
  • 15,635
  • 1
  • 41
  • 66
147pm
  • 2,137
  • 18
  • 28

2 Answers2

1

No magic, xs is the tail of the list.

There are two things to understand: lists and pattern matching.

In SML, the list syntax [a, b, c] is just a shorthand for a :: b :: c :: nil, where :: is the (infix) cons constructor. Other than this shorthand, there is nothing magic about lists in SML, they are pre-defined as this type:

datatype 'a list = nil | :: of 'a * 'a list
infixr 5 ::

The latter definition turns :: into a right-associative infix operator of precedence 5.

Secondly, the definition is using pattern matching on the argument. A patten like x::xs matches a (non-empty) list of the same shape, binding x to the head of the list and xs to its tail, corresponding to the definition above. In your function, x furthermore replaced by another pattern itself.

That's all. No magic. This would equally work with a custom list representation:

datatype my_list = empty | cons of (int * int * int) * my_list
infixr 5 cons

fun count (empty, x) = 0
  | count ((_,y,_) cons xs, x) =
    if x = y then 1 + count (xs, x) else count (xs, x)

val test = count ((1,2,3) cons (3,4,5) cons (6,2,7) cons empty, 2)
Andreas Rossberg
  • 34,518
  • 3
  • 61
  • 72
  • Yes, of course. That's why `(tl xs)` was nonsense. But how could this be changed to, say, build a new list of matches rather than just counting them? To do that I'd want to have the nil case return the `[]`, then with the `...((_,x2,_) :: xs, m)` case I would have to get the head -- which is what? It's not `x`, and `(_,x2,_) :: match_month (xs, m)` isn't right either. What would that incoming list's head be bound to? – 147pm May 20 '19 at 02:28
  • So yes, I changed `...((_,x2,_) :: xs, m)` to `...((x1,x2,x3) :: xs, m)...` and it worked, but that seems like a kludge. I fear I'm missing a deeper principle here. . . . – 147pm May 20 '19 at 02:35
  • If you want to use the whole triple then name it in the pattern, replacing `(_,x2,_)` with just a single variable: `... | match_month (date :: xs, m) = let val (_,x2,_) = date in ... date :: match month_ (xs, m)`. You can also name a subpattern directly using the `as` keyword: `((date as (_,x2,_)) :: xs, m)`. – Andreas Rossberg May 20 '19 at 06:21
1

But how could this be changed to, say, build a new list of matches rather than just counting them?

In that case, you want two modifications to your current solution:

  1. You want to change the pattern of your recursive case to one where you can extract the entire date 3-tuple if it matches. Right now you're only extracting the month part for comparison, throwing away the other bits, since you just want to increment a counter in case the month matches.

  2. The result of the function should not be 1 + ..., but rather (x1,x2,x3) :: ....

So a quick fix:

fun dates_of_month ([], _) = []
  | dates_of_month ((year,month,day) :: dates, month1) =
    if month = month1
    then (year,month,day) :: dates_of_month (dates, month1)
    else                     dates_of_month (dates, month1)

I changed ...((_,x2,_) :: xs, m) to ...((x1,x2,x3) :: xs, m)... and it worked, but that seems like a kludge.

Here are Andreas Rossberg's two alternatives spelled out:

Using let-in-end:

fun dates_of_month ([], _) = []
  | dates_of_month (date :: dates, month1) =
    let val (_, month, _) = date
    in
      if month = month1
      then date :: dates_of_month (dates, month1)
      else         dates_of_month (dates, month1)
    end

Using as:

fun dates_of_month ([], _) = []
  | dates_of_month ((date as (_,month,_)) :: dates, month1) =
    if month = month1
    then date :: dates_of_month (dates, month1)
    else         dates_of_month (dates, month1)

And here is a third option that abstracts out the recursion by using a higher-order list combinator:

fun dates_of_month (dates, month1) =
    List.filter (fn (_, month, _) => month = month1) dates
sshine
  • 15,635
  • 1
  • 41
  • 66
  • This is great stuff. I'm following along with https://courses.cs.washington.edu/courses/cse341/18au/ which is a good intro to ML IMHO. Thinking of giving it to some high-schoolers too. But first I have to master it myself. – 147pm May 20 '19 at 16:08