Пусть поглотят функторы Хаскелла - PullRequest
31 голосов
/ 01 июля 2010

Learn You a Haskell содержит пример функторов. Я могу читать LYAH и текст, и выяснить, что должно произойти, - но я не знаю достаточно, чтобы написать что-то вроде этого. Я часто нахожу эту проблему в Хаскеле.

instance Functor (Either a) where  
    fmap f (Right x) = Right (f x)  
    fmap f (Left x) = Left x

Тем не менее, я запутался .. Почему это не дополняет

instance Functor (Either a) where  
    fmap f (Right x) = Right (x)  
    fmap f (Left x) = Left (f x)

Если f не используется в верхнем определении, то что еще ограничивает x так, что оно не может удовлетворить Left

Ответы [ 10 ]

41 голосов
/ 02 июля 2010

Вот класс функторов:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Обратите внимание, что "f" сам по себе является конструктором типа, потому что он применяется к переменной типа в строке fmap. Вот несколько примеров, чтобы прояснить это:

Тип конструкторов:

IO
Maybe
Either String

Типы:

IO Char
Maybe a
Either String String

"Maybe a" - это тип с одним конструктором типов ("Maybe") и одной переменной типа ("a"). Это еще не что-то конкретное, но его можно использовать в сигнатурах типов для полиморфных функций.

