Заинтригованный (->) как экземпляры монады и функтора

Я очень заинтригован (->), когда посмотрел информацию о (->) в ghci. В нем говорится:

data (->) a b -- Defined in `GHC.Prim`

Пока все хорошо, но потом становится очень интересно, когда он говорит -

instance Monad ((->) r) -- Defined in `GHC.Base`
instance Functor ((->) r) -- Defined in `GHC.Base`

Что это подразумевает? Почему GHC определяет его как экземпляр Monad, а Functor для (->)?

Ответ 1

Сначала это может быть немного запутанным, но важно помнить, что (->) не является монадой или функтором, а (->) r is. Monad и Functor все имеют вид * -> *, поэтому они ожидают только одного параметра типа.

Это означает, что fmap для (->) r выглядит как

fmap g func = \x -> g (func x)

который также известен как

fmap g func = g . func

который является просто нормальной функциональной композицией! Когда вы fmap g над func, вы изменяете тип вывода, применяя к нему g. В этом случае, если func имеет тип a -> b, g должен иметь тип типа b -> c.

Экземпляр Monad более интересен. Он позволяет использовать результат применения приложения "до" этого приложения. То, что помогло мне понять, это увидеть пример вроде

f :: Double -> (Double,Double)
f = do
    x1 <- (2*)
    x2 <- (2+)
    return (x1, x2)

> f 1.0
(2.0, 3.0)

Что это значит, применяет неявный аргумент к f к каждой из функций в правой части привязок. Поэтому, если вы перейдете в 1.0 в f, он привяжет значение 2 * 1.0 к x1 и привяжет 2 + 1.0 к x2, а затем вернет (x1, x2). Это действительно упрощает применение одного аргумента ко многим подвыражениям. Эта функция эквивалентна

f' x = (2 * x, 2 + x)

Почему это полезно? Одним из распространенных способов использования является монада Reader, которая представляет собой просто оболочку newtype вокруг (->) r. Монада Reader упрощает применение статической глобальной конфигурации в вашем приложении. Вы можете написать код, например

myApp :: Reader Config ()
myApp = do
    config <- ask
    -- Use config here
    return ()

И затем вы запустите свое приложение с помощью runReader myApp initialConfig. Вы можете легко записывать действия в монаде Reader Config, составлять их, объединять их вместе, и все они имеют доступ к глобальной конфигурации readonly. Кроме того, есть компаньон ReaderT monad transformer, который позволяет вам встраивать его в ваш стек трансформатора, позволяя вам иметь очень сложные приложения, которые имеют легкий доступ к статической конфигурации.

Ответ 2

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

data a->b

instance Monad (r -> )

выглядит намного более естественным.

Для краткого объяснения: я считаю весьма полезным рассмотреть частный случай Monad (Bool -> ), который является в основном двухэлементным типом контейнера. Он имеет два элемента

\case
  False -> elem1
  True -> elem2

Итак, вы можете думать о экземпляре functor так же, как и для списков: отображать все "содержащиеся элементы".

Применения и экземпляры монады немного разные, это может помочь сделать "контейнерное преобразование" явным:

data Pair a = Pair a a

instance Functor Pair where
  fmap f (Pair a b) = Pair (f a) (f b)

instance Monad Pair where
  return a = Pair a a
  join (Pair (Pair a _) (Pair _ b))
      = Pair       a            b