Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

Нечеткое объяснение будет заключаться в том, что 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, и многие модули определяют больше экземпляров Functor (и других классов типов).

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

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

Ответ 2

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

Учебное пособие Haskell Functors

Я отвечу на ваш вопрос, используя примеры, и я поставлю типы под комментариями.

Следите за шаблоном в типах.

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

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

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

Поэтому он использует функцию (subtract 1) внутри списка. Фактически, для списков fmap делает exaccty, что делает 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 находит длину списка, поэтому мы можем fmap его над 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 или fail Left errordetails, а иногда просто для смешивания значений двух типов в один. Во всяком случае, функтор для любого типа данных работает только с 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*) внутри Либо, но для 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 на IO

Теперь один из моих любимых способов использования 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 your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

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

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

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

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

мы можем использовать его следующим образом:

greet3 :: IO ()
greet3 = putStrLn "What 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, eg2 [4,5,6] имеет Int в нем.)

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

это работает для того, что есть List-of (записано []), 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, потому что больше, чем просто данные изменились - структура или контекст для этих данных изменились.

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

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

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

Ответ 3

Важно держать раздельно в вашей голове различие между самим функтором и значением в типе, к которому применяется функтор. Сам функтор является конструктором типа 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 представляет целочисленное значение, точное значение которого зависит от всего юниверса (или, наоборот, оно представляет целочисленное значение, которое может быть получено путем запуска внешнего процесса). A Char -> Int является целочисленным значением для любого значения Char ( "функция, принимающая r в качестве аргумента" ) является функтором для любого типа r, а r как Char (->) Char является конструктором типа который является функтором, который применяется к Int, становится (->) Char Int или Char -> Int в инфиксной нотации).

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

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

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

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

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

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

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

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

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

Ответ 4

В 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, поэтому они называются endofunctors (функторы от категории к себе).

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

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

Ответ 5

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

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 действовать внутри этих контейнеров, не открывая их. Результаты все еще закрыты "внутри", typeclass Functor не дает нам способностей открыть их или заглянуть внутрь. Просто какая-то трансформация внутри непрозрачных вещей - это все, что мы получаем. Любая другая функциональность должна появиться откуда-то еще.

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

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

fmap id === id
fmap (f . g) === fmap f . fmap g