Как функторы работают в haskell?

Я пытаюсь изучить Haskell, и я изучил все основы. Но теперь я застрял, пытаясь разобраться в функторах.

Я читал, что «Функтор преобразует одну категорию в другую категорию». Что это значит?

Я знаю, что это много вопросов, но может ли кто-нибудь дать мне простой английский объяснение функторов или, может быть, простой вариант использования?


person Matias Rasmussen    schedule 30.10.2012    source источник
comment
Я нашел сообщение в блоге Габриэля, Шаблон проектирования функтора , чтобы быть неплохим. Это не совсем простой английский, но вы должны прочитать его и посмотреть, поможет ли он.   -  person Dan Burton    schedule 30.10.2012
comment
Ссылка от Антона Гурьянова: en.wikibooks.org/wiki/Haskell/Category_theory   -  person David Eisenstat    schedule 09.02.2015


Ответы (5)


Нечеткое объяснение будет заключаться в том, что Functor - это своего рода контейнер и связанная с ним функция fmap, которая позволяет вам изменять все, что содержится, с учетом функции, которая преобразует содержимое.

Например, списки представляют собой такой контейнер, что fmap (+1) [1,2,3,4] дает [2,3,4,5].

Maybe также можно сделать функтором, так что fmap toUpper (Just 'a') дает Just 'A'.

Общий вид fmap довольно четко показывает, что происходит:

fmap :: Functor f => (a -> b) -> f a -> f b

А специализированные версии могут прояснить это. Вот версия списка:

fmap :: (a -> b) -> [a] -> [b]

И версия Maybe:

fmap :: (a -> b) -> Maybe a -> Maybe b

Вы можете получить информацию о стандартных Functor экземплярах, запросив GHCI с :i Functor, и многие модули определяют больше экземпляров Functors (и других классов типов).

Однако, пожалуйста, не относитесь к слову «контейнер» слишком серьезно. Functors - это четко определенная концепция, но вы часто можете рассуждать об этом, используя эту нечеткую аналогию.

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

person Sarah    schedule 30.10.2012
comment
По своему опыту могу сказать, что аналогия с контейнером для функторов усложнила их понимание (особенно когда дело касается IO). Итак, вначале их можно рассматривать как своего рода вычисление, прикрепленное к значению, а не как контейнер, содержащий значение. - person Anton Guryanov; 30.10.2012
comment
IO сам по себе не является чистым Haskell. Я нашел это объяснение более ясным: подумайте о putStrLn :: String -> IO (). Эта функция принимает String и возвращает IO (), которое можно рассматривать как вычисление, в данном случае вывод на stdout. Объяснение контейнера здесь не так ясно, как объяснение вычислений. Но это только мое мнение. - person Anton Guryanov; 30.10.2012
comment
@WillNess @Anton правильный. Аналогию с контейнером можно использовать только для простоты. Однако это ни в коем случае не определено. Рассмотрим, например, функтор Const a :) - person is7s; 30.10.2012
comment
Нет смысла раскалывать волосы из-за аналогии, которая была описана в ответе как нечеткая. Все аналогии не выдерживают критики, и Сара правильно указала ФП в направлении более полного и основанного на законах понимания. Контейнер - хорошее начало. Продюсер - следующий шаг. Вычислительный контекст довольно общий, но слишком абстрактный, чтобы начинать с него. (На мой взгляд, как учитель.) OP все равно просил простой английский, давайте не будем слишком теоретизировать. - person AndrewC; 31.10.2012
comment
В теории категорий функтор часто визуализируется как стрелка между категориями. Почему конструктор списка [] считается функтором? Означает ли это, что я могу визуализировать [] как стрелку функтора между Hask -> Hask? Означает ли это, что как функтор [] сопоставляет такие объекты, как a, с [a]? Но что означает для функтора [] отображение морфизмов? И почему [] просто не называется морфизмом в категории Hask? - person CMCDragonkai; 10.03.2015
comment
@CMCDragonkai: Все функторы в Haskell являются эндофункторами. - person Sarah; 11.03.2015
comment
@CMCDragonkai Означает ли это, что я могу визуализировать [] как стрелку функтора между Hask - ›Hask? Да. Означает ли это, что как функтор [] сопоставляет такие объекты, как a, с [a]? Да. Но что для функтора [] означает отображение морфизмов? В Hask функтор имеет два бита: отображение типов в типы и отображение функций в функции. [] сотрудничает с map, чтобы предоставить [] a = [a] для типов и map f для функций. Почему [] не называется просто морфизмом в категории Hask? Потому что в Hask морфизмы - это функции, которые действуют на значения, а не функторы, которые действуют на типы и функции. - person AndrewC; 20.02.2020

