SML 中 "local" 和 "let" 的区别

Difference between "local" and "let" in SML

对于 SML 中 "local" 和 "let" 关键字之间的区别,我找不到初学者友好的答案。有人可以提供一个简单的例子并解释何时使用一个吗?

(TL;DR)

  1. 只有一个临时绑定时使用case ... of ...
  2. 使用 let ... in ... end 实现非常具体的辅助函数。
  3. 永远不要使用 local ... in ... end。请改用不透明模块。

将一些关于用例的想法添加到 sepp2k's fine answer:

  • (总结) local ... in ... end是一个声明,let ... in ... end是一个表达式,因此有效地限制了它们可以使用的地方: 允许声明的地方(例如在顶层或模块内部),以及内部值声明(valfun)。

    那又怎样?通常似乎两者都可以使用。例如,Rosetta Stone QuickSort code 可以使用其中任何一种来构造,因为辅助函数只使用一次:

    (* 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 主要用于当你有一个或多个临时声明(例如辅助函数),你想在它们使用后隐藏,但它们应该在 多个 非本地声明。例如

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

    如果没有多个,您可以使用 let ... in ... end

    您总是可以避免使用 local ... in ... end 以支持 不透明模块 (见下文)。

  • let ... in ... end 主要用于计算临时结果,或解构产品类型(元组、记录)的值,在函数内执行一次或多次。例如

    fun quicksort [] = []
      | quicksort (x::xs) =
        let
          val (left, right) = List.partition (fn y => y < x) xs
        in
          quicksort left @ [x] @ quicksort right
        end
    

    以下是 let ... in ... end 的一些好处:

    1. 每个函数调用计算一次绑定(即使多次使用)。
    2. 一个绑定可以同时被解构(这里是leftright)。
    3. 声明的范围有限。 (与 local ... in ... end 相同的论点。)
    4. 内部函数可以使用外部函数的参数,或者外部函数本身。
    5. 相互依赖的多个绑定可以整齐地排列起来。


    等等... 真的,let-expressions 非常好。

    当一个辅助函数被使用一次时,你不妨将它嵌套在一个let ... in ... end.

    特别是如果其他原因也适用。

一些补充意见

  1. case ... of ...也很棒。)

    当你只有一个 let ... in ... end 时,你可以改为写

    fun quicksort [] = []
      | quicksort (x::xs) =
        case List.partition (fn y => y < x) xs of
          (left, right) => quicksort left @ [x] @ quicksort right
    

    这些是等价的。您可能喜欢其中一种风格。不过,case ... of ... 有一个优点,即它也适用于 sum types'a option'a list 等),例如

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

    case ... of ...的一个缺点:模式块不会停止,因此嵌套它们通常需要括号。您还可以以不同的方式将两者结合起来,例如

    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
    

    这与 不是 使用 local ... in ... end 无关。

  2. 隐藏不会在别处使用的声明是明智的。例如

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

    等等。基本上,

    1. 如果声明(例如函数)将被重复使用,请不要隐藏它。
    2. 如果不是,local ... in ... end 超过 let ... in ... end 的分数无效。
  3. local ... in ... end没用。)

    您永远不想使用 local ... in ... end。由于它的工作是将一组辅助声明隔离到您的主要声明的一个子集,这会迫使您根据它们所依赖的内容对这些主要声明进行分组,而不是按照更理想的顺序进行分组。

    一个更好的选择是简单地编写一个结构,给它一个签名并使该签名不透明。这样,所有内部声明都可以在整个模块中自由使用,而无需导出。

    j4cbo SML on Stilts web-framework 中的一个例子是 StaticServer 模块:它仅导出 val server : ...,即使该结构也包含两个声明 structure U = WebUtilval 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
    

简短的回答是:local是一个声明,let是一个表达式。因此,它们用于不同的句法上下文,local 需要在 inend 之间声明,而 let 需要那里的表达式。没那么深了。

正如@SimonShine 提到的,local 通常不鼓励使用模块。