По каким причинам протоколы и мультиметоды в 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, обычно путем помещения аргумента в новый тип для отправки.
Несколько вещей, которые нельзя сделать с помощью мультиметодов, насколько мне известно:
- Отправка по типу возврата
- Хранить полиморфные значения для всех типов классов типов
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)]
...
Такая отправка явно невозможна с мульти-методами, которые должны посылать свои аргументы.
Смотрите также мое более обширное сравнение с простым ОО.