Haskell: класс типов против передачи функции

16

Мне кажется, что вы всегда можете передавать аргументы функции, а не использовать класс типов. Например, вместо определения класса типов равенства:

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

И использование его в других функциях для указания аргумента типа должно быть экземпляром Eq:

elem                    :: (Eq a) => a -> [a] -> Bool

Разве мы не можем просто определить нашу elemфункцию без использования класса типов и вместо этого передать аргумент функции, который выполняет эту работу?

mahdix
источник
2
это называется передача словаря. Вы можете рассматривать ограничения класса типов как неявные аргументы.
Поскат
2
Вы можете сделать это, но, очевидно, гораздо удобнее не передавать функцию, а просто использовать «стандартную» в зависимости от типа.
Робин Зигмонд
2
Вы могли бы выразиться так, да. Но я бы сказал, что есть, по крайней мере, еще одно важное преимущество: возможность писать полиморфные функции, которые работают с любым типом, который реализует определенный «интерфейс» или набор функций. Я думаю, что ограничения класса типов выражают это очень четко, а не передача дополнительных аргументов функции. В частности, из-за (к сожалению, неявных) «законов», которым должны удовлетворять многие классы типов. Monad mОграничение говорит для меня больше , чем передачи дополнительных аргументов функции типов a -> m aи m a -> (a -> m b) -> m b.
Робин Зигмонд
1
Смотрите также, нужны ли классы типов?
Луки
1
TypeApplicationsРасширение позволяет сделать неявный аргумент явным. (==) @Int 3 5сравнивает 3и 5конкретно как Intценности. Вы можете думать о нем @Intкак о ключе в словаре функций равенства для конкретных типов, а не самой Intфункции сравнения.
chepner

Ответы:

19

Да. Это называется "стиль передачи словаря". Иногда, когда я делаю какие-то особенно сложные вещи, мне нужно отбросить класс типов и превратить его в словарь, потому что передача словаря является более мощной 1 , но часто довольно громоздкой, что делает концептуально простой код выглядящим довольно сложным. Иногда я использую стиль передачи словаря в языках, которые не являются Haskell, для имитации классов типов (но я узнал, что это обычно не такая хорошая идея, как кажется).

Конечно, всякий раз, когда есть разница в выразительной силе, есть компромисс. Хотя вы можете использовать данный API-интерфейс несколькими способами, если он написан с использованием DPS, API-интерфейс получает больше информации, если вы не можете. На практике это проявляется в Data.Setтом, что для Ordкаждого типа существует только один словарь. В Setмагазинах его элементы сортируются в соответствии с Ord, и если вы строите набор с одного словаря, а затем вставить элемент , используя другую, как это было бы возможно с DPS, вы можете разбить Set«s инвариантно и привести к аварии. Эта проблема уникальности может быть смягчена с помощью фантомного экзистенциальноготип, чтобы отметить словарь, но, опять же, ценой довольно немного раздражающей сложности в API. Это также проявляется почти таким же образом в TypeableAPI.

Бит уникальности появляется не очень часто. Классы типов хороши в написании кода для вас. Например,

catProcs :: (i -> Maybe String) -> (i -> Maybe String) -> (i -> Maybe String)
catProcs f g = f <> g

который берет два «процессора», которые принимают входные данные и могут выдавать выходные данные, и объединяют их, сглаживая их Nothing, должны быть записаны в DPS примерно так:

catProcs f g = (<>) (funcSemi (maybeSemi listSemi)) f g

По сути, нам снова пришлось прописать тип, в котором мы его используем, даже если мы уже прописали его в сигнатуре типа, и даже это было избыточно, потому что компилятор уже знает все типы. Поскольку существует только один способ создать данное значение Semigroupдля типа, компилятор может сделать это за вас. Это имеет эффект типа «сложный интерес», когда вы начинаете определять множество параметрических экземпляров и использовать структуру ваших типов для вычислений для вас, как в Data.Functor.*комбинаторах, и это используется для deriving viaдостижения большого эффекта, когда вы можете получить практически все «стандартная» алгебраическая структура вашего типа написана для вас.

И даже не заводите меня на MPTC и fundeps, которые возвращают информацию обратно к проверке типов и выводам. Я никогда не пытался преобразовать такую ​​вещь в DPS - я подозреваю, что это потребовало бы передачи большого количества доказательств равенства типов - но в любом случае я уверен, что это будет намного больше работы для моего мозга, чем мне было бы удобно с.

-

1 U nless использовать reflectionв этом случае они становятся эквивалентными в силе - но reflectionтакже может быть обременительным для использования.

luqui
источник
Я очень заинтересован в fundeps, выраженных через DPS. Знаете ли вы какие-либо рекомендуемые ресурсы на эту тему? Во всяком случае, очень понятное объяснение.
Боб
@ Боб, не случайно, но это было бы интересное исследование. Может задать новый вопрос по этому поводу?
Луки
5

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

elemBy :: (a -> a -> Bool) -> a -> [a] -> Bool
elemBy _ _ [] = False
elemBy eq x (y:ys) = eq x y || elemBy eq x ys

Вызов elemBy (==) x xsтеперь эквивалентен elem x xs. И в этом конкретном случае вы можете пойти еще дальше: eqкаждый раз иметь один и тот же первый аргумент, так что вы можете взять на себя ответственность вызывающего абонента за применение этого, и в итоге получите следующее:

elemBy2 :: (a -> Bool) -> [a] -> Bool
elemBy2 _ [] = False
elemBy2 eqx (y:ys) = eqx y || elemBy2 eqx ys

Вызов elemBy2 (x ==) xsтеперь эквивалентен elem x xs.

... Ой, подожди. Это просто any. (А на самом деле в стандартной библиотекеelem = any . (==) .)

Джозеф Сибл-Восстановить Монику
источник
Передача словаря AFAIU - это подход Scala к кодированию классов типов. Эти дополнительные аргументы могут быть объявлены как, implicitи компилятор вставит их для вас из области видимости.
michid