Понимание того, как Либо является экземпляром Functor

В свободное время я изучаю Haskell, так что это вопрос для начинающих.

В своих чтениях я наткнулся на пример, иллюстрирующий, как Either a становится экземпляром Functor:

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

Теперь я пытаюсь понять, почему реализация отображается в случае конструктора значений Right, но не в случае Left?

Вот мое понимание:

Сначала позвольте мне переписать приведенный выше пример как

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

Сейчас:

  1. Я знаю, что fmap :: (c -> d) -> f c -> f d

  2. если мы заменим f на Either a, мы получим fmap :: (c -> d) -> Either a c -> Either a d

  3. тип Right (g x) — это Either a (g x), а тип g x — это d, поэтому мы имеем, что тип Right (g x) — это Either a d, чего мы и ожидаем от fmap (см. 2. выше)

  4. теперь, если мы посмотрим на Left (g x), мы можем использовать те же рассуждения, чтобы сказать, что его тип Either (g x) b, то есть Either d b, чего мы не ожидаем от fmap (см. 2. выше): d должно быть вторым параметром, а не первый! Поэтому мы не можем нанести на карту Left.

Верны ли мои рассуждения?


person MarcoS    schedule 04.03.2011    source источник
comment
Либо, возможно, с большей очевидностью является бифунктором, чем функтором — бифунктор имеет операцию bimap — bimap :: (a -> m) -> (b -> n) -> f a b -> f m n. Это дает вам сопоставление как для левого, так и для правого случаев. В стандартной библиотеке Haskell нет класса Bifunctor, это потому, что бифункторов гораздо меньше, чем функторов в дикой природе, хотя это удобно для иного и пары (a,b).   -  person stephen tetley    schedule 04.03.2011
comment
В 3 вы используете (g x) как часть типа, но это значение. Похоже, вы хотели написать там typeof (g x), а не (g x).   -  person Peaker    schedule 06.03.2011
comment
@stephen tetley: это интересно! Спасибо   -  person MarcoS    schedule 07.03.2011
comment
@stephentetley Я знаю, что вы написали это некоторое время назад, но теперь Bifunctor находится в стандартной библиотеке, начиная с base-4.8.0.0: hackage.haskell.org/package/base-4.8.0.0/docs/   -  person modular    schedule 06.05.2016


Ответы (4)


Это правильно. Есть еще одна очень важная причина такого поведения: вы можете думать о Either a b как о вычислении, которое может завершиться успешно и вернуть b или завершиться ошибкой с сообщением об ошибке a. (Это также то, как работает экземпляр монады). Поэтому вполне естественно, что экземпляр функтора не будет касаться значений Left, поскольку вы хотите отобразить вычисление, если оно не удастся, манипулировать нечем.

person fuz    schedule 04.03.2011
comment
Я еще не узнал о монадах :) Я читал, что Either a b используется так, как вы описываете, и что a представляет сообщение об ошибке: но это всего лишь соглашение, верно? - person MarcoS; 04.03.2011
comment
@MarcoS: правильно, это всего лишь соглашение. Либо можно интерпретировать как дизъюнкт: значение типа Либо a b содержит значение типа a, либо значение типа b, но не оба одновременно. - person Artyom Shalkhakov; 04.03.2011
comment
В индивидуальном случае -- Либо String String Либо String Char, Либо String Int и т. д. -- это, конечно, так; разницы между правым и левым типами нет. Но в качестве функтора вы можете использовать fmap over - as (Either String), в этих примерах - полная асимметрия. Тип, который входит в конкретный экземпляр Functor (здесь String), должен представлять что-то общее, что можно найти во всех обычных вычислениях с любым типом (что происходит справа, через fmap) — поэтому интерпретация «ошибки», в то время как вряд ли единственный, это не случайность. - person applicative; 05.03.2011

Ваш счет, конечно, прав. Возможно, причина, по которой у нас возникают трудности с подобными экземплярами, заключается в том, что мы на самом деле определяем бесконечно много экземпляров функторов одновременно — по одному для каждого возможного типа Left. Но экземпляр Functor — это систематический способ работы с бесконечным множеством типов в системе. Таким образом, мы определяем бесконечное множество способов систематической работы с бесконечным множеством типов в системе. Пример предполагает общность двумя способами.

Впрочем, если рассматривать поэтапно, может быть, это и не так уж и странно. Первый из этих типов — это расширенная версия Maybe, использующая тип единицы измерения () и его единственное допустимое значение ():

data MightBe b     = Nope ()    | Yep b
data UnlessError b = Bad String | Good b
data ElseInt b     = Else Int   | Value b

Здесь мы могли бы устать и сделать абстракцию:

data Unless a b    = Mere a     | Genuine b

Теперь мы без проблем создадим экземпляры Functor, первый из которых очень похож на экземпляр для Maybe:

instance Functor MightBe where
  fmap f (Nope ()) = Nope ()   -- compare with Nothing
  fmap f (Yep x)   = Yep (f x) -- compare with Just (f x)

instance Functor UnlessError where
  fmap f (Bad str) = Bad str   -- a more informative Nothing
  fmap f (Good x)  = Good (f x)

instance Functor ElseInt where
  fmap f (Else n) = Else n 
  fmap f (Value b) = Value (f b)

Но, опять же, зачем заморачиваться, сделаем абстракцию:

instance Functor (Unless a) where
  fmap f (Mere a) = Mere a
  fmap f (Genuine x) = Genuine (f x)

Термины Mere a не затрагиваются, так как значения (), String и Int не затрагиваются.

person applicative    schedule 04.03.2011
comment
Спасибо, это тоже очень хорошее объяснение. Тем не менее, я принял сообщение FUZxxl в качестве ответа, потому что, помимо подтверждения моих рассуждений, он также дает мне интересный намек на интерпретацию Either a b как вычисление (монада) - person MarcoS; 07.03.2011
comment
Это отличный ответ. Я особенно ценю ваш первый абзац, который объясняет двойную общность. - person Ludovic Kuty; 02.11.2018

Как уже упоминалось, тип Either является функтором в обоих аргументах. Но в Haskell мы можем (напрямую) определять только функторы в последних аргументах типа. В подобных случаях мы можем обойти ограничение, используя newtypes:

newtype FlipEither b a = FlipEither { unFlipEither :: Either a b }

Итак, у нас есть конструктор FlipEither :: Either a b -> FlipEither b a, который оборачивает Either в наш newtype с переставленными аргументами типа. И у нас есть деструктор unFlipEither :: FlipEither b a -> Either a b, который разворачивает его обратно. Теперь мы можем определить экземпляр функтора в последнем аргументе FlipEither, который на самом деле является первым аргументом Either:

instance Functor (FlipEither b) where
    fmap f (FlipEither (Left x))  = FlipEither (Left (f x))
    fmap f (FlipEither (Right x)) = FlipEither (Right x)

Обратите внимание, что если мы на время забудем FlipEither, мы получим только определение Functor для Either, просто поменяв местами Left/Right. И теперь, всякий раз, когда нам нужен экземпляр Functor в аргументе первого типа Either, мы можем обернуть значение в FlipEither и затем развернуть его. Например:

fmapE2 :: (a -> b) -> Either a c -> Either b c
fmapE2 f = unFlipEither . fmap f . FlipEither

Обновление: посмотрите Data.Bifunctor, экземплярами которого являются Either и (,). Каждый бифунктор имеет два аргумента и является функтором в каждом из них. Это отражено в методах Bifunctor first и second.

Определение Bifunctor из Either очень симметрично:

instance Bifunctor Either where
    bimap f _ (Left a)  = Left (f a)
    bimap _ g (Right b) = Right (g b)

    first  f = bimap f id

    second f = bimap id f
person Petr    schedule 11.11.2012

Теперь я пытаюсь понять, почему реализация отображается в случае конструктора значения Right, но не в случае левого?

Включите здесь, и это может иметь смысл.

Предположим, что a = String (сообщение об ошибке). Вы применяете либо a к Float.

Итак, у вас есть f: Float -> Integer, скажем, например, округление.

(Любая строка) (Плавающая) = Любая строка с плавающей запятой.

теперь (fmap f):: Либо String Float -> Либо String Int Итак, что вы собираетесь делать с f? f понятия не имеет, что делать со строками, поэтому вы ничего не можете там сделать. Это очевидно, единственное, на что вы можете воздействовать, — это правильные значения, оставляя левые значения без изменений.

Другими словами, Либо a является функтором, потому что существует такое очевидное отображение fmap, которое задается следующим образом:

  • для правильных значений применяются f
  • для левых значений ничего не делать
person jbolden1517    schedule 04.03.2011