Haskell и классы типов с несколькими параметрами

#haskell

#haskell

Вопрос:

У меня возникла простая проблема, когда дело доходит до написания классов типов, которые наследуются друг от друга. Я пытаюсь создать иерархию классов типов для достижения некоторого уровня абстракции представления. Допустим, мне нужен класс типов collections:

 {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}

class Collection c a where
  isMember :: a -> c -> Bool
  

И я определил тип дерева:

 data Tree a = Empty | Node a (Tree a) (Tree a)
  deriving (Show, Eq)
  

Я хотел бы сделать свое дерево коллекцией, поэтому:

 inOrder :: Tree a -> [a]
inOrder Empty = []
inOrder (Node a l r) = (inOrder l)    [a]    (inOrder r)

instance (Eq a) => Collection (Tree a) a where
  isMember a c = a `elem` (inOrder c)
  

Это работает не совсем правильно:

 *Main> (isMember '2' Empty)
<interactive>:1:1:
    No instance for (Collection (Tree a) Char)
      arising from a use of `isMember' at <interactive>:1:1-18
    Possible fix:
      add an instance declaration for (Collection (Tree a) Char)
    In the expression: (isMember '2' Empty)
    In the definition of `it': it = (isMember '2' Empty)
  

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

Ответ №1:

Проблема здесь в том, что по умолчанию каждый параметр класса type не зависит от других. Простого применения isMember к Char и дереву неизвестного типа элемента недостаточно, чтобы позволить ему сделать вывод, что он должен использовать (казалось бы, очевидный) экземпляр.

Это явно не то, как вы хотите, чтобы это работало, и немного глупо при загрузке, но именно так это работает. Чтобы справиться с проблемой, вам нужно предоставить ему какой-то способ установления соединения, для чего существуют другие расширения: FunctionalDependencies является более распространенным, хотя TypeFamilies и более новым и гораздо более удобным в использовании, но все еще имеет некоторые шероховатости.

В случае функциональных зависимостей вы должны указать, что некоторое подмножество параметров класса типов полностью определяет другие, например Collection c a | c -> a . Это имеет свои подводные камни, но работает достаточно хорошо во многих случаях.

С семействами типов вы, вероятно, сократили бы это до класса типа с одним параметром, с соответствующим типом элемента, например:

 class Collection c where
    type Elem c
    isMember :: Elem c -> c -> Bool
  

Ответ №2:

Необходимо добавить функциональную зависимость, чтобы указать, что тип контейнера определяет тип элемента:

 {-# LANGUAGE FunctionalDependencies #-}

class Collection c a | c -> a where
    ...
  

Есть и другие способы сделать это, например, используя семейства типов, но я думаю, что это самое простое решение в данном случае.

Ответ №3:

Проблема в том, что Empty тип является Tree a , а ghc не располагает достаточной информацией, чтобы знать, что вы хотите, чтобы в этом случае a было Char . Если вы вручную укажете тип, это сработает:

 isMember '2' (Empty::Tree Char)
  

Причина, по которой ghc не может просто сделать вывод, что a должно быть Char , заключается в том, что теоретически у вас может быть один экземпляр Collection (Tree Char) Char и другой Collection (Tree Int) Char . В этом случае не было бы способа определить, какой из них следует использовать, если вы не укажете это явно.