【问题标题】:Is it possible to create a "generic" function in Standard ML?是否可以在标准 ML 中创建“通用”函数?
【发布时间】:2020-11-25 22:27:49
【问题描述】:

我想创建一个函数remove_duplicates 接受任何类型的list(例如可以是int listbool listint list listwhatever list)并返回相同的值没有重复的列表,这在标准 ML 中是否可行?

【问题讨论】:

    标签: functional-programming sml smlnj ml mosml


    【解决方案1】:

    在标准 ML 中是否有一个函数可以获取任何类型的列表并返回不重复的列表?

    没有。

    要确定一个元素是否与另一个元素重复,它们的值必须是可比较的。 “任何类型”,或标准 M​​L 中的 'a,对于相等性是不可比较的。因此,虽然您不能拥有删除重复项的 val nub : 'a list -> 'a list,但这里有四个替代选项:

    1. @qouify 建议,内置相等类型''a,所以你可以在= 上使用任何东西:

      val nub : ''a list -> ''a list
      
    2. @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²)。 (-_- )ノ⌒┻━┻

    3. 因为是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) 个步骤。

    4. SML 的一大特色是其可组合的模块系统。您可以创建一个将另一个模块作为参数的模块(函子),而不是使用参数多态性并为函数nubOrd 提供顺序比较函数。

      首先,让我们为表示类型排序的模块定义一个签名

      signature ORD =
      sig
        type t
        val compare : t * t -> order
      end
      

      (注意t前面没有'。)

      这意味着任何人都可以通过为ts 指定t 和相应的compare 函数来创建struct ... end : ORD。许多内置类型都有预定义的compare 函数:intInt.comparerealReal.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 有二次运行时有这么大吗?我们所有的软件都需要以最佳速度运行吗?
    • @ThorkilVærge:是的,也不是。
    • Nit:SML没有高阶模块系统,只有一阶模块系统。
    • @AndreasRossberg:感谢您指出这一点。 :) 我使用术语“高阶模块”作为“高阶函数”的类似物。我想您永远不会创建将任意模块作为参数的functors,但始终是具体模块。
    • @SimonShine,是的,在普通的 SML 中你不能做更多的事情。函子不能将函子作为参数。
    【解决方案2】:

    是的,sml 提供了多态性来做这些事情。在许多情况下,您实际上并不关心列表(或其他结构)中项目的类型。例如,此函数检查(已经存在于List 结构中)列表中是否存在项目:

    fun exists _ [] = false
      | exists x (y :: l) = x = y orelse exists x l
    

    这样的函数适用于任何类型的列表,只要为这种类型定义了相等运算符(这种类型称为相等类型)。您可以对remove_duplicates 执行相同的操作。为了处理非相等类型的项目列表,您必须为remove_duplicates 提供一个额外的函数来检查两个项目是否相等。

    【讨论】:

      【解决方案3】:

      是的。通过使用参数多态性,这在 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)。

      【讨论】:

        猜你喜欢
        • 2021-12-21
        • 2017-08-04
        • 2016-01-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-09
        • 1970-01-01
        相关资源
        最近更新 更多