Как вернуть чистое значение из нечистого метода
Я знаю, что это должно звучать тривиально, но мне было интересно, как вы можете развернуть значение из функтора и вернуть его как чистое значение?
Я пытался:
f::IO a->a
f x=(x>>=)
f= >>=
Что я должен разместить в правой части? Я не могу использовать return
так как это обернет это снова.
2 ответа
Это часто задаваемый вопрос: как извлечь значение "из" из моей монады не только в Хаскеле, но и в других языках. У меня есть теория о том, почему этот вопрос продолжает возникать, поэтому я постараюсь ответить в соответствии с этим; Я надеюсь, что это помогает.
Контейнеры отдельных значений
Вы можете думать о функторе (и, следовательно, о монаде) как о контейнере значений. Это наиболее ощутимо с (избыточным) Identity
функтор:
Prelude Control.Monad.Identity> Identity 42
Identity 42
В данном случае это всего лишь обертка вокруг значения 42
, Для этого конкретного контейнера вы можете извлечь значение, потому что оно гарантированно будет там:
Prelude Control.Monad.Identity> runIdentity $ Identity 42
42
В то время как Identity
кажется довольно бесполезным, вы можете найти другие функторы, которые, кажется, обернуть одно значение. Например, в F# вы часто встречаете такие контейнеры, как Async<'a>
или же Lazy<'a>
, которые используются для представления асинхронных или ленивых вычислений (последний не нужен для Haskell, потому что по умолчанию он ленив).
В Haskell вы найдете множество других однозначных контейнеров, таких как Sum
, Product
, Last
, First
, Max
, Min
и т. д. Общим для всех них является то, что они переносят одно значение, что означает, что вы можете извлечь это значение.
Я думаю, что когда люди впервые сталкиваются с функторами и монадами, они склонны думать о концепции контейнера данных таким образом: как контейнер с одним значением.
Контейнеры необязательных значений
К сожалению, некоторые распространенные монады в Хаскеле, кажется, поддерживают эту идею. Например, Maybe
также является контейнером данных, но может содержать ноль или одно значение. К сожалению, вы можете извлечь значение, если оно есть:
Prelude Data.Maybe> fromJust $ Just 42
42
Проблема в том, что fromJust
не полный, поэтому он потерпит крах, если вы позвоните с Nothing
значение:
Prelude Data.Maybe> fromJust Nothing
*** Exception: Maybe.fromJust: Nothing
Вы можете увидеть такую же проблему с Either
, Хотя я не знаю о встроенной частичной функции для извлечения Right
значение, вы можете легко написать один с сопоставлением с шаблоном (если вы игнорируете предупреждение компилятора):
extractRight :: Either l r -> r
extractRight (Right x) = x
Опять же, он работает в сценарии "счастливого пути", но может так же легко потерпеть крах:
Prelude> extractRight $ Right 42
42
Prelude> extractRight $ Left "foo"
*** Exception: <interactive>:12:1-26: Non-exhaustive patterns in function extractRight
Тем не менее, так как функции, такие как fromJust
существует, я полагаю, что это обманывает людей, впервые знакомых с концепцией функторов и монад, с мыслью о них как о контейнерах данных, из которых можно извлечь значение.
Когда вы сталкиваетесь с чем-то вроде IO Int
Впервые я могу понять, почему вы склонны думать о нем как о контейнере с одним значением. В некотором смысле, это так, но в другом смысле это не так.
Контейнеры с несколькими значениями
Даже со списками вы можете (пытаться) извлечь значение 'из' из списка:
Prelude> head [42..1337]
42
Тем не менее, он может потерпеть неудачу:
Prelude> head []
*** Exception: Prelude.head: empty list
На этом этапе, однако, должно быть ясно, что попытка извлечь значение '' из любого произвольного функтора является бессмысленной. Список - это функтор, но он содержит произвольное количество значений, включая ноль и бесконечное число.
Однако вы всегда можете написать функции, которые принимают "содержащиеся" значения в качестве входных данных и возвращают другое значение в качестве выходных данных. Вот произвольный пример такой функции:
countAndMultiply :: Foldable t => (t a, Int) -> Int
countAndMultiply (xs, factor) = length xs * factor
Хотя вы не можете "извлечь значение" из списка, вы можете применить свою функцию к каждому из значений в списке:
Prelude> fmap countAndMultiply [("foo", 2), ("bar", 3), ("corge", 2)]
[6,9,10]
поскольку IO
это функтор, вы можете сделать то же самое с ним:
Prelude> foo = return ("foo", 2) :: IO (String, Int)
Prelude> :t foo
foo :: IO (String, Int)
Prelude> fmap countAndMultiply foo
6
Дело в том, что вы не извлекаете значение из функтора, вы входите в функтор.
монада
Иногда функция, которую вы применяете к функтору, возвращает значение, уже заключенное в тот же контейнер данных. Например, у вас может быть функция, которая разбивает строку на определенный символ. Для простоты давайте просто посмотрим на встроенную функцию words
который разбивает строку на слова:
Prelude> words "foo bar"
["foo","bar"]
Если у вас есть список строк и применить words
каждому вы получите вложенный список:
Prelude> fmap words ["foo bar", "baz qux"]
[["foo","bar"],["baz","qux"]]
Результатом является вложенный контейнер данных, в данном случае список списков. Вы можете сгладить это с join
:
Prelude Control.Monad> join $ fmap words ["foo bar", "baz qux"]
["foo","bar","baz","qux"]
Это оригинальное определение монады: это функтор, который вы можете сгладить. В современном Хаскеле Monad
определяется связыванием (>>=
), из которого можно извлечь join
, но также возможно получить >>=
от join
,
IO как все значения
В этот момент вы можете задаться вопросом: какое это имеет отношение к IO
? не IO a
контейнер одного значения типа a
?
На самом деле, нет. Одна интерпретация IO
является то, что это контейнер, который содержит произвольное значение типа a
, Согласно этой интерпретации, она аналогична многомировой интерпретации квантовой механики. IO a
это суперпозиция всех возможных значений типа a
,
В оригинальном мысленном эксперименте Шредингера кот в коробке и жив, и мертв, пока его не заметят. Это два возможных состояния с наложением. Если мы думаем о переменной с именем catIsAlive
, это было бы эквивалентно суперпозиции True
а также False
, Итак, вы можете думать о IO Bool
как набор возможных значений {True, False}
это только рухнет в одно значение при наблюдении.
Точно так же, IO Word8
можно интерпретировать как суперпозицию множества всех возможных Word8
значения, т.е. {0, 1, 2,.. 255}
, IO Int
как суперпозиция всего возможного Int
ценности, IO String
как все возможно String
значения (то есть бесконечное множество) и так далее.
Так как же вы оцениваете ценность?
Вы не извлекаете его, вы работаете в контейнере данных. Вы можете, как показано выше, fmap
а также join
над ним. Таким образом, вы можете написать свое приложение как чистые функции, которые вы затем создаете с нечистыми значениями fmap
, >>=
, join
, и так далее.
Это тривиально, так что это будет длинный ответ. Короче говоря, проблема заключается в подписи, IO a -> a
, это тип, не разрешенный в Haskell. Это действительно меньше связано с IO
быть функтором, чем тот факт, что IO
особенный.
Для некоторых функторов вы можете восстановить чистое значение. Например, частично примененная пара, (,) a
Функтор. Мы разворачиваем значение через snd
,
snd :: (a,b) -> b
snd (_,b) = b
Так что это функтор, который мы можем развернуть до чистого значения, но на самом деле это не имеет никакого отношения к тому, чтобы быть функтором. Это больше связано с парами, принадлежащими к другой категории Теоретической категории, Comonad, с:
extract :: Comonad w => w a -> a
любой Comonad
будет функтором, для которого вы можете восстановить чистую стоимость.
Многие (некомонадные) функторы имеют, скажем, "оценщики", которые допускают что-то вроде того, что спрашивают. Например, мы можем оценить Maybe
с maybe :: a -> Maybe a -> a
, Предоставляя значение по умолчанию, maybe a
имеет желаемый тип, Maybe a -> a
, Еще один полезный пример от государства, evalState :: State s a -> s -> a
имеет свои аргументы в обратном порядке, но концепция та же; учитывая монаду, State s a
и начальное состояние, s
мы разворачиваем чистую стоимость, a
,
Наконец, к специфике IO
, Нет "оценщика" для IO
предоставляется на языке Haskell или в библиотеках. Мы могли бы рассмотреть возможность запуска самой программы в качестве оценщика - во многом в том же духе evalState
, Но если это верный концептуальный ход, то это должно только помочь убедить вас в том, что нет разумного способа развернуть IO
- любая программа написана только IO a
вход в его функцию оценки.
Вместо этого то, что вы вынуждены делать -по замыслу - это работать в IO
монада. Например, если у вас есть чистая функция, f :: a -> b
, вы применяете это в IO
контекст через, fmap f :: IO a -> IO b
TL; DR Вы не можете получить чистую ценность из IO
монада. Применять чистые функции в пределах IO
контекст, например, fmap