5

Я играл с TypeFamilies, FunctionalDependencies и MultiParamTypeClasses. И мне кажется, что TypeFamilies не добавляет каких-либо конкретных функций по сравнению с двумя другими. (Но не наоборот). Но я знаю, что семейства типов очень нравятся, поэтому я чувствую, что у меня что-то не хватает:Что может типа семейств, что классы множественного типа и функциональные зависимости не могут

«открытое» отношение между типами, такое как функция преобразования, которая не представляется возможной с TypeFamilies. Совершенно с MultiParamTypeClasses:

class Convert a b where 
    convert :: a -> b 

instance Convert Foo Bar where 
    convert = foo2Bar 

instance Convert Foo Baz where 
    convert = foo2Baz 

instance Convert Bar Baz where 
    convert = bar2Baz 

Сюръективная соотношение между типами, такими как своего родом типом безопасного псевдо-утком механизма типизации, которая обычно делаются с помощью стандартного типа семьи. Совершенно с MultiParamTypeClasses и FunctionalDependencies:

class HasLength a b | a -> b where 
    getLength :: a -> b 

instance HasLength [a] Int where 
    getLength = length 

instance HasLength (Set a) Int where 
    getLength = S.size 

instance HasLength Event DateDiff where 
    getLength = dateDiff (start event) (end event) 

Биективных отношениями между типами, такими как для Unboxed контейнера, который может быть сделан через TypeFamilies с семьей данных, хотя тогда вы должны объявить новый тип данных для каждого содержащегося типа , например, с newtype. Либо это, либо с инъективного типа семьи, который я думаю, не доступен до GHC 8. Done с MultiParamTypeClasses и FunctionalDependencies:

class Unboxed a b | a -> b, b -> a where 
    toList :: a -> [b] 
    fromList :: [b] -> a 

instance Unboxed FooVector Foo where 
    toList = fooVector2List 
    fromList = list2FooVector 

instance Unboxed BarVector Bar where 
    toList = barVector2List 
    fromList = list2BarVector 

И, наконец Сюръективный отношений между двумя типами и третьего типа, такие как python2 или java style функция разделения, которая может быть выполнена с помощью TypeFamilies, также используя MultiParamTypeClasses. Совершено с MultiParamTypeClasses и FunctionalDependencies:

class Divide a b c | a b -> c where                 
    divide :: a -> b -> c                    

instance Divide Int Int Int where                  
    divide = div 

instance Divide Int Double Double where                
    divide = (/) . fromIntegral                  

instance Divide Double Int Double where                
    divide = (. fromIntegral) . (/)                 

instance Divide Double Double Double where               
    divide = (/) 

Еще одна вещь, которую я должен также добавить, что кажется, FunctionalDependencies и MultiParamTypeClasses также весьма немного более кратким (для приведенных выше в любом случае примеров), как у вас есть только написать тип один раз, и вы не должны придумать имя фиктивного типа, который затем вы должны ввести для каждого экземпляра, как вы делаете с TypeFamilies:

instance FooBar LongTypeName LongerTypeName where 
    FooBarResult LongTypeName LongerTypeName = LongestTypeName 
    fooBar = someFunction 

против:

instance FooBar LongTypeName LongerTypeName LongestTypeName where 
    fooBar = someFunction 

Поэтому, если я не убежден в этом, мне кажется, что я не должен беспокоиться о TypeFamilies и использовать исключительно FunctionalDependencies и MultiParamTypeClasses. Поскольку, насколько я могу судить, это сделает мой код более кратким, более последовательным (одно меньшее расширение, чтобы заботиться), а также даст мне большую гибкость, например, с отношениями открытого типа или биективными отношениями (возможно, последний является решателем GHC 8).

+1

Типовые семейства обычно работают намного лучше, чем фонды, особенно при реализации функций типа. – ErikR

+1

Существует также вопрос читаемости. Эквивалент типа F a = G (H (G a) (G a)), выраженный через FunDeps, требует нескольких ограничений, связанных с несколькими вспомогательными переменными типа. Когда я читал такие ограничения, я обнаружил, что пытаюсь выразить их в функциональной форме. Возможно, это потому, что я больше привык читать функциональный код, чем пролог. – chi

+0

@ErikR Почему? Это кажется невероятно странным, потому что примеры, в которых оба варианта жизнеспособны, выглядят полностью идентичными с точки зрения разрешения типа и не для меня. Это просто недостаток в текущей реализации «Функциональныхзависимостей» или «MultiParamTypeClasses» или их взаимодействия? Или это нечто более фундаментальное? Я действительно надеюсь на первое, так как было бы неплохо не использовать другой и, казалось бы, более подробный синтаксис, половину времени для одной и той же вещи только по соображениям производительности. – semicolon

ответ

3