Я случайно написал

Руководство по функциям Haskell

Я отвечу на ваш вопрос примерами, а типы помещу внизу в комментариях.

Следите за узором в типах.

fmap является обобщением map

Функторы предназначены для предоставления вам функции fmap. fmap работает как map, поэтому давайте сначала проверим map:

map (subtract 1) [2,4,8,16] = [1,3,7,15]
--    Int->Int     [Int]         [Int]

Поэтому он использует функцию (subtract 1) внутри списка. Фактически, для списков fmap делает именно то, что делает map. Давайте на этот раз умножим все на 10:

fmap (* 10)  [2,4,8,16] = [20,40,80,160]
--  Int->Int    [Int]         [Int]

Я бы описал это как отображение функции, которая умножается на 10, по списку.

fmap также работает на Maybe

Что еще я могу fmap? Давайте использовать тип данных Maybe, который имеет два типа значений: Nothing и Just x. (Вы можете использовать Nothing для обозначения невозможности получить ответ, а Just x - для обозначения ответа.)

fmap  (+7)    (Just 10)  = Just 17
fmap  (+7)     Nothing   = Nothing
--  Int->Int  Maybe Int    Maybe Int

Хорошо, снова fmap использует (+7) внутри Maybe. И мы можем отображать и другие функции. length находит длину списка, поэтому мы можем отобразить его более Maybe [Double]

fmap    length             Nothing                      = Nothing
fmap    length    (Just [5.0, 4.0, 3.0, 2.0, 1.573458]) = Just 5
--  [Double]->Int         Maybe [Double]                  Maybe Int

На самом деле length :: [a] -> Int, но я использую его здесь, на [Double], поэтому я специализировался на нем.

Давайте использовать show, чтобы превратить материал в строки. Втайне реальный тип show - Show a => a -> String, но это немного длинновато, и я использую его здесь на Int, поэтому он специализируется на Int -> String.

fmap  show     (Just 12)  = Just "12"
fmap  show      Nothing   = Nothing
-- Int->String  Maybe Int   Maybe String

также, оглядываясь на списки

fmap   show     [3,4,5] = ["3", "4", "5"]
-- Int->String   [Int]       [String]

fmap работает на Either something

Давайте воспользуемся им для немного другой структуры Either. Значения типа Either a b являются либо Left a значениями, либо Right b значениями. Иногда мы используем Either для обозначения успеха Right goodvalue или неудачи Left errordetails, а иногда просто для того, чтобы смешать значения двух типов в один. В любом случае, функтор для типа данных Either работает только с Right - он оставляет только Left значения. Это имеет смысл, особенно если вы используете правильные значения как успешные (и на самом деле мы не сможем в состоянии заставить его работать на обоих, потому что типы не обязательно совпадают). Давайте использовать тип Either String Int в качестве примера.

fmap (5*)      (Left "hi")     =    Left "hi"
fmap (5*)      (Right 4)       =    Right 20
-- Int->Int  Either String Int   Either String Int

Это заставляет (5*) работать внутри Either, но для Eithers изменяются только значения Right. Но мы можем сделать это наоборот на Either Int String, если функция работает со строками. Давайте поместим ", cool!" в конец материала, используя (++ ", cool!").

fmap (++ ", cool!")          (Left 4)           = Left 4
fmap (++ ", cool!") (Right "fmap edits values") = Right "fmap edits values, cool!"
--   String->String    Either Int String          Either Int String

Особенно здорово использовать fmap на вводе-выводе