«Любой» - это конструктор типа, который принимает два аргумента типа, поэтому даже после применения одного (например, Either String это все еще конструктор типа, поскольку он может принимать другой аргумент типа.

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

Когда вы пишете instance Functor (Either c), Either c заполняется везде в объявлении fmap. Это дает fmap следующий тип для этого экземпляра:

fmap :: (a -> b) -> (Either c) a -> (Either c) b

При определении Either единственный полезный способ получить этот тип - применить значение Right к функции. Помните, что «Любой» имеет два возможных значения с разными типами. Здесь значение Left имеет тип 'c', поэтому вы не можете применить его к функции (которая ожидает 'a') [1], и результат также не будет правильным, потому что вы останетесь с Either b a, что не соответствует определению класса.

После замены "f" на "Either c", чтобы получить указанную выше сигнатуру типа для fmap с экземпляром "Either c", написание реализации будет следующим. Есть два случая, чтобы рассмотреть, левый и правый. Подпись типа говорит нам, что тип левой стороны, "c", не может измениться. У нас также нет никакого способа изменить значение, потому что мы не знаем, какой это тип на самом деле. Все, что мы можем сделать, это оставить это в покое:

fmap f (Left rval) = Left rval

В правой части подпись типа говорит о том, что мы должны перейти от значения с типом "a" к значению с типом "b". Первый аргумент - это функция, которая делает именно это, поэтому мы используем функцию с входным значением, чтобы получить новый вывод. Соединение двух дает полное определение

instance Functor (Either c) where
  fmap f (Right rval) = Right (f rval)
  fmap f (Left lval) = Left lval

Здесь работает более общий принцип, поэтому написать экземпляр Functor, который настраивает левую сторону, невозможно, по крайней мере, с определениями Prelude. Копирование некоторого кода сверху:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor (Either c) where ...

Несмотря на то, что у нас есть переменная типа 'c' в определении экземпляра, мы не можем использовать ее ни в одном из методов класса, потому что она не упоминается в определении класса. Так что вы не можете написать

leftMap :: (c -> d) -> Either c a -> Either d a
leftMap mapfunc (Left x) = Left (mapfunc x)
leftMap mapfunc (Right x) = Right x

instance Functor (Either c) where
  --fmap :: (c -> d) -> Either c a -> Either d a
  fmap = leftMap

Результат leftMap и, следовательно, fmap теперь равен (Either d) a. (Either c) изменился на (Either d), но это недопустимо, потому что нет способа выразить это в классе Functor. Чтобы выразить это, вам нужен класс с двумя переменными типа, например,

class BiFunctor f where
  lMap :: (a -> b) -> f a c -> f b c
  rMap :: (c -> d) -> f a c -> f a d
  biMap :: (a -> b) -> (c -> d) -> f a c -> f b d

В этом классе, поскольку переменные типа left и right находятся в области видимости, можно написать методы, которые работают с одной (или с обеих) сторон.

instance BiFunctor Either where
  lMap = leftMap
  rMap = rightMap  --the same as the standard fmap definition
  biMap fl fr e = rMap fr (lMap fl e)

Хотя на практике люди обычно просто пишут «biMap» для класса BiFunctor и используют «id» для другой функции, если необходимо сопоставление влево или вправо.

[1] Точнее, значение Left имеет тип «c», функция ожидает «a», но средство проверки типов не может объединить эти типы, потому что тип «c» не находится в области видимости в классе определение.

9 голосов
/ 01 июля 2010

Left и Right не являются типами, а Left x и Right y относятся к одному и тому же типу.Они просто конструкторы Либо.Вы можете рассмотреть

Left :: c -> Either c d
Right :: d -> Either c d

У вас может быть 2 fmap объявлений, потому что мы знаем, что левые и правые значения разные.Это как

g :: Int -> Int
g 1 = 2
g 2 = 4
g n = n

Здесь нельзя сказать, что 1 и 2, а n - это разные «типы» только потому, что работает сопоставление с образцом.


Определен класс Functorтакой, что

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Обратите внимание, что a и b являются произвольными типами.Для ясности давайте переименуем a в вашем экземпляре на c, а функцию f на func.

instance Functor (Either c) where  
    fmap func (Right x) = Right (x)  
    fmap func (Left x) = Left (func x)

Предположим, что ваш Either следует определению по умолчанию

data Either c d = Left c | Right d

тогда по вашему определению

fmap    func     (Right x) = Right x
-- # (a -> b) ->      f a       f  b
-- # f = Either c

это силы a = b, а

fmap    func     (Left x) = Left (func x)
-- # (a -> b) ->     f a       f b
-- # f = Either c

силы c = a = b.Оба недопустимы, учитывая, что a, b и c являются независимыми произвольными типами.

8 голосов
/ 01 июля 2010

Хорошо, вот еще одна очень простая попытка.

Вы спрашиваете, почему это не компилируется:

instance Functor (Either a) where
    fmap f (Right x) = Right (x)
    fmap f (Left x) = Left (f x)

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

Это дает нам

foo f (Right x) = Right (x)
foo f (Left x) = Left (f x)

Который действительно компилируется. ghci сообщает нам тип подписи:

*Main> :t foo
foo :: (t1 -> a) -> Either t1 t -> Either a t

Мы переименуем некоторые переменные, чтобы получить более равномерный вид:

foo :: (a -> b) -> Either a c -> Either b c

Это имеет смысл. Он берет функцию и применяет ее слева от любого.

Но какая подпись для fmap?

*Main> :t fmap
fmap :: (Functor f) => (a -> b) -> f a -> f b

Итак, давайте заменим Either c на f в сигнатуре fmap (я переименовал Either a в Either c, чтобы наши два разных a не смешались):

fmap :: (a -> b) -> Either c a -> Either c b

Вы видите проблему? Ваша функция совершенно корректна - она ​​просто имеет тип, отличный от того, что fmap для Either a обязательно должен иметь .

Это что-то прекрасное в типах. Учитывая подпись для fmap, на самом деле есть только одна значимая реализация для fmap на любом из них.

Иногда, когда нам везет и мы осторожны, мы можем оказаться в подобных ситуациях - учитывая сигнатуру типа, функция почти пишет сама.

Редактировать : пытаться ответить на вопросы ниже.

1) «Сочетание двух функций» не происходит. Чтобы получить сигнатуру типа для fmap сверх Either a, просто пройдите через сигнатуру функции fmap и в каждом месте, где вы видите f, замените его на Either a. Мы бы назвали это «специализацией» сигнатуры типа fmap. То есть строго менее общий , чем обычный тип fmap - в любом месте, где требуется функция более специализированного типа, вы можете передать что-то общего типа без проблем. *

2) Ваша функция для отображения на левой стороне (которую я назвал "foo" в приведенных выше примерах) просто в порядке. Работает нормально, делает то что хочешь. Вы просто не можете назвать его fmap и использовать его в экземпляре Functor. Лично я бы назвал это что-то вроде onLeft или mapLeft.

Все следующее можно игнорировать / предназначено для информации, но не для дальнейшего чтения в ближайшем будущем / фактического использования:

Если кто-то хочет стать очень техническим, потому что вы можете отобразить как левую, так и правую сторону (хотя вы можете объявить только Functor для последней), Either - это не только Functor, но и Bifunctor. Это предусмотрено, например, в библиотеке экстра-категорий ekmett (см. http://hackage.haskell.org/packages/archive/category-extras/0.44.4/doc/html/Control-Bifunctor.html).

Есть много интересных вещей, связанных с вычислениями с помощью программ и "программированием оригами", в котором бифункторы используются более строго. Вы можете прочитать об этом здесь: http://lambda -the-ultimate.org / node / 1360 . Но вы, вероятно, не хотите, по крайней мере, пока вы не будете намного лучше знакомы с Haskell. Это компьютерная наука, математика, исследование и очень круто, но не обязательно вообще для понимания идиоматического программирования на Haskell.

3 голосов
/ 03 июля 2010

Верьте или нет, это не волшебство. Все это связано с типом Either a b, который отличается от типа Either b a. Вот определение Either from Prelude

data  Either a b  =  Left a | Right b

... Обратите внимание, что сначала следует переменная типа a, затем b, а также обратите внимание, что мы включаем только a в объявление Either Functor:

instance Functor (Either a) where  
    fmap f (Right x) = Right (f x)  
    fmap f (Left x) = Left x

Теперь давайте посмотрим на определение Maybe Functor

instance Functor Maybe where
    fmap = map

Здесь нет переменной типа, хотя Maybe принимает один параметр типа (например, Maybe Int). Я пытаюсь понять, что типы не являются функторами, конструкторы типов являются функторами (функторы имеют вид *->*).

Так что давайте f :: b -> c, в версии Either Functor, которая работает, x из (Left x) имеет тип a, что хорошо, так как это (Either a), который является функтором, x в (Right x) имеет тип b, поэтому (Right x) имеет тип ((Either a) b), а (Right (f x)) имеет тип ((Either a) c), поэтому fmap имеет тип (b->c) -> ((Either a) b) -> ((Either a) c), как требуется.

В вашей версии, которая потерпела неудачу, у нас есть x в (Right (x)) не типа a, а типа b, поэтому (Right (x)) равно , а не типа ((Either a) c) который не соответствует типу fmap.

Итак, чтобы подвести итог: работает fmap: (b -> c) -> (Either a) b -> (Either a) c, но выходит тот, который не работает: (b -> c) -> (Either b) a -> (Either c) a и это не правильный тип для fmap.

3 голосов
/ 01 июля 2010

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

data Choice a = Default Integer | Chosen a

-- This corresponds to your top, working, instance.
instance Functor Choice where
  fmap f (Default i) = Default i
  fmap f (Chosen  a) = Chosen  (f a)

Должно быть понятно, почему этот экземпляр работает.Однако, как насчет следующего:

-- Broken!
instance Functor Choice where
  fmap f (Default i) = Default (f i)
  fmap f (Chosen  a) = Chosen  a

Вы должны увидеть, почему это не работает.Тип fmap равен Functor f => (a -> b) -> f a -> f b;в этом контексте это (a -> b) -> Choice a -> Choice b.Таким образом, аргумент f имеет тип a -> b.Однако во втором (неудачном) объявлении экземпляра вы пишете f i.Из-за объявления типа данных мы знаем, что i должно быть Integer, поэтому мы не можем применить f к нему.Аналогичным образом, поскольку a имеет тип a, Chosen a будет иметь тип Chosen a, а не тип Chosen b.Таким образом, экземпляр Functor в нижней части не может работать.

Ну, ваш верхний экземпляр для Either работает, потому что, как и в примере Choice, он подчиняется типам.Давайте посмотрим на это с несколькими переименованиями:

instance Functor (Either c) where  
  fmap f (Left  c) = Left  c
  fmap f (Right a) = Right (f a)

Это объявление экземпляра не объявляет экземпляр Functor для Either - это невозможно.Что-то, что является экземпляром Functor, должно принимать один параметр типа.Таким образом, Int не может быть функтором, поскольку Int не принимает параметров типа, но [] и Maybe могут быть, поскольку [a] и Maybe a являются полными типами.Однако Either принимает два параметра типа: Either a b.Таким образом, то, что этот экземпляр делает , объявляет, что Either c является функтором для любого возможного c.Это c является фиксированным на время объявления экземпляра.Итак, давайте пройдемся и добавим типы (это недопустимый синтаксис!):

instance Functor (Either c) where
  fmap :: forall a b. (a -> b) -> (Either c) a -> (Either c) b
  fmap f (Left  (c :: c)) = Left  c
  fmap f (Right (a :: a)) = Right (f a :: b)

Поскольку f имеет тип a -> b, но тип c зафиксирован на c, мыне могу написать Left (f c);и даже если бы мы могли, мы хотим, чтобы c был оставлен в покое, чтобы мы могли вернуть (Either c) b.Точно так же мы должны применить f к a, чтобы получить что-то типа b.

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

instance Functor (Either c) where  
  fmap :: forall a b. (a -> b) -> (Either c) a -> (Either c) b
  fmap f (Left  (c :: c)) = Left  (f c)
  fmap f (Right (a :: a)) = Right a

Здесь ваша первая часть определения функции пытается применить функцию f :: a -> b к чему-то фиксированного типа c, который не может работать, так что это уже не удается.Но давайте посмотрим, какой тип это генерирует.В этом случае мы ожидаем, что (так или иначе) f c будет иметь тип b, а a будет иметь тип a.В этом случае мы возвращаем значение типа Either b a, которое по-прежнему недопустимо.

По сути, проблема связана с этим.Во-первых, обратите внимание, что f одинаково между двумя предложениями определения функции, поэтому оно не может меняться между строками.Во-вторых, обратите внимание, что мы исправляем c и объявляем экземпляр для , что c.Это верно для любого c, но мы смотрим только по одному за раз.Наконец, из-за этого аргумент Left не параметризован типом, ожидаемым f;гарантированно есть фиксированный тип c.Это означает, что (а) вы не можете применить f к нему, и (б) вы должны применить его к аргументу Right, так как иначе вы не измените тип, который выОжидается, что изменится.

3 голосов
/ 01 июля 2010

(Изменить, чтобы попытаться ответить на вопрос лучше)

Определение либо:

data Either a b = Left a | Right b

Так что "Любой" принимает два аргумента типа. Кстати, технически «любой» на самом деле не тип, а конструктор типа; для создания типа нужны аргументы типа.

Определение Функтора:

class Functor f where
   fmap :: (p -> q) -> f p -> f q

Так что в этом определении класса любой тип "f", который является экземпляром Functor, должен принимать аргумент типа. Это не объявлено; это выведено из "f p" и "f q"; так как «f» здесь дается параметр типа, это должен быть тип, который принимает его.

(Примечание: оригинальное определение использовало «a» и «b» вместо «p» и «q». Я использую разные буквы, чтобы отличить их от «Either ab», когда я вернусь к этому позже) 1013 *

В большинстве случаев "f" - это тип контейнера, такой как список или дерево. Так, например, у вас есть

data Tree a = ...

instance Functor Tree where
   fmap func_a2b tree_of_a = ... --  tree of b.

Однако «Любой» принимает два параметра типа, так как мы можем вписать его в эту схему? Ответ заключается в том, что типы могут иметь частичное применение, как и функции. Так же, как Я могу объявить функцию

foo x y = ...

и затем произнесите «foo 2», чтобы получить новую функцию, которая ожидает второй аргумент, поэтому я могу сказать «Либо a», чтобы получить новый тип, который ожидает аргумент второго типа.

Теперь посмотрите на оригинальный экземпляр:

instance Functor (Either a) where ....

Так что здесь "Either a" - это конструктор типа, который ожидает еще один аргумент, точно так же, как Functor ожидает от своих экземпляров. Таким образом, тип "fmap" для "Either a" будет

fmap :: (p -> q) -> Either a p -> Either a q

Так что теперь в предложении «где» вы должны дать определение «fmap», имеющего этот тип. Первый, который вы цитируете, имеет этот тип, потому что второй параметр типа используется для конструктора «Right», и это тот, к которому применяется функция. Второй не будет работать, потому что он будет иметь тип

fmap :: (p -> q) -> Either p a -> Either q a

И это не то, о чем говорит класс Functor.

2 голосов
/ 02 июля 2010

Top работает, потому что fmap::(b -> c) -> Either a b -> Either a c, а ваш -bottom- не работает, потому что для этого потребуется fmap::(a -> c) -> Either a b -> Either a c.Но это будет работать, если вы измените Either на

data Either' a b = Left' b | Right' a deriving (Eq, Show)

instance Functor (Either' a) where  
    fmap f (Right' x) = Right' (x)  
    fmap f (Left' x) = Left' (f x)
2 голосов
/ 01 июля 2010

Надеюсь, это поможет ...

Во-первых, хотя, некоторый фон:

1) Функтору нужен "конструктор типов", а (ну, не тип как таковой,...) тип, который «нуждается» в другом обычном типе, заданном ему, чтобы сформировать «полный тип», точно так же как универсальный в Java / C ++.Так, например, List - это Функтор (это, между прочим), или Array, потому что (среди прочего) полный тип это не просто List, но List<A>.Итак,: Functor принимает «конструктор типа», неполный тип.

2) Either - это тип конструктора, который люди из Хаскелла (читай: Эдвард Кметт и другие хорошо обеспеченные математикой звезды)) позвоните бифунктору.Для этого нужно два типа данных.Например, полное использование Either: Either Integer String, что означает (да, да, "Дух!"), Это либо (Left) Integer, либо (Right) String.Таким образом, это означает, что Either Integer является неполным типом, который является либо Left Integer, либо Right...b, когда вы решаете, каким должен быть этот "b".

Теперь для забавной части!

Основные вещи работают потому, что fmap использует конструктор типов и использует его с функцией a -> b, чтобы сделать аналогичную функцию из f a в f b - практический любимый пример для этогов Haskell - список списков, карта АКА :: (a -> b) -> ([a] -> [b]), где Функтор - это часть [ ].Теперь, используя что-то вроде Either a (давайте продолжим и используем Either Integer из более раннего), сигнатура типа fmap выглядит следующим образом:

fmap :: (a -> b) -> (Either Integer a -> Either Integer b)

и два примера (из верхней части) показывают, что fmapделает с репрезентативными значениями этого Either Integer a типа, чтобы получить Either Integer b -типовое значение.

Теперь ваш -bottom- не работает, потому что:

  1. У вас есть функция f, которая принимает от a с до b с.
  2. Ваш тип Left должен иметь тип Integer, оставаться целым числом (или типом Float и оставатьсятип с плавающей запятой, любой тип left один из двух типов Either типа, который вы используете).
  3. Ваш тип Right должен быть любого типа, которыйфункция принимает ("a") и переходит к типу, который делает функция ("b").

Она должна сделать это (но ваши вещи не -вот почему это не работает), потому что это тип, который fmap должен иметь.В частности, у вас есть эти уравнения:

fmap f (Right x) = Right (x)  
fmap f (Left x) = Left (f x)

Ваши уравнения дают fmap типы:

fmap :: (a -> b) -> Either c d -> Either c d
fmap :: (a -> b) -> Either a d -> Either b d

, которые не только не соответствуют тому, что хочет fmap, но и неДаже не согласуются друг с другом!

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

1 голос
/ 02 июля 2010

Эмм ... Как насчет нескольких слов о "видах"? ..
Думаю, это может упростить понимание.
Помните, что такое карри.Т.е. в ghci:

Prelude> let f x y z = x + y * z
f :: (Num a) => a -> a -> a -> a
Prelude> :t f 1
f 1 :: (Num t) => t -> t -> t
Prelude> :t f 1 2
f 1 2 :: (Num t) => t -> t
Prelude> :t f 1 2 3
f 1 2 3 :: (Num t) => t

То же самое у вас с типами.Когда вы говорите Either тип этого типа * -> * -> * (то есть он принимает два типа и производит тип) и когда вы говорите Either a тип * -> * и для Either a b это * (кстати Monad aFunctor a требует, чтобы a был вида * -> *, насколько я помню).Поэтому, когда вы говорите тип Either a, это означает, что тип все еще не завершен (требуется некоторый «аргумент» для привязки), поэтому fmap :: (a -> b) -> f a -> f b становится fmap :: (a -> b) -> (Either c) a -> (Either c) b, когда f заменяется на Either c.

1 голос
/ 01 июля 2010

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

fmap2 :: (a -> b) -> Either a c -> Either b c

Если вы установите LANGUAGE прагму TypeOperators, GHC также принимает тип

fmap2 :: (a -> b) -> (a `Either` c) -> (b `Either` c)

В идеальном мире это также будет работать:

fmap2 :: (a -> b) -> (`Either` c) a -> (`Either` c) b

, который дал бы экземпляр Functor для (`Either` c), но сходство между обычными операторами (и их секциями) и операторами типов на этом этапе нарушается (если только у меня отсутствует опция GHC!)

Вкратце: ваше понимание функторов в порядке, но вас укусило отсутствие лямбд на уровне типов.

...