Functor: применить функцию внутри обёртки
Вторая ступень иерархии. Functor отвечает на один вопрос: как применить обычную функцию к значению, которое лежит внутри какой-то структуры, не разбирая структуру руками. Метод один,
fmap, законов два. В этом уроке:fmapи оператор<$>, два закона функтора, почему функтор это не только контейнер (функции, пары и Either тоже функторы), инстанс своими руками и композиция функторов.
Идея
У тебя есть Maybe Int и функция (+1) :: Int -> Int. Применить её в лоб нельзя: (+1) хочет Int, а у тебя Maybe Int. Можно разобрать паттерн-матчингом:
bump :: Maybe Int -> Maybe Int
bump Nothing = Nothing
bump (Just x) = Just (x + 1)
Знакомо до зевоты: то же самое ты писал для списков, для Either, для дерева. Везде один скелет, меняется только функция, которую применяем внутри. Этот скелет и есть Functor:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Читается так: дай мне функцию a -> b и структуру с a внутри, я верну такую же структуру, но с b внутри. Слово «структура» тут важнее слова «контейнер», к этому вернёмся. Инстанс для Maybe это ровно наш bump, обобщённый до любой функции:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
ghci> fmap (+1) (Just 10)
Just 11
ghci> fmap (+1) Nothing
Nothing
ghci> fmap show (Just 42)
Just "42"
fmap show поменял и значение, и его тип (Maybe Int стал Maybe String), но не тронул форму: Just остался Just, Nothing остался Nothing. В этом вся суть: fmap сохраняет форму.
Оператор <$>
fmap пишут так часто, что у него есть инфиксный псевдоним:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
Сравни (*2) рядом со знакомым $. Помнишь из урока про функции высшего порядка: f $ x это просто f x, применение. Тогда f <$> x это применение, но сквозь обёртку. Один символ разницы, и он намеренный:
ghci> (*2) $ 21
42
ghci> (*2) <$> Just 21
Just 42
ghci> (*2) <$> [1, 2, 3]
[2,4,6]
Список тоже функтор, и его fmap это в точности map из урока 04. То есть map всё это время был частным случаем fmap, прибитым к спискам.
Два закона
Functor, как и моноид, держится на законах, которые компилятор не проверяет:
-- 1. тождество: fmap id == id
-- 2. композиция: fmap (g . f) == fmap g . fmap f
Первый закон запрещает функтору делать что-либо кроме применения функции: fmap id обязан вернуть структуру нетронутой. Если твой fmap втихаря переставляет элементы или дублирует их, закон тождества падает. Второй говорит, что два прохода с f, потом с g это то же самое, что один проход с g . f. Практический смысл: компилятор и ты вправе сливать цепочки fmap в одну, не меняя смысл. Оба закона выполняются автоматически, если fmap честно только применяет функцию и ничего больше.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.