Теперь один из моих любимых способов использования fmap - использовать его со значениями IO для редактирования значения, которое мне дает какая-либо операция ввода-вывода. Давайте сделаем пример, который позволяет вам что-то ввести, а затем сразу же распечатать:

echo1 :: IO ()
echo1 = do
    putStrLn "Say something!"
    whattheysaid <- getLine  -- getLine :: IO String
    putStrLn whattheysaid    -- putStrLn :: String -> IO ()

Мы можем написать это так, как мне кажется:

echo2 :: IO ()
echo2 = putStrLn "Say something" 
        >> getLine >>= putStrLn

>> делает одно за другим, но мне это нравится потому, что >>= берет строку, которую нам дал getLine, и передает ее в putStrLn, который принимает строку. Что, если бы мы хотели просто поприветствовать пользователя:

greet1 :: IO ()
greet1 = do
    putStrLn "What's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

Если бы мы хотели написать это более аккуратно, я бы немного застрял. Я должен был бы написать

greet2 :: IO ()
greet2 = putStrLn "What's your name?" 
         >> getLine >>= (\name -> putStrLn ("Hello, " ++ name))

что не лучше, чем версия do. На самом деле там есть обозначение do, поэтому вам не нужно этого делать. Но может ли fmap прийти на помощь? Да, оно может. ("Hello, "++) - это функция, которую я могу отобразить по getLine!

fmap ("Hello, " ++)  getLine   = -- read a line, return "Hello, " in front of it
--   String->String  IO String    IO String

мы можем использовать это так:

greet3 :: IO ()
greet3 = putStrLn "What's your name?" 
         >> fmap ("Hello, "++) getLine >>= putStrLn

Мы можем проделать этот трюк со всем, что нам дают. Давайте не соглашаемся с тем, что было набрано: "True" или "False":

fmap   not      readLn   = -- read a line that has a Bool on it, change it
--  Bool->Bool  IO Bool       IO Bool

Или просто сообщим размер файла:

fmap  length    (readFile "test.txt") = -- read the file, return its length
--  String->Int      IO String              IO Int
--   [a]->Int        IO [Char]              IO Int     (more precisely)

Выводы: что делает fmap и для чего он нужен?

Если вы наблюдали закономерности в типах и размышляли над примерами, то заметили, что fmap принимает функцию, которая работает с некоторыми значениями, и применяет эту функцию к чему-то, что имеет или каким-то образом производит эти значения, редактируя значения. (например, readLn должен был читать Bool, поэтому в типе IO Bool в нем есть логическое значение в том смысле, что оно дает Bool, например, 2 [4,5,6] содержит Int.)

fmap :: (a -> b) -> Something a -> Something b

это работает для Something, являющихся списком (написано []), Maybe, Either String, Either Int, IO и множеством других вещей. Мы называем это Functor, если это работает разумно (есть некоторые правила - позже). Фактический тип fmap

fmap :: Functor something => (a -> b) -> something a -> something b

но для краткости мы обычно заменяем something на f. А вот с компилятором все равно:

fmap :: Functor f => (a -> b) -> f a -> f b

Посмотрите еще раз на типы и убедитесь, что это всегда работает - подумайте о Either String Int внимательно - что f в тот раз?

Приложение: что такое правила Functor и почему они у нас есть?

id - функция идентификации:

id :: a -> a
id x = x

Вот правила:

fmap id  ==  id                    -- identity identity
fmap (f . g)  ==  fmap f . fmap g  -- composition

Во-первых, идентичность идентичности: если вы сопоставите функцию, которая ничего не делает, это ничего не меняет. Это кажется очевидным (многие правила таковы), но вы можете интерпретировать это как утверждение, что fmap разрешено только изменять значения, а не структуру. fmap не разрешено превращать Just 4 в Nothing, или [6] в [1,2,3,6], или Right 4 в Left 4, потому что изменились не только данные - изменилась структура или контекст для этих данных.

