По каким причинам протоколы и мультиметоды в Clojure менее эффективны для полиморфизма, чем классы типов в Haskell?

В более широком смысле этот вопрос касается различных подходов к проблеме выражения. Идея состоит в том, что ваша программа представляет собой комбинацию типа данных и операций над ним. Мы хотим иметь возможность добавлять новые случаи без перекомпиляции старых классов.

Теперь у 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?

2 ответа

Решение

Очевидно, что протоколы могут отправлять сообщения только по первому и только первому аргументам. Это означает, что они примерно эквивалентны

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

куда a должен быть первым аргументом. Классы типов в Haskell a появляются в любом месте функции. Так что протоколы легко менее выразительны, чем классы типов.

Далее идет мультиметоды. Мультиметоды, если я не ошибаюсь, разрешают диспетчеризацию на основе функции всех аргументов. В некотором смысле это выглядит более выразительно, чем в Haskell, поскольку вы можете отправлять аргументы одного и того же типа по-разному. Тем не менее, это может быть сделано в Haskell, обычно путем помещения аргумента в новый тип для отправки.

Несколько вещей, которые нельзя сделать с помощью мультиметодов, насколько мне известно:

  1. Отправка по типу возврата
  2. Хранить полиморфные значения для всех типов классов типов forall a. Foo a => a

Чтобы увидеть, как 1. вступает в игру, рассмотрим Monoid это имеет значение mempty :: Monoid m => m, Это не функция, и имитировать это невозможно с Clojure, поскольку у нас нет никакой информации о типе того, какой метод мы должны выбрать.

Для 2. рассмотреть read :: Read a => String -> a, В Haskell мы могли бы создать список, который имеет тип [forall a. Read a => a]мы по сути отложили вычисления и теперь можем запускать и перезапускать элементы списка, чтобы попытаться прочитать их как разные значения.

Классы типов также имеют статические типы, так что есть некоторая проверка, чтобы убедиться, что вы не будете в конечном итоге "зависать" без экземпляра для статического вызова, но Clojure типизируется динамически, поэтому я объясню это до разницы в стиле между двумя языки, а не конкретное преимущество, так или иначе. Также, конечно, преимущество заключается в том, что классы типов имеют намного меньше накладных расходов, чем мультиметоды, поскольку обычно запись свидетеля может быть встроенной, и все разрешается статически.

Самым фундаментальным отличием является то, что с классами типов диспетчеризация выполняется для типов, а не для значений. Никакого значения не требуется для его выполнения. Это позволяет гораздо более общие случаи. Наиболее очевидный пример - отправка (часть) типа результата функции. Рассмотрим, например, класс чтения Haskell:

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

Такая отправка явно невозможна с мульти-методами, которые должны посылать свои аргументы.

Смотрите также мое более обширное сравнение с простым ОО.

Другие вопросы по тегам