如何在sml中部分实现功能

问题描述

我正在阅读《机器学习中的现代编译器实现》一书,同时学习 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 _ = ...