Я однажды применил это правило, когда работал над проектом графического пользовательского интерфейса - я хотел иметь возможность редактировать значения, но не мог этого сделать, не изменив структуру внизу. Никто бы на самом деле не заметил разницы, потому что он имел тот же эффект, но осознание того, что он не подчиняется правилам функтора, заставило меня переосмыслить весь мой дизайн, и теперь он стал намного чище, изящнее и быстрее.

Во-вторых, композиция: это означает, что вы можете выбрать, отображать ли fmap по одной функции за раз, или fmap их обеих одновременно. Если fmap оставляет структуру / контекст ваших значений в покое и просто редактирует их с заданной функцией, он также будет работать с этим правилом.

У математиков есть секретное третье правило, но мы не называем его правилом в Haskell, потому что оно просто выглядит как объявление типа:

fmap :: (a -> b) -> something a -> something b

Это мешает вам применить функцию, например, только к первому значению в вашем списке. Этот закон соблюдается компилятором.

Зачем они нам? Чтобы убедиться, что fmap не делает ничего скрытно за кулисами и не меняет того, чего мы не ожидали. Они не выполняются компилятором (просить компилятор доказать теорему перед компиляцией вашего кода нечестно и замедлит компиляцию - программист должен это проверить). Это означает, что вы можете немного обмануть законы, но это плохой план, потому что ваш код может дать неожиданные результаты.

Законы Functor должны гарантировать, что fmap применяет вашу функцию справедливо, одинаково, везде и без каких-либо других изменений. Хорошая, чистая, понятная, надежная вещь многоразового использования.

person AndrewC    schedule 30.10.2012
comment
Для редактирования требуется минимум 6 изменений, поэтому вместо этого комментарий: в fmap (* 10) выводе есть опечатка, где 10 должно быть 20. Хороший мини-учебник :) - person drumfire; 26.12.2014
comment
@drumfire Спасибо за это - исправлено. :) - person AndrewC; 26.12.2014
comment
Исключительно хорошо структурированное объяснение, все сделано без использования множества других терминов, которые не узнает тот, кто не понимает Функторы (я бы сказал, что «параметризованный» - главный виновник). - person HenryRootTwo; 02.01.2015
comment
Разве putStrLn "What's your name?" >> getLine >>= (\name -> putStrLn ("Hello, " ++ name)) не то же самое, что putStrLn "What's your name?" >> getLine >>= (putStrLn . ("Hello, " ++))? Что немного лучше. - person Cameron Martin; 27.01.2015
comment
@CameronMartin Да, конечно, лучше. Я бы использовал версию fmap в реальной жизни и сделал бы import Data.Functor (по секрету я почти всегда использую import Control.Applicative, чтобы получить <*>), поэтому я могу писать как putStrLn "What's your name?" >> ("Hello, " ++) <$> getLine >>= putStrLn - person AndrewC; 30.01.2015
comment
Это лучшее объяснение, которое я когда-либо видел. Может быть, вы случайно написали учебник по аппликативу и монаде? Я бы заплатил сколько бы вы ни заплатили, чтобы увидеть их = D - person Ivan Wang; 31.03.2017
comment
@AndrewC поклонись! Большое спасибо за ваши усилия по предоставлению такого ясного и ясного объяснения! - person Swapnil B.; 21.04.2018
comment
Во-вторых, сделайте одно для аппликативного и монадного - person P Hemans; 13.05.2021

Важно держать в голове различие между самим функтором и значением в типе, к которому применен функтор. Сам функтор является конструктором типа, например Maybe, IO или конструктором списка []. Значение в функторе - это какое-то конкретное значение в типе, к которому применен этот конструктор типа. например Just 3 - одно конкретное значение в типе Maybe Int (этот тип - это функтор Maybe, применяемый к типу Int), putStrLn "Hello World" - одно конкретное значение в типе IO (), а [2, 4, 8, 16, 32] - одно конкретное значение в типе [Int].

Мне нравится думать о значении в типе с примененным функтором как о «таком же», что и значение в базовом типе, но с некоторым дополнительным «контекстом». Люди часто используют аналогию с контейнером для функтора, который довольно естественно работает для довольно многих функторов, но затем становится больше помехой, чем помощью, когда вам нужно убедить себя, что IO или (->) r подобны контейнеру.

Таким образом, если Int представляет собой целое значение, тогда Maybe Int представляет собой целое значение, которое может отсутствовать («может не присутствовать» - это «контекст»). [Int] представляет собой целое число с рядом возможных значений (это та же интерпретация функтора списка, что и «недетерминированная» интерпретация монады списка). IO Int представляет собой целочисленное значение, точное значение которого зависит от всего юниверса (или, альтернативно, оно представляет собой целое значение, которое может быть получено путем запуска внешнего процесса). Char -> Int - это целочисленное значение для любого Char значения («функция, принимающая r в качестве аргумента» - это функтор для любого типа r; с r как Char (->) Char - это конструктор типа, который является функтором, который применяется к Int становится (->) Char Int или Char -> Int в инфиксная запись).

Единственное, что вы можете сделать с общим функтором, - это fmap с типом Functor f => (a -> b) -> (f a -> f b). fmap преобразует функцию, которая работает с нормальными значениями, в функцию, которая работает со значениями с дополнительным контекстом, добавленным функтором; то, что именно он делает, различается для каждого функтора, но вы можете сделать это со всеми из них.

Таким образом, с функтором Maybe fmap (+1) - это функция, которая вычисляет целое число, которое может отсутствовать, на 1 больше, чем его входное целое число, которое может отсутствовать. Со списковым функтором fmap (+1) - это функция, которая вычисляет недетерминированное целое число на 1 больше, чем его входное недетерминированное целое число. С функтором IO fmap (+1) - это функция, которая вычисляет целое число на 1 больше, чем его входное целое число, значение которого зависит от внешней вселенной. С функтором (->) Char fmap (+1) - это функция, которая добавляет 1 к целому числу, зависящему от Char (когда я передаю Char возвращаемому значению, я получаю на 1 больше, чем то, что я получил бы, если бы тот же Char был введен в исходное значение. ).

Но в целом для какого-то неизвестного функтора f, fmap (+1), примененного к некоторому значению в f Int, является "версией функтора" функции (+1) на обычных Ints. Он добавляет 1 к целому числу в любом «контексте», который имеет этот конкретный функтор.

Сам по себе fmap не обязательно так полезен. Обычно, когда вы пишете конкретную программу и работаете с функтором, вы работаете с одним конкретным функтором и часто думаете о fmap как о том, что он делает для этого конкретного функтора. Когда я работаю с [Int], я часто не думаю о своих [Int] значениях как о недетерминированных целых числах, я просто думаю о них как о списках целых чисел, и я думаю о fmap так же, как я думаю о map.

Так зачем возиться с функторами? Почему бы просто не использовать map для списков, applyToMaybe для Maybe и applyToIO для IO? Тогда все будут знать, что они делают, и никому не придется понимать странные абстрактные концепции, такие как функторы.

Ключевым моментом является признание того, что существует много функторов; для начала почти все типы контейнеров (отсюда и аналогия с контейнером для того, что такое функторы ). У каждого из них есть операция, соответствующая fmap, даже если у нас нет функторов. Всякий раз, когда вы пишете алгоритм исключительно в терминах операции fmap (или map, или того, что она называется для вашего конкретного типа), тогда, если вы пишете его в терминах функторов, а не вашего конкретного типа, тогда он работает для всех функторы.

Он также может служить формой документации. Если я передам одно из значений своего списка написанной вами функции, которая работает со списками, она сможет делать что угодно. Но если я передам свой список функции, которую вы написали, которая работает со значениями в произвольном функторе, то я знаю, что реализация вашей функции не может использовать функции списка, а только функции функтора.

Если вспомнить, как бы вы использовали функциональные элементы в традиционном императивном программировании, это может помочь увидеть преимущества. Типы контейнеров, такие как массивы, списки, деревья и т. Д., Обычно имеют некоторый шаблон, который вы используете для итерации по ним. Он может немного отличаться для разных контейнеров, хотя библиотеки часто предоставляют стандартные интерфейсы итераций для решения этой проблемы. Но вы все равно пишете небольшой цикл for каждый раз, когда хотите перебирать их, и когда вам нужно вычислить результат для каждого элемента в контейнере и собрать все результаты, которые вы обычно в конечном итоге смешиваете в логике для создания нового контейнера на ходу.

