问题描述
我正在阅读《机器学习中的现代编译器实现》一书,同时学习 sml。
我被第一章中的一个练习(1.1.b)难住了。 我们被要求实现一个维护键/值对的二叉树, 键是字符串,值是参数化类型。 我的数据类型定义如下
type key = string
datatype 'a tree = LEAF | TREE of 'a tree * key * 'a * 'a tree
我们被要求实现一个类型为 lookup
的 'a tree * key -> 'a
函数。
我不知道如何实现这个函数,因为我不知道当树是 LEAF
时要返回什么。 'a
没有默认值。书中的说明没有说明找不到密钥时的处理方法,但它坚持该函数必须返回类型 'a
。
这只是书中的错误,还是在 sml 中有正确的方法可以做到这一点?
在这种情况下,我想尝试引发异常,但编译器显然不会让我在不捕获异常的情况下抛出异常。
如果我在 Scala 中实现它,我会将返回类型更改为 Option[A]
并在未找到键时返回 None
;或者在 Common Lisp 中,我会传递一个 continuation 以使用找到的值进行调用,然后如果未找到键就不会调用 continuation。
这是我的代码,尚未运行。
(* page 12,exercise 1.1 b *)
type key = string
datatype 'a tree = LEAF | TREE of 'a tree * key * 'a * 'a tree
val empty = LEAF
fun insert(key,value,LEAF) = TREE(LEAF,key,LEAF)
| insert(key,TREE(l,k,v,r)) =
if key<k
then TREE(insert(key,l),r)
else if key>k
then TREE(l,insert(key,r))
else TREE(l,r)
fun lookup(LEAF,key) = (* !!!HELP!!! I don't kNow what to do in this case *)
| lookup(TREE(l,r),key) = if k=key
then v
else if key<k
then lookup(l,key)
else lookup(r,key)
val t1 = insert("a",1,empty)
val t2 = insert("c",2,t1)
val t3 = insert("b",3,t2)
;
lookup(t3,"a");
lookup(t3,"b");
lookup(t3,"c");
lookup(t3,"d");
顺便说一句,我不明白为什么 emacs sml-mode 坚持缩进对 lookup
的调用。
解决方法
您绝对可以将 lookup
更改为使用 'a option
。
这是一个基本的字典实现,它使用不透明的模块和类型参数而不是模块参数。您派生的示例使用了一个混合体,其中 key
是一个模块参数,而 'a
作为值仍然是多态的。这通常是一个好主意,因为您在实施方面具有更大的灵活性。但由于您只是将列表用作字典,因此您可以使用 ''key
表示可比较相等的类型参数。 (如果您想要更高效的表示,例如树或哈希图,则需要更大的接口。)
signature DICT = sig
type (''key,'value) dict
val empty : (''key,'value) dict
val insert : ''key -> 'value -> (''key,'value) dict -> (''key,'value) dict
val lookup : ''key -> (''key,'value) dict -> 'value option
end
structure Dict :> DICT = struct
type (''key,'value) dict = (''key * 'value) list
val empty = []
fun insert k v [] = [(k,v)]
| insert k v ((k2,v2)::dict) =
if k = k2
then (k,v)::dict
else (k2,v2)::insert k v dict
fun lookup k dict = Option.map #2 (List.find (fn (k2,v2) => k = k2) dict)
end
现在
- val demo = Dict.lookup "foo" (Dict.insert "foo" 42 Dict.empty)
> val demo = SOME 42 : int option
val t1 = insert("a",1,empty)
val t2 = insert("c",2,t1)
val t3 = insert("b",3,t2)
; X
lookup(t3,"a");
lookup(t3,"b");
lookup(t3,"c");
lookup(t3,"d");
顺便说一句,我不明白为什么 emacs sml-mode 坚持缩进查找调用。
那是因为 sml-mode 认为 ;
是表达式运算符而不是声明分隔符。我已经在其中放置了一个 X
,如果它是表达式运算符,那么在这里放置表达式是很自然的。并且做换行,后面的表达式放在同一个缩进点上是很自然的。
Emacs sml-mode 仅使用前一行来确定缩进级别,这使得它有些简单。您可以通过在所有声明前添加 val ... =
或以不那么随意的方式放置 ;
来避免在此处与缩进作斗争,例如
...
val t3 = insert ("b",t2)
val L1 = lookup(t3,"a")
val L2 = ...
或
...
val t3 = insert ("b",t2)
;lookup(t3,"a");
;lookup(t3,"b");
这里两边 ;
背后的想法是,当我们谈论声明分隔符(而不是表达式运算符)时,您可以任意重复多个 ;
,因此,因为 ;
不需要终止 val ...
声明,所以这些行不容易受到前后上下文更改的影响,就像 val x = ...
一样。只要您仅将 SML 代码作为脚本/在 REPL 中进行评估,这种风格才真正有意义。否则,如果您只对副作用感兴趣,您可能想要命名绑定或使用 val _ = ...
。