itmo_conspects

Лекция 6. Аппликативный функтор и монада

Аппликативный функтор

Функтор - это конструктор, который помогает функциям работать с составными типами

Аппликативный функтор - функтор, который умеет извлекать функцию из составного типа f и применять ее к значению внутри типа f, чтобы получить результат, обернутый в тип f

В Haskell аппликативный функтор выражается классом типов Applicative:

ghci> :i Applicative
type Applicative :: (* -> *) -> Constraint
class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}
        -- Defined in `GHC.Internal.Base'
-- ...

Для создания представителя нужно определить две функции - pure и оператор <*> (либо pure и liftA2)

Функция pure оборачивает значение типа a в составной тип f a (например, для f = Maybe функция pure a возвращает Just a). Оператор <*> же переводит тип f (a -> b), содержащий функцию a -> b, в функцию f a -> f b

Для типа Maybe a аппликативный функтор выглядит так:

instance Applicative Maybe where
  pure :: a -> Maybe a
  pure x = Just x

  (<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
  Nothing <*> _  = Nothing -- здесь функция обернута в Nothing, то есть не дана как аргумент
  Just f  <*> x  = fmap f x -- здесь функция обернута в Just

Таким образом, можно использовать функции внутри составных типов:

ghci> Just (+1) <*> Just 2
Just 3
ghci> (+) <$> Just 1 <*> Just 2 -- альтернативная запись
Just 3
ghci> Just (+1) <*> Nothing
Nothing
ghci> Nothing <*> Just 2   
Nothing

Рассмотрим определения аппликативного функтора для других типов:

Functor позволяет применить функцию только одного аргумента к значению в контейнере, тогда как Applicative позволяет применить функцию к нескольким обернутым аргументам сразу и выстраивать цепочку вида f <$> a <*> b <*> c, например, для того, чтобы собрать результат из нескольких независимых проверок:

data User = User String Int
  deriving Show

validateName :: String -> Maybe String
validateName name
  | null name = Nothing
  | otherwise = Just name

validateAge :: Int -> Maybe Int
validateAge age
  | age >= 0 && age < 150 = Just age
  | otherwise             = Nothing

mkUser :: String -> Int -> Maybe User
mkUser name age = User <$> validateName name <*> validateAge age

Функция mkUser возвращает Nothing, если у пользователя нет имени либо возраст не находится в допустимом диапазоне, иначе Just (User ...)

Для аппликативного функтора справедливы свойства:

Монада

Мона́да (или мона́д, Monad) - это аппликативный функтор с операцией >>=, которая позволяет выбирать следующий шаг по результату предыдущего

В Haskell монада представлена в виде класса типов Monad:

ghci> :i Monad
type Monad :: (* -> *) -> Constraint
class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  {-# MINIMAL (>>=) #-}
        -- Defined in `GHC.Internal.Base'
-- ...

Здесь функция return по умолчанию является оберткой над pure из Applicative - так случилось, потому что иерархически в старых версиях Haskell классы Applicative и Monad не были связаны

Другая функция - оператор связывания >>= (bind operator) - берет упакованное значение (например, Maybe a), достает из него значение и передает их в функцию, которая возвращает новый упакованный результат. Оператор >>= является левоассоциативным и имеет приоритет 1

Тип функции (a -> m b) в объявлении >>= связан с тем, что помимо обычной функции вида a -> b, которая создается с помощью функциональной стрелки, выделяют стрелку Клейсли - функцию a -> m b, которая помимо обработки аргумента возвращает результат, обернутый в монаду, которая несет дополнительный эффект

Любую стандартную чистую функцию a -> b можно превратить в стрелку Клейсли, если обернуть её результат в монаду с помощью функции return:

toKleisli :: Monad m => (a -> b) -> (a -> m b)
toKleisli f = \x -> return (f x)

Оператор >>= как раз принимает упакованное в монаду значение и стрелку Клейсли, чтобы затем вернуть упакованный результат

Другой оператор упрощенного связывания >> (или последовательный оператор, then operator) работает немного по-другому - вместо стрелки Клейсли принимает упакованный результат, чтобы вернуть его m b либо первый аргумент m a на основе эффекта, который содержит m a:

(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y

Здесь вместо тривиального x >> y = y определение x >> y = x >>= \_ -> y будет корректным, так как учитывает x при возврате, таким образом, например, возвращая ошибку Left err, вместо результата Right result

Рассмотрим обертку Identity, для которой определим аппликативный функтор и монаду:

newtype Identity a = Identity { runIdentity :: a }
  deriving (Eq, Show)

instance Functor Identity where
  fmap f m = Identity { runIdentity = (f (runIdentity m)) }

instance Applicative Identity where
  pure x = Identity { runIdentity = x }
  (Identity f) <*> (Identity x) = Identity $ f x

instance Monad Identity where
  (>>=) :: Identity a -> (a -> Identity b) -> Identity b
  (Identity x) >>= k = k x
  -- или аналогично
  m >>= k = k (runIdentity m)

Возьмем стрелку Клейсли, которая берет целое число, прибавляет один и запаковывает в Identity:

plusOneM :: Integer -> Identity Integer
plusOneM x = Identity (x + 1)

Пример использования:

ghci> plusOneM 5
Identity {runIdentity = 6}
ghci> runIdentity (plusOneM 5)
6
ghci> runIdentity $ plusOneM 5 >>= plusOneM >>= plusOneM >>= plusOneM
9

Monad расширяет возможности Applicative в том, что можно задать порядок, если следующий шаг обработки зависит от результата предыдущего, что делает оператор >>=

Для Monad справедливы следующие свойства: