Подтвердить что ты не робот

Монад без упаковки?

В большинстве объяснений монады используются примеры, где монада обертывает значение. Например. 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 вроде этого. Пример полностью надуман. Мне просто нужен был пример, где никогда не было бы причин вытащить ценность из монады. Поэтому я не прошу простейший способ решить проблему с роботом. Скорее, этот мысленный эксперимент о монадах без внутренних ценностей является попыткой лучше понять монады в целом.

4b9b3361

Ответ 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, тогда вам нужно сохранить это значение.