I couldn't find a beginner friendly answer to what the difference between the "local" and "let" keywords in SML is. Could someone provide a simple example please and explain when one is used over the other?
case ... of ...
when you only have one temporary binding.let ... in ... end
for very specific helper functions.local ... in ... end
. Use opaque modules instead.(Summary) local ... in ... end
is a declaration and let ... in ... end
is an expression, so that effectively limits where they can be used: Where declarations are allowed (e.g. at the top level or inside a module), and inside value declarations (val
and fun
), respectively.
But so what? It often seems that either can be used. The Rosetta Stone QuickSort code, for example, could be structured using either, since the helper functions are only used once:
(* First using local ... in ... end *)
local
fun par_helper([], x, l, r) = (l, r)
| par_helper(h::t, x, l, r) =
if h <= x
then par_helper(t, x, l @ [h], r)
else par_helper(t, x, l, r @ [h])
fun par(l, x) = par_helper(l, x, [], [])
in
fun quicksort [] = []
| quicksort (h::t) =
let
val (left, right) = par(t, h)
in
quicksort left @ [h] @ quicksort right
end
end
(* Second using let ... in ... end *)
fun quicksort [] = []
| quicksort (h::t) =
let
fun par_helper([], x, l, r) = (l, r)
| par_helper(h::t, x, l, r) =
if h <= x
then par_helper(t, x, l @ [h], r)
else par_helper(t, x, l, r @ [h])
fun par(l, x) = par_helper(l, x, [], [])
val (left, right) = par(t, h)
in
quicksort left @ [h] @ quicksort right
end
local ... in ... end
is mainly used when you have one or more temporary declarations (e.g. helper functions) that you want to hide after they're used, but they should be shared between multiple non-local declarations. E.g.
(* Helper function shared across multiple functions *)
local
fun par_helper ... = ...
fun par(l, x) = par_helper(l, x, [], [])
in
fun quicksort [] = []
| quicksort (h::t) = ... par(t, h) ...
fun median ... = ... par(t, h) ...
end
If there weren't multiple, you could have used a let ... in ... end
instead.
You can always avoid using local ... in ... end
in favor of opaque modules (see below).
let ... in ... end
is mainly used when you want to compute temporary results, or deconstruct values of product types (tuples, records), one or more times inside a function. E.g.
fun quicksort [] = []
| quicksort (x::xs) =
let
val (left, right) = List.partition (fn y => y < x) xs
in
quicksort left @ [x] @ quicksort right
end
Here are some of the benefits of let ... in ... end
:
left
and right
here).local ... in ... end
.)
And so on... Really, let-expressions are quite nice.
When a helper function is used once, you might as well nest it inside a let ... in ... end
.
Especially if other reasons for using one applies, too.
(case ... of ...
is awesome, too.)
When you have only one let ... in ... end
you can instead write e.g.
fun quicksort [] = []
| quicksort (x::xs) =
case List.partition (fn y => y < x) xs of
(left, right) => quicksort left @ [x] @ quicksort right
These are equivalent. You might like the style of one or the other. The case ... of ...
has one advantage, though, being that it also work for sum types ('a option
, 'a list
, etc.), e.g.
(* Using case ... of ... *)
fun maxList [] = NONE
| maxList (x::xs) =
case maxList xs of
NONE => SOME x
| SOME y => SOME (Int.max (x, y))
(* Using let ... in ... end and a helper function *)
fun maxList [] = NONE
| maxList (x::xs) =
let
val y_opt = maxList xs
in
Option.map (fn y => Int.max (x, y)) y_opt
end
The one disadvantage of case ... of ...
: The pattern block does not stop, so nesting them often requires parentheses. You can also combine the two in different ways, e.g.
fun move p1 (GameState old_p) gameMap =
let val p' = addp p1 old_p in
case getMapPos p' gameMap of
Grass => GameState p'
| _ => GameState old_p
end
This isn't so much about not using local ... in ... end
, though.
Hiding declarations that won't be used elsewhere is sensible. E.g.
(* if they're overly specific *)
fun handvalue hand =
let
fun handvalue' [] = 0
| handvalue' (c::cs) = cardvalue c + handvalue' cs
val hv = handvalue' hand
in
if hv > 21 andalso hasAce hand
then handvalue (removeAce hand) + 1
else hv
end
(* to cover over multiple arguments, e.g. to achieve tail-recursion, *)
(* or because the inner function has dependencies anyways (here: x). *)
fun par(ys, x) =
let fun par_helper([], l, r) = (l, r)
| par_helper(h::t, l, r) =
if h <= x
then par_helper(t, l @ [h], r)
else par_helper(t, l, r @ [h])
in par_helper(ys, [], []) end
And so on. Basically,
local ... in ... end
over let ... in ... end
is void.(local ... in ... end
is useless.)
You never want to use local ... in ... end
. Since its job is to isolate one set of helper declarations to a subset of your main declarations, this forces you to group those main declarations according to what they depend on, rather than perhaps a more desired order.
A better alternative is simply to write a structure, give it a signature and make that signature opaque. That way, all internal declarations can be used freely throughout the module without being exported.
One example of this in j4cbo's SML on Stilts web-framework is the module StaticServer: It exports only val server : ...
, even though the structure also holds the two declarations structure U = WebUtil
and val content_type = ...
.
structure StaticServer :> sig
val server: { basepath: string,
expires: LargeInt.int option,
headers: Web.header list } -> Web.app
end = struct
structure U = WebUtil
val content_type = fn
"png" => "image/png"
| "gif" => "image/gif"
| "jpg" => "image/jpeg"
| "css" => "text/css"
| "js" => "text/javascript"
| "html" => "text/html"
| _ => "text/plain"
fun server { basepath, expires, headers } (req: Web.request) = ...
end
The short answer is: local
is a declaration, let
is an expression. Consequently, they are used in different syntactic contexts, and local
requires declarations between in
and end
, while let
requires an expression there. It's not much deeper than that.
As @SimonShine mentioned, local
is often discouraged in favour of using modules.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With