SML 中 "local" 和 "let" 的区别
Difference between "local" and "let" in SML
对于 SML 中 "local" 和 "let" 关键字之间的区别,我找不到初学者友好的答案。有人可以提供一个简单的例子并解释何时使用一个吗?
(TL;DR)
- 只有一个临时绑定时使用
case ... of ...
。
- 使用
let ... in ... end
实现非常具体的辅助函数。
- 永远不要使用
local ... in ... end
。请改用不透明模块。
将一些关于用例的想法添加到 sepp2k's fine answer:
(总结) local ... in ... end
是一个声明,let ... in ... end
是一个表达式,因此有效地限制了它们可以使用的地方: 允许声明的地方(例如在顶层或模块内部),以及内部值声明(val
和 fun
)。
那又怎样?通常似乎两者都可以使用。例如,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
的一些好处:
- 每个函数调用计算一次绑定(即使多次使用)。
- 一个绑定可以同时被解构(这里是
left
和right
)。
- 声明的范围有限。 (与
local ... in ... end
相同的论点。)
- 内部函数可以使用外部函数的参数,或者外部函数本身。
- 相互依赖的多个绑定可以整齐地排列起来。
等等... 真的,let-expressions 非常好。
当一个辅助函数被使用一次时,你不妨将它嵌套在一个let ... in ... end
.
中
特别是如果其他原因也适用。
一些补充意见
(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
无关。
隐藏不会在别处使用的声明是明智的。例如
(* 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
等等。基本上,
- 如果声明(例如函数)将被重复使用,请不要隐藏它。
- 如果不是,
local ... in ... end
超过 let ... in ... end
的分数无效。
(local ... in ... end
没用。)
您永远不想使用 local ... in ... end
。由于它的工作是将一组辅助声明隔离到您的主要声明的一个子集,这会迫使您根据它们所依赖的内容对这些主要声明进行分组,而不是按照更理想的顺序进行分组。
一个更好的选择是简单地编写一个结构,给它一个签名并使该签名不透明。这样,所有内部声明都可以在整个模块中自由使用,而无需导出。
j4cbo SML on Stilts web-framework 中的一个例子是 StaticServer 模块:它仅导出 val server : ...
,即使该结构也包含两个声明 structure U = WebUtil
和 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
简短的回答是:local
是一个声明,let
是一个表达式。因此,它们用于不同的句法上下文,local
需要在 in
和 end
之间声明,而 let
需要那里的表达式。没那么深了。
正如@SimonShine 提到的,local
通常不鼓励使用模块。
对于 SML 中 "local" 和 "let" 关键字之间的区别,我找不到初学者友好的答案。有人可以提供一个简单的例子并解释何时使用一个吗?
(TL;DR)
- 只有一个临时绑定时使用
case ... of ...
。 - 使用
let ... in ... end
实现非常具体的辅助函数。 - 永远不要使用
local ... in ... end
。请改用不透明模块。
将一些关于用例的想法添加到 sepp2k's fine answer:
(总结)
local ... in ... end
是一个声明,let ... in ... end
是一个表达式,因此有效地限制了它们可以使用的地方: 允许声明的地方(例如在顶层或模块内部),以及内部值声明(val
和fun
)。那又怎样?通常似乎两者都可以使用。例如,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
的一些好处:- 每个函数调用计算一次绑定(即使多次使用)。
- 一个绑定可以同时被解构(这里是
left
和right
)。 - 声明的范围有限。 (与
local ... in ... end
相同的论点。) - 内部函数可以使用外部函数的参数,或者外部函数本身。
- 相互依赖的多个绑定可以整齐地排列起来。
等等... 真的,let-expressions 非常好。当一个辅助函数被使用一次时,你不妨将它嵌套在一个
中let ... in ... end
.特别是如果其他原因也适用。
一些补充意见
(
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
无关。隐藏不会在别处使用的声明是明智的。例如
(* 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
等等。基本上,
- 如果声明(例如函数)将被重复使用,请不要隐藏它。
- 如果不是,
local ... in ... end
超过let ... in ... end
的分数无效。
(
local ... in ... end
没用。)您永远不想使用
local ... in ... end
。由于它的工作是将一组辅助声明隔离到您的主要声明的一个子集,这会迫使您根据它们所依赖的内容对这些主要声明进行分组,而不是按照更理想的顺序进行分组。一个更好的选择是简单地编写一个结构,给它一个签名并使该签名不透明。这样,所有内部声明都可以在整个模块中自由使用,而无需导出。
j4cbo SML on Stilts web-framework 中的一个例子是 StaticServer 模块:它仅导出
val server : ...
,即使该结构也包含两个声明structure U = WebUtil
和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
简短的回答是:local
是一个声明,let
是一个表达式。因此,它们用于不同的句法上下文,local
需要在 in
和 end
之间声明,而 let
需要那里的表达式。没那么深了。
正如@SimonShine 提到的,local
通常不鼓励使用模块。