Finding the minimum of two values
Your expression if y < z then y else z
has a built-in name Int.min (y, z)
.
Dealing with empty lists
You handle the empty list as minlist nil = nil
, which means that "the smallest int of an empty int list is the empty list". But the empty list is not an int and so cannot be an element in an int list, or the return value for a function that otherwise returns smallest ints.
As molbdnilo also says, you could either live with a compilation warning (and risk having a Match
exception raised at runtime if you ever feed the function an empty list), or raise a specific exception such as Empty
when given the empty list. Neither are good, but the latter at least makes the problem clear.
Writing this without foldr
it might look like:
fun minimum [] = raise Empty
| minimum [x] = x
| minimum (x::xs) = Int.min (x, minimum xs)
Converting a recursive function to a fold
Given some recursive function foo
that depends on some function bar
and some default value acc
:
fun foo [] = acc
| foo (x::xs) = bar (x, foo xs)
you may notice the similarities between minimum
and foo
:
acc
is x
, some minimum value
bar
is Int.min
.
This is an attempt to generalise the recursion scheme of minimum
.
Given the function foldr
:
fun foldr f e [] = e
| foldr f e (x::xs) = f (x, foldr f e xs);
you may notice the same similarities:
f
is bar
, but made into a parameter
e
is acc
, but made into a parameter
The only thing from minimum
that does not fit this general recursion scheme is handling the empty list. So you still have to do that separate from foldr
:
fun minimum [] = ...
| minimum (x::xs) = foldr ...
But the rest is similar.
Error-aware return types
A third option would be to change the type signature of the function into
val minimum : int list -> int option
which your current exercise perhaps disallows.
Writing this without foldr
it might look like:
fun minimum [] = NONE
| minimum [x] = SOME x
| minimum (x::xs) =
case minimum xs of
NONE => SOME x
| SOME y => SOME (Int.min (x, y))
or better yet:
fun minimum [] = NONE
| minimum [x] = SOME x
| minimum (x::xs) = Option.map (fn y => Int.min (x, y)) (minimum xs)
Converting this function to use foldr
is the same process, but with a different f
.
Tail recursion
The minimum
function without folding (repeated from above):
fun minimum [] = raise Empty
| minimum [x] = x
| minimum (x::xs) = Int.min (x, minimum xs)
has a problem being that it mainly uses stack memory.
This can be illustated by evaluating the function by hand:
minimum [1,2,3,4,5]
~> Int.min (1, minimum [2,3,4,5])
~> Int.min (1, Int.min (2, minimum [3,4,5]))
~> Int.min (1, Int.min (2, Int.min (3, minimum [4,5])))
~> Int.min (1, Int.min (2, Int.min (3, Int.min (4, minimum [5]))))
~> Int.min (1, Int.min (2, Int.min (3, Int.min (4, 5))))
~> Int.min (1, Int.min (2, Int.min (3, 4)))
~> Int.min (1, Int.min (2, 3))
~> Int.min (1, 2)
~> 1
Since the outer Int.min
cannot be calculated before the recursive call has returned, the amount of stack memory used to compute the function grows proportional to the length of the list.
You can avoid this by using an accumulating argument:
fun minimum [] = raise Empty
| minimum (y::ys) =
let fun helper [] acc = acc
| helper (x::xs) acc = helper xs (Int.min (x, acc))
in helper ys y end
Evaluating this function by hand:
minimum [1,2,3,4,5]
~> helper [2,3,4,5] 1
~> helper [3,4,5] (Int.min (2, 1))
~> helper [3,4,5] 1
~> helper [4,5] (Int.min (3, 1))
~> helper [4,5] 1
~> helper [5] (Int.min (4, 1))
~> helper [5] 1
~> helper [] (Int.min (5, 1))
~> helper [] 1
~> 1
Since Int.min
is commutative, you might as well solve this exercise with foldl
instead of foldr
in the exact same way as you would have above, and you'd have a tail-recursive variant that uses less stack space.