Я случайно написал
Руководство по функциям 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