fmap - это каждый цикл for той формы, которую вы когда-либо напишете, раз и навсегда отсортированный авторами библиотеки, прежде чем вы даже сядете за программирование. Кроме того, его также можно использовать с такими вещами, как Maybe и (->) r, которые, вероятно, не будут рассматриваться как имеющие какое-либо отношение к разработке согласованного интерфейса контейнера на императивных языках.

person Ben    schedule 31.10.2012

В Haskell функторы охватывают понятие контейнеров с «материалом», так что вы можете манипулировать этим «материалом» без изменения формы контейнера.

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

fmap :: Functor f => (a -> b) -> (f a -> f b) 

Например, [], конструктор типа списка, является функтором:

> fmap show [1, 2, 3]
["1","2","3"]

и многие другие конструкторы типов Haskell, такие как Maybe и Map Integer 1:

> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]

Обратите внимание, что fmap не разрешено изменять «форму» контейнера, поэтому, если, например, вы fmap список, результат будет иметь такое же количество элементов, а если вы fmap a Just, он не может стать Nothing. Формально мы требуем, чтобы fmap id = id, т.е. если вы fmap функция идентификации, ничего не изменится.

До сих пор я использовал термин «контейнер», но на самом деле он немного шире. Например, IO также является функтором, и то, что мы подразумеваем под «формой» в этом случае, так это то, что fmap в действии IO не должно изменять побочные эффекты. Фактически, любая монада является функтором 2.

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

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

1 Но Set нет, потому что он может хранить только Ord типов. Функторы должны иметь возможность содержать любой тип.
2 По историческим причинам Functor не является суперклассом Monad, хотя многие думают, что так и должно быть.

person hammar    schedule 30.10.2012

Посмотрим на типы.

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

Но что это значит?

Во-первых, f - это переменная типа здесь, и она обозначает конструктор типа: f a - это тип; a - это переменная типа, обозначающая некоторый тип.

Во-вторых, учитывая функцию g :: a -> b, вы получите fmap g :: f a -> f b. Т.е. fmap g - это функция, преобразующая объекты типа f a в объекты типа f b. Обратите внимание, здесь мы не можем добраться до вещей типа a и b. Функция g :: a -> b каким-то образом заставлена ​​работать с объектами типа f a и преобразовывать их в объекты типа f b.

Обратите внимание, что f то же самое. Меняется только другой тип.

Что это обозначает? Это может означать многое. f обычно рассматривается как «контейнер» с вещами. Затем fmap g позволяет g воздействовать на внутреннюю часть этих контейнеров, не взламывая их. Результаты по-прежнему заключены «внутрь», класс типов Functor не дает нам возможности открыть их или заглянуть внутрь. Просто некоторая трансформация внутри непрозрачных вещей - это все, что мы получаем. Любая другая функциональность должна быть откуда-то еще.

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

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

fmap id      ===  id
fmap (h . g) ===  fmap h . fmap g

Обратите внимание на разные типы операторов (.):

     g  :: a -> b                         fmap g  :: f a -> f b
 h      ::      b -> c           fmap h           ::        f b -> f c
----------------------          --------------------------------------
(h . g) :: a      -> c          (fmap h . fmap g) :: f a        -> f c

Это означает, что какие бы отношения ни существовали между типами a, b и c путем соединения проводов, так сказать о таких функциях, как g и h , также существует между типами f a, f b и f c путем соединения проводов функций fmap g и fmap h.

Или любую подключенную диаграмму, которую можно нарисовать «с левой стороны», в мире a, b, c, ..., можно нарисовать «с правой стороны», в мире f a, f b, f c, ..., заменив функции g, h, ... на функции fmap g, fmap h, ..., а функции id :: a -> a на fmap id, которые сами также являются id :: f a -> f a по законам Functor.

person Will Ness    schedule 30.10.2012