【发布时间】:2020-11-25 22:27:49
【问题描述】:
我想创建一个函数remove_duplicates 接受任何类型的list(例如可以是int list 或bool list 或int list list 或whatever list)并返回相同的值没有重复的列表,这在标准 ML 中是否可行?
【问题讨论】:
标签: functional-programming sml smlnj ml mosml
我想创建一个函数remove_duplicates 接受任何类型的list(例如可以是int list 或bool list 或int list list 或whatever list)并返回相同的值没有重复的列表,这在标准 ML 中是否可行?
【问题讨论】:
标签: functional-programming sml smlnj ml mosml
在标准 ML 中是否有一个函数可以获取任何类型的列表并返回不重复的列表?
没有。
要确定一个元素是否与另一个元素重复,它们的值必须是可比较的。 “任何类型”,或标准 ML 中的 'a,对于相等性是不可比较的。因此,虽然您不能拥有删除重复项的 val nub : 'a list -> 'a list,但这里有四个替代选项:
@qouify 建议,内置相等类型''a,所以你可以在= 上使用任何东西:
val nub : ''a list -> ''a list
@kopecs 建议,一个以相等运算符作为参数的函数:
val nub : ('a * 'a -> bool) -> 'a list -> 'a list
这是 1. 的概括,因为这里是 nub op= : ''a list -> ''a list。这个解决方案很简洁,因为它不仅可以删除重复项,还可以删除任意等价类的冗余代表,例如nub (fn (x, y) => (x mod 3) = (y mod 3)) 只会保留模 3 不同的整数。但它的复杂度是 O(n²)。 (-_- )ノ⌒┻━┻
因为是O(n²),nub is considered harmful.
正如文章还建议的那样,替代方法是使用排序而不是相等来将复杂度降低到 O(n log n)。在 Haskell 中,这意味着只更改类型类约束:
nub :: Eq a => [a] -> [a]
nubOrd :: Ord a => [a] -> [a]
并调整算法,在 SML 中表达这个约束变得有点复杂。虽然我们确实有''a代表Eq a => a(我们可以在输入中使用=),但我们不有类似的特殊语法支持可以比较为小于/等于/大于的元素,而且我们也没有类型类。我们确实有以下内置订单类型:
datatype order = LESS | EQUAL | GREATER
因此,如果您喜欢 kopecs 的解决方案,运行时间更长的变体是:
val nubOrd : ('a * 'a -> order) -> 'a list -> 'a list
因为它可以使用类似以前见过的元素的数学集合,使用某种平衡搜索树来实现; n 插入每个复杂度 O(log n) 总共需要 O(n log n) 个步骤。
SML 的一大特色是其可组合的模块系统。您可以创建一个将另一个模块作为参数的模块(函子),而不是使用参数多态性并为函数nubOrd 提供顺序比较函数。
首先,让我们为表示类型排序的模块定义一个签名:
signature ORD =
sig
type t
val compare : t * t -> order
end
(注意t前面没有'。)
这意味着任何人都可以通过为ts 指定t 和相应的compare 函数来创建struct ... end : ORD。许多内置类型都有预定义的compare 函数:int 有Int.compare,real 有Real.compare。
然后,定义一个基于树的集合数据结构;我使用了二叉搜索树,并且跳过了大多数功能,但执行此壮举所必需的功能。理想情况下,您可以扩展接口并使用更好的树类型,例如自平衡树。 (不幸的是,由于您已将此 Q&A 标记为 SML/NJ 和 Moscow ML,我不确定使用哪个模块,因为它们在平衡树方面以不同的方式扩展了标准库。)
functor TreeSet (X : ORD) =
struct
type t = X.t
datatype 'a tree = Leaf | Branch of 'a tree * 'a * 'a tree
val empty = Leaf
fun member (x, Leaf) = false
| member (x, Branch (left, y, right)) =
case X.compare (x, y) of
EQUAL => true
| LESS => member (x, left)
| GREATER => member (x, right)
fun insert (x, Leaf) = Branch (Leaf, x, Leaf)
| insert (x, Branch (left, y, right)) =
case X.compare (x, y) of
EQUAL => Branch (left, y, right)
| LESS => Branch (insert (x, left), y, right)
| GREATER => Branch (left, y, insert (x, right))
end
最后,ListUtils 函子包含nubOrd 实用函数。仿函数采用结构 X : ORD 就像 TreeSet 仿函数一样。它通过使用相同的排序模块专门化TreeSet 函子来创建XSet 结构。然后它使用这个XSet 来有效地记录它之前看到的元素。
functor ListUtils (X : ORD) =
struct
structure XSet = TreeSet(X)
fun nubOrd (xs : X.t list) =
let
val init = ([], XSet.empty)
fun go (x, (ys, seen)) =
if XSet.member (x, seen)
then (ys, seen)
else (x::ys, XSet.insert (x, seen))
in rev (#1 (foldl go init xs))
end
end
使用此函子删除 int list 中的重复项:
structure IntListUtils = ListUtils(struct
type t = int
val compare = Int.compare
end)
val example = IntListUtils.nubOrd [1,1,2,1,3,1,2,1,3,3,2,1,4,3,2,1,5,4,3,2,1]
(* [1, 2, 3, 4, 5] *)
所有这些混乱的目的是没有直接的额外函数参数的nubOrd。
不幸的是,为了将其扩展到int list list,您需要为该类型创建compare 函数,因为与Int.compare 不同,标准库中也没有可用的通用函数。 (这就是 Haskell 更符合人体工程学的地方。)
所以你可能会去写一个通用的字典列表比较函数:如果你知道如何比较'a类型的两个元素,你就知道如何比较这两个列表,不管元素类型是什么:
fun listCompare _ ([], []) = EQUAL (* empty lists are equal *)
| listCompare _ ([], ys) = LESS (* empty is always smaller than non-empty *)
| listCompare _ (xs, []) = GREATER (* empty is always smaller than non-empty *)
| listCompare compare (x::xs, y::ys) =
case compare (x, y) of
EQUAL => listCompare compare (xs, ys)
| LESS => LESS
| GREATER => GREATER
现在,
structure IntListListUtils = ListUtils(struct
type t = int list
val compare = listCompare Int.compare
end)
val example2 = IntListListUtils.nubOrd [[1,2,3],[1,2,3,2],[1,2,3]]
(* [[1,2,3],[1,2,3,2]] *)
因此,即使 [1,2,3] 和 [1,2,3,2] 包含重复项,当您 compare 时,它们也不是 EQUAL。但是第三个元素 是 EQUAL 到第一个元素,因此它被作为副本删除。
最后的一些观察:
您可能会认为,即使每个 compare 只运行 O(log n) 次,对于某些复杂的数据结构(例如 (whatever * int) list list)来说,单个 compare 可能仍然是昂贵的。因此,您可以在此处进行的另一项改进是缓存每个 compare 输出的结果,这实际上是 Haskell 的 nubOrdOn 运算符所做的。 ┳━┳ヽ(ಠل͜ಠ)ノ
函子方法在Jane Street's OCaml Base library 中广泛使用。快速的解决方案是在您每次nub 时传递一个'a * 'a -> order 函数。不过,一个道理是,虽然模块系统确实增加了冗长,但如果您在标准库中提供足够多的这种机制,它将变得非常方便。
如果您认为从 O(n²) 到 O(n log n) 的改进还不够,请考虑 Fritz Henglein 的 Generic top-down discrimination for sorting and partitioning in linear time (2012) 和 Edward Kmett 的Haskell discrimination package's nub 用于 O(n) nub。
【讨论】:
nub 有二次运行时有这么大吗?我们所有的软件都需要以最佳速度运行吗?
functors,但始终是具体模块。
是的,sml 提供了多态性来做这些事情。在许多情况下,您实际上并不关心列表(或其他结构)中项目的类型。例如,此函数检查(已经存在于List 结构中)列表中是否存在项目:
fun exists _ [] = false
| exists x (y :: l) = x = y orelse exists x l
这样的函数适用于任何类型的列表,只要为这种类型定义了相等运算符(这种类型称为相等类型)。您可以对remove_duplicates 执行相同的操作。为了处理非相等类型的项目列表,您必须为remove_duplicates 提供一个额外的函数来检查两个项目是否相等。
【讨论】:
是的。通过使用参数多态性,这在 SML 中是可能的。您想要一个最通用类型的函数'a list -> 'a list,其中'a 是一个类型变量(即,跨越类型的变量),它会被读取为 alpha。
有关如何应用它的一些更具体的示例(fun 之后的显式类型变量是可选的):
fun 'a id (x : 'a) : 'a = x
这里我们有类型为'a -> 'a的标识函数。
例如,我们可以声明具有某种程度特殊化类型的类似函数
fun map _ [] = []
| map f (x::xs) = f x :: map f xs
其中map 具有最通用的类型('a -> 'b) -> 'a list -> 'b list,即采用两个柯里化参数,一个具有某种函数类型,另一个具有某种列表类型(与函数的域一致)并返回一个具有由codomain 给出的类型的新列表函数。
对于您的具体问题,您可能还需要使用相等函数来确定什么是“重复”,或者您可能会将自己限制为“相等类型”(可以与 op= 进行比较的类型,由带有两个前导撇号的类型变量表示,例如''a)。
【讨论】: