26

更广泛地说,这个问题是关于表达问题的各种方法。这个想法是您的程序是数据类型和对它的操作的组合。我们希望能够在不重新编译旧类的情况下添加新案例。

现在 Haskell对TypeClass的表达式问题有了一些非常棒的解决方案。特别是 - 我们可以这样做:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool


member :: (Eq a) => a -> [a] -> Bool
member y [] = False
member y (x:xs) = (x == y) || member y xs

现在在Clojure中有多种方法- 所以你可以这样做:

(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
    (* (:wd r) (:ht r)))
(defmethod area :Circle [c]
    (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def r (rect 4 13))
(def c (circle 12))
(area r)
-> 52
(area c)
-> 452.3893421169302
(area {})
-> :oops

同样在 Clojure 中,您有协议- 您可以使用这些协议:

(defprotocol P
  (foo [x])
  (bar-me [x] [x y]))

(deftype Foo [a b c]
  P
  (foo [x] a)
  (bar-me [x] b)
  (bar-me [x y] (+ c y)))

(bar-me (Foo. 1 2 3) 42)
=> 45

(foo
 (let [x 42]
   (reify P
     (foo [this] 17)
     (bar-me [this] x)
     (bar-me [this y] x))))

=> 17

现在这个人提出索赔

但是,有协议和多种方法。它们非常强大,但不如 Haskell 的类型类强大。您可以通过在协议中指定您的合同来引入类型类之类的东西。这仅在第一个参数上分派,而 Haskell 可以在整个签名上分派,包括返回值。多方法比协议更强大,但不如 Haskell 的调度强大。

我的问题是:Clojure 中的协议和多方法在多态性方面不如 Haskell 中的类型类强大的原因是什么?

4

2 回答 2

24

很明显,协议只能在第一个且只有第一个参数上分派。这意味着它们大致相当于

 class Foo a where
   bar  :: a -> ...
   quux :: a -> ...
   ...

wherea 必须是第一个参数。Haskell 的类型类a可以出现在函数的任何地方。因此,协议的表达能力很容易不如类型类。

接下来是多方法。如果我没记错的话,多方法允许基于所有参数的函数进行调度。这在某些方面看起来比 Haskell 更具表现力,因为您可以以不同的方式分派相同类型的参数。然而,这实际上可以在 Haskell 中完成,通常是通过将参数包装在一个新类型中进行调度。

据我所知,使用多种方法无法完成的一些事情:

  1. 派送返回类型
  2. 存储类型类的所有类型的多态值forall a. Foo a => a

要了解 1. 如何发挥作用,请考虑Monoid它具有 value mempty :: Monoid m => m。它不是一个函数,用 Clojure 模拟它是不可能的,因为我们没有任何关于我们应该选择什么方法的类型信息。

对于 2. 考虑read :: Read a => String -> a。在 Haskell 中,我们实际上可以创建一个具有 type 的列表[forall a. Read a => a],我们基本上推迟了计算,现在我们可以运行和重新运行列表的元素以尝试将它们读取为不同的值。

类型类也具有静态类型,因此需要进行一些检查以确保您不会在没有实例静态调用的情况下最终“卡住”,但 Clojure 是动态类型的,因此我将其归结为两者之间的风格差异一种或另一种语言,而不是一种特定的优势。当然,类型类的开销也比多方法少得多,因为通常可以内联见证记录并且静态解决所有问题。

于 2014-02-26T11:56:22.803 回答
8

最根本的区别在于,对于类型类,调度是在类型上而不是在上。执行它不需要任何值。这允许更一般的情况。最明显的例子是在(部分)函数的结果类型上分派。考虑例如 Haskell 的 Read 类:

class Read a where
  readsPrec :: Int -> String -> [(a, String)]
  ...

对于必须根据参数进行调度的多方法,这种调度显然是不可能的。

另请参阅我与普通 OO 的更广泛比较

于 2014-02-26T12:29:52.020 回答