Вот пример того, где TypeFamilies действительно светит по сравнению с MultiParamClasses с FunctionalDependencies. На самом деле, я призываю вас, чтобы придумать эквивалентной MultiParamClasses решения, даже тот, который использует FlexibleInstancesOverlappingInstance, и т.д.

Рассмотрим задачу замещения на уровне типа (я натыкался конкретного варианта этого в Quipper в QData.hs) , По сути, вы хотите, чтобы рекурсивно заменить один тип на другой.Например, я хочу, чтобы иметь возможность

  • заменителя Int для Bool в Either [Int] String и получить Either [Bool] String,
  • заменителем [Int] для Bool в Either [Int] String и получить Either Bool String,
  • заменителем [Int] для [Bool] в Either [Int] String и получить Either [Bool] String ,

В общем, я хочу обычное понятие замены уровня. С закрытым типом семейства я могу сделать это для любых типов (хотя мне нужна дополнительная строка для каждого конструктора более высокого типа - я остановился на * -> * -> * -> * -> *).

{-# LANGUAGE TypeFamilies #-} 

-- Subsitute type `x` for type `y` in type `a` 
type family Substitute x y a where 
    Substitute x y x = y 
    Substitute x y (k a b c d) = k (Substitute x y a) (Substitute x y b) (Substitute x y c) (Substitute x y d) 
    Substitute x y (k a b c) = k (Substitute x y a) (Substitute x y b) (Substitute x y c) 
    Substitute x y (k a b) = k (Substitute x y a) (Substitute x y b) 
    Substitute x y (k a) = k (Substitute x y a) 
    Substitute x y a = a 

И пытается на ghci я получить желаемый результат:

> :t undefined :: Substitute Int Bool (Either [Int] String) 
undefined :: Either [Bool] [Char] 
> :t undefined :: Substitute [Int] Bool (Either [Int] String) 
undefined :: Either Bool [Char] 
> :t undefined :: Substitute [Int] [Bool] (Either [Int] String) 
undefined :: Either [Bool] [Char] 

С тем, что, может быть, вы должны спросить себя, почему я использую MultiParamClasses и не TypeFamilies. Из приведенных выше примеров все, кроме Convert, переводите типы семейств (хотя вам потребуется дополнительная строка для каждого экземпляра для объявления type).

Опять же, для Convert, я не уверен, что это хорошая идея, чтобы определить такую ​​вещь. Естественное продолжение Convert бы случаи, такие как

instance (Convert a b, Convert b c) => Convert a c where 
    convert = convert . convert 

instance Convert a a where 
    convert = id 

которые, как неразрешимые для GHC, поскольку они элегантны писать ...

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

This old HaskellWiki page does an OK job of comparing the two.

EDIT

Некоторые более контрастным и истории я наткнулся от augustss blog

Тип семьи вырос из необходимости иметь классы типа с связанных типов. Последнее не является строго необходимым, так как оно может быть , эмулированное с помощью классов с несколькими параметрами, но во многих случаях оно дает гораздо более удобную нотацию . То же самое верно для семейств типов; они могут также эмулироваться классами с несколькими параметрами. Но MPTC дает очень логичный стиль программирования для вычисления типа; тогда как типы семейства (которые являются только функциями типа, которые могут сопоставлять шаблоны по аргументам ), как функциональное программирование.

Использование закрытых типов добавляет дополнительную силу, которая не может быть достигнута классами классов. К получите такую ​​же мощность от классов классов, что нам нужно будет добавить закрытые типы классов .Это было бы очень полезно; это то, что дает вам цепочки .

+0

Хорошо, я думаю, что, наконец, более или менее получаю это. Правильно ли я полагаю, что если все, что вы делаете, это указать отношения типа, подобные тому, что я делал в исходном столбце, и вы не используете их каким-либо особо нестандартным способом, тогда функции FunctionalDependencies и MultiParamTypeClasses работают достаточно хорошо. Но если вы попытаетесь выйти за рамки этого и выполнить правильное программирование на уровне уровня, то вам намного лучше с 'TypeFamilies'. С точки зрения гибкости и общей элегантности. И поэтому из-за этого вы должны просто придерживаться 'TypeFamilies' в целом, чтобы поддерживать эту элегантность и гибкость. – semicolon

+0

@semicolon Да, это хорошее резюме. 'TypeFamilies' новее, чем' MultiParamTypeClasses', и был IFRC, предназначенный для того, чтобы сделать программирование на уровне типов более похожим на программирование типа значений (вместо логических эквивалентов). – Alec

1

Функциональные зависимости влияют только на процесс решения ограничений, в то время как семейства типов вводят понятие несинтаксического равенства типов, представленное в промежуточной форме GHC путем принуждения. Это означает, что семьи типов лучше взаимодействуют с GADT. См. this question для канонического примера того, как функциональные зависимости терпят неудачу.