Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between "local" and "let" in SML

Tags:

let

local

sml

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?

like image 885
Johan Avatar asked Sep 19 '16 16:09

Johan


2 Answers

(TL;DR)

  1. Use case ... of ... when you only have one temporary binding.
  2. Use let ... in ... end for very specific helper functions.
  3. Never use local ... in ... end. Use opaque modules instead.

Adding some thoughts on use-cases to sepp2k's fine answer:

  • (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
    

So let's focus on when it is particularly useful to use one or the other.

  • 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:

    1. A binding is computed once per function call (even when used multiple times).
    2. A binding can simultaneously be deconstructed (into left and right here).
    3. The declaration's scope is limited. (Same argument as for local ... in ... end.)
    4. Inner functions may use the arguments of the outer function, or the outer function itself.
    5. Multiple bindings that depend on each other may neatly be lined up.


    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.

Some additional opinions

  1. (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.

  2. 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,

    1. If a declaration (e.g. function) will be re-used, don't hide it.
    2. If not, the point of local ... in ... end over let ... in ... end is void.
  3. (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
    
like image 51
sshine Avatar answered Oct 18 '22 08:10

sshine


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.

like image 26
Andreas Rossberg Avatar answered Oct 18 '22 07:10

Andreas Rossberg