Раздел 21 · Haskell

Functor: применить функцию внутри обёртки

middle~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

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-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.