Монад без упаковки?
В большинстве объяснений монады используются примеры, где монада обертывает значение. Например. Maybe a
, где переменная типа a
- это то, что завернуто. Но мне интересно о монадах, которые никогда ничего не обертывают.
Для надуманного примера предположим, что у меня есть реальный робот, который можно контролировать, но не имеет датчиков. Может быть, я хотел бы контролировать это следующим образом:
robotMovementScript :: RobotMonad ()
robotMovementScript = do
moveLeft 10
moveForward 25
rotate 180
main :: IO ()
main =
liftIO $ runRobot robotMovementScript connectToRobot
В нашем воображаемом API connectToRobot
возвращает какой-то дескриптор физического устройства. Это соединение становится "контекстом" RobotMonad
. Поскольку наше соединение с роботом никогда не может отправить нам значение, конкретный тип монады всегда RobotMonad ()
.
Некоторые вопросы:
- Может ли мой надуманный пример выглядеть правильно?
- Я правильно понимаю идею монада "контекст"? Правильно ли я описываю соединение робота как контекст?
- Имеет ли смысл иметь монаду - например,
RobotMonad
- которая никогда не обертывает значение? Или это противоречит основному понятию монадов?
- Моноиды лучше подходят для такого рода приложений? Я могу представить, как конкатенировать действия управления роботом с помощью
<>
. Хотя обозначение do
кажется более читаемым.
- В определении монады было бы/могло бы быть что-то, что гарантирует, что тип всегда
RobotMonad ()
?
В качестве примера я рассмотрел Data.Binary.Put
. Кажется, он похож (или, может быть, идентичен?) На то, о чем я думаю. Но он также включает монадию Writer и моноид Builder. Учитывая эти добавленные морщины и мой текущий уровень мастерства, я думаю, что монада Put
может быть не самым поучительным примером.
Edit
На самом деле мне не нужно создавать робот или API вроде этого. Пример полностью надуман. Мне просто нужен был пример, где никогда не было бы причин вытащить ценность из монады. Поэтому я не прошу простейший способ решить проблему с роботом. Скорее, этот мысленный эксперимент о монадах без внутренних ценностей является попыткой лучше понять монады в целом.
Ответы
Ответ 1
TL; DR Monad без его обернутого значения не является особо особенным, и вы получаете все ту же мощь, что и его список как список.
Там известна монада Free
. Это полезно, потому что оно в некотором смысле является хорошим представителем для всех других монадов - если вы можете понять поведение монады Free
, в каком-то случае вы хорошо понимаете, как Monad
вообще будет вести себя там.
Похоже на это
data Free f a = Pure a
| Free (f (Free f a))
и всякий раз, когда f
является Functor
, Free f
является Monad
instance Functor f => Monad (Free f) where
return = Pure
Pure a >>= f = f a
Free w >>= f = Free (fmap (>>= f) w)
Итак, что происходит, когда a
всегда ()
? Нам больше не нужен параметр a
data Freed f = Stop
| Freed (f (Freed f))
Очевидно, что это не может быть Monad
больше, поскольку оно имеет неправильный вид (тип типов).
Monad f ===> f :: * -> *
Freed f :: *
Но мы все же можем определить что-то вроде функции Monad
ic, избавившись от a
частей
returned :: Freed f
returned = Stop
bound :: Functor f -- compare with the Monad definition
=> Freed f -> Freed f -- with all `a`s replaced by ()
-> Freed f
bound Stop k = k Pure () >>= f = f ()
bound (Freed w) k = Free w >>= f =
Freed (fmap (`bound` k) w) Free (fmap (>>= f) w)
-- Also compare with (++)
(++) [] ys = ys
(++) (x:xs) ys = x : ((++) xs ys)
Что выглядит (и есть!) a Monoid
.
instance Functor f => Monoid (Freed f) where
mempty = returned
mappend = bound
И Monoid
может быть первоначально смоделирован списками. Мы используем универсальное свойство списка Monoid
, где, если у нас есть функция Monoid m => (a -> m)
, мы можем превратить список [a]
в m
.
convert :: Monoid m => (a -> m) -> [a] -> m
convert f = foldr mappend mempty . map f
convertFreed :: Functor f => [f ()] -> Freed f
convertFreed = convert go where
go :: Functor f => f () -> Freed f
go w = Freed (const Stop <$> w)
Итак, в случае с вашим роботом мы можем уйти, просто используя список действий
data Direction = Left | Right | Forward | Back
data ActionF a = Move Direction Double a
| Rotate Double a
deriving ( Functor )
-- and if we're using `ActionF ()` then we might as well do
data Action = Move Direction Double
| Rotate Double
robotMovementScript = [ Move Left 10
, Move Forward 25
, Rotate 180
]
Теперь, когда мы передаем его в IO
, мы явно преобразуем этот список направлений в Monad
, и мы можем видеть, что, беря наш начальный Monoid
и отправляя его на Freed
, а затем обрабатывая Freed f
как Free f ()
и интерпретируя это как начальное Monad
над действиями IO
, которые мы хотим.
Но ясно, что если вы не используете "завернутые" значения, вы не используете структуру Monad
. У вас может быть просто список.
Ответ 2
Я попытаюсь дать частичный ответ для этих частей:
- Имеет ли смысл иметь монаду - например,
RobotMonad
- которая никогда не обертывает значение? Или это противоречит основному понятию монадов? - Моноиды лучше подходят для такого рода приложений? Я могу представить, как конкатенировать действия управления роботом с помощью
<>
. Хотя обозначение кажется более читаемым. - В определении монады было бы/могло бы быть что-то, что гарантирует, что тип всегда
RobotMonad ()
?
Операция ядра для монадов - операция монадического связывания
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
Это означает, что действие зависит (или может зависеть) от значения предыдущего действия. Итак, если у вас есть концепция, которая по своей сути не несет в себе что-то, что можно рассматривать как значение (даже в сложной форме, такой как монада продолжения), монада не является хорошей абстракцией.
Если мы откажемся от >>=
, мы в основном остаемся с Applicative
. Это также позволяет нам составлять действия, но их комбинации не могут зависеть от значений предыдущих.
Существует также экземпляр Applicative
, который не несет никаких значений, как вы предположили: Data.Functor.Constant. Его действия типа a
должны быть моноидами, так что они могут быть составлены вместе. Это похоже на самое близкое понятие к вашей идее. И, конечно, вместо Constant
мы могли бы напрямую использовать Monoid
.
Тем не менее, возможно, более простое решение состоит в том, чтобы иметь монаду RobotMonad a
, которая несет значение (которое было бы по существу изоморфно монаде Writer
, как уже упоминалось). И объявите runRobot
требованием RobotMonad ()
, поэтому можно было бы выполнить только скрипты без значения:
runRobot :: RobotMonad () -> RobotHandle -> IO ()
Это позволит использовать нотацию do
и работать со значениями внутри робота script. Даже если у робота нет датчиков, часто бывает полезно иметь возможность передавать значения вокруг. Расширение концепции позволит вам создать монадный трансформатор, такой как RobotMonadT m a
(похожий на WriterT
) с чем-то вроде
runRobotT :: (Monad m) => RobotMonadT m () -> RobotHandle -> IO (m ())
или, возможно,
runRobotT :: (MonadIO m) => RobotMonadT m () -> RobotHandle -> m ()
которая была бы мощной абстракцией, которая позволила бы объединить роботизированные действия с произвольной монадой.
Ответ 3
Ну есть
data Useless a = Useless
instance Monad Useless where
return = const Useless
Useless >>= f = Useless
но, как я указал, это не полезно.
То, что вы хотите, это монада Writer
, которая обертывает моноид в виде монады, поэтому вы можете использовать обозначение.
Ответ 4
Ну, похоже, у вас есть тип, который поддерживает только
(>>) :: m a -> m b -> m b
Но вы также указываете, что хотите использовать m ()
s. В этом случае я проголосовал за
foo = mconcat
[ moveLeft 10
, moveForward 25
, rotate 180]
Как простое решение. Альтернативой является сделать что-то вроде
type Robot = Writer [RobotAction]
inj :: RobotAction -> Robot ()
inj = tell . (:[])
runRobot :: Robot a -> [RobotAction]
runRobot = snd . runWriter
foo = runRobot $ do
inj $ moveLeft 10
inj $ moveForward 25
inj $ rotate 180
Использование монады Writer
.
Проблема не обертывания значения заключается в том, что
return a >>= f === f a
Итак, предположим, что у нас была монада, которая игнорировала значение, но содержала другую интересную информацию,
newtype Robot a = Robot {unRobot :: [RobotAction]}
addAction :: RobotAction -> Robot a -> Robot b
f a = Robot [a]
Теперь, если мы игнорируем значение,
instance Monad Robot where
return = const (Robot [])
a >>= f = a -- never run the function
Тогда
return a >>= f /= f a
поэтому у нас нет монады. Поэтому, если вы хотите, чтобы монада имела какие-либо интересные состояния, ==
возвращает false, тогда вам нужно сохранить это значение.