Почему монады?Как это решает побочные эффекты? - PullRequest
47 голосов
/ 20 октября 2011

Я изучаю Хаскель и пытаюсь понять Монады. У меня два вопроса:

  1. Из того, что я понимаю, Monad - это просто еще один класс типов, который объявляет способы взаимодействия с данными внутри "контейнеров", включая Maybe, List и IO. Кажется разумным и ясным реализовать эти 3 вещи с помощью одной концепции, но на самом деле, дело в том, что может быть чистая обработка ошибок в цепочке функций, контейнеров и побочных эффектов. Это правильная интерпретация?

  2. Как именно решается проблема побочных эффектов? С этой концепцией контейнеров язык по существу говорит, что все внутри контейнеров является недетерминированным (например, ввод / вывод). Поскольку списки и IO являются контейнерами, списки классифицируются с помощью IO, хотя значения внутри списков кажутся мне довольно детерминированными. Так что же является детерминированным и что имеет побочные эффекты? Я не могу понять, что базовое значение является детерминированным, пока вы не поместите его в контейнер (который не является чем-то особенным, чем то же значение с некоторыми другими значениями рядом с ним, например, Nothing), и теперь оно может быть случайным.

Может ли кто-нибудь объяснить, как, интуитивно, Haskell сходит с рук при изменении состояния с помощью входов и выходов? Я не вижу здесь магии.

Ответы [ 7 ]

34 голосов
/ 25 октября 2011

Дело в том, что в цепочке функций, контейнеров и побочных эффектов может быть чистая обработка ошибок. Это правильная интерпретация?

Не совсем. Вы упомянули множество концепций, на которые люди ссылаются, пытаясь объяснить монады, включая побочные эффекты, обработку ошибок и недетерминизм, но кажется, что вы неправильно поняли, что все эти концепции применимы ко всем монадам. Но вы упомянули одну концепцию: цепочка .

Есть два разных варианта этого, поэтому я объясню это двумя разными способами: один без побочных эффектов и один с побочными эффектами.

Нет побочных эффектов:

Возьмите следующий пример:

addM :: (Monad m, Num a) => m a -> m a -> m a
addM ma mb = do
    a <- ma
    b <- mb
    return (a + b)

Эта функция добавляет два числа с тем поворотом, что они заключены в какую-то монаду. Какая монада? Не имеет значения! Во всех случаях этот специальный синтаксис do освобождает от следующего:

addM ma mb =
    ma >>= \a ->
    mb >>= \b ->
    return (a + b)

... или с явным приоритетом оператора:

ma >>= (\a -> mb >>= (\b -> return (a + b)))

Теперь вы действительно можете видеть, что это цепочка маленьких функций, все составленные вместе, и ее поведение будет зависеть от того, как >>= и return определены для каждой монады. Если вы знакомы с полиморфизмом в объектно-ориентированных языках, это, по сути, одно и то же: один общий интерфейс с несколькими реализациями. Это немного более утомительно, чем ваш обычный интерфейс ООП, поскольку интерфейс представляет собой политику вычислений , а не, скажем, животное, фигуру или что-то в этом роде.

Хорошо, давайте посмотрим некоторые примеры того, как addM ведет себя в разных монадах. Монада Identity - хорошее место для старта, поскольку ее определение тривиально:

instance Monad Identity where
    return a = Identity a  -- create an Identity value
    (Identity a) >>= f = f a  -- apply f to a

Так что же происходит, когда мы говорим:

addM (Identity 1) (Identity 2)

Расширяя это, шаг за шагом:

(Identity 1) >>= (\a -> (Identity 2) >>= (\b -> return (a + b)))
(\a -> (Identity 2) >>= (\b -> return (a + b)) 1
(Identity 2) >>= (\b -> return (1 + b))
(\b -> return (1 + b)) 2
return (1 + 2)
Identity 3

Отлично. Теперь, так как вы упомянули чистую обработку ошибок, давайте посмотрим на монаду Maybe. Его определение лишь немного сложнее, чем Identity:

instance Monad Maybe where
    return a = Just a  -- same as Identity monad!
    (Just a) >>= f = f a  -- same as Identity monad again!
    Nothing >>= _ = Nothing  -- the only real difference from Identity

Итак, вы можете себе представить, что если мы скажем addM (Just 1) (Just 2), мы получим Just 3. Но что касается улыбок, давайте расширим addM Nothing (Just 1) вместо:

Nothing >>= (\a -> (Just 1) >>= (\b -> return (a + b)))
Nothing

Или наоборот, addM (Just 1) Nothing:

(Just 1) >>= (\a -> Nothing >>= (\b -> return (a + b)))
(\a -> Nothing >>= (\b -> return (a + b)) 1
Nothing >>= (\b -> return (1 + b))
Nothing

Таким образом, определение >>= монады Maybe было изменено, чтобы учесть неудачу. Когда функция применяется к значению Maybe с использованием >>=, вы получаете то, что ожидаете.

Хорошо, вы упомянули недетерминизм. Да, монаду списка можно рассматривать как моделирование недетерминизма в некотором смысле ... Это немного странно, но думать о списке как о представлении альтернативных возможных значений: [1, 2, 3] - это не коллекция, это отдельный недетерминированный число, которое может быть один, два или три. Это звучит глупо, но это начинает иметь некоторый смысл, когда вы думаете о том, как >>= определено для списков: оно применяет данную функцию к каждому возможному значению. Таким образом, addM [1, 2] [3, 4] фактически рассчитает все возможные суммы этих двух недетерминированных значений: [4, 5, 5, 6].

Хорошо, теперь ответим на ваш второй вопрос ...

Побочные эффекты:

Допустим, вы применяете addM к двум значениям в монаде IO, например:

addM (return 1 :: IO Int) (return 2 :: IO Int)

Вы не получаете ничего особенного, только 3 в монаде IO. addM не читает и не записывает какие-либо изменяемые состояния, так что это неинтересно. То же самое касается State или ST монад. Не весело. Итак, давайте использовать другую функцию:

fireTheMissiles :: IO Int  -- returns the number of casualties

Очевидно, что мир будет отличаться каждый раз, когда запускаются ракеты. Ясно. Теперь предположим, что вы пытаетесь написать какой-то совершенно безобидный, не вызывающий побочных эффектов код, не запускающий ракету. Возможно, вы пытаетесь еще раз добавить два числа, но на этот раз без каких-либо монад, летающих вокруг:

add :: Num a => a -> a -> a
add a b = a + b

и вдруг ваша рука поскользнулась, и вы случайно опечатали:

add a b = a + b + fireTheMissiles

Честная ошибка, правда. Ключи были так близко друг к другу. К счастью, поскольку fireTheMissiles имел тип IO Int, а не просто Int, компилятор может предотвратить катастрофу.

Ладно, полностью надуманный пример, но дело в том, что в случае IO, ST и друзей, система типов сохраняет эффекты изолированными для некоторого определенного контекста. Он волшебным образом не устраняет побочные эффекты, делая код ссылочно прозрачным, чего не должно быть, но он действительно дает понять во время компиляции, какой областью действия ограничены.

Итак, возвращаясь к исходной точке: какое это имеет отношение к сцеплению или составу функций? В данном случае это просто удобный способ выражения последовательности эффектов:

fireTheMissilesTwice :: IO ()
fireTheMissilesTwice = do
    a <- fireTheMissiles
    print a
    b <- fireTheMissiles
    print b

Резюме:

Монада представляет некоторую политику для цепочки вычислений. Политика Identity - это чистая композиция функций, политика Maybe - это композиция функций с распространением ошибок, политика IO - нечистая композиция функций и т. Д.

11 голосов
/ 21 октября 2011

Вы можете увидеть данную монаду m как набор / семейство (или область, домен и т. Д.) Из действий (вспомните оператор C). Монада m определяет вид (побочных) эффектов, которые могут иметь ее действия:

  • с помощью [] вы можете определить действия, которые могут разветвлять их выполнение в разных "независимых параллельных мирах";
  • с помощью Either Foo вы можете определить действия, которые могут завершиться с ошибками типа Foo;
  • с помощью IO вы можете определить действия, которые могут иметь побочные эффекты для «внешнего мира» (доступ к файлам, сети, процессы запуска, выполнение HTTP GET ...);
  • у вас может быть монада с эффектом «случайности» (см. Пакет MonadRandom);
  • Вы можете определить монаду, чьи действия могут сделать ход в игре (скажем, шахматы, Го ...) и получить ход от оппонента, но не могут писать в вашу файловую систему или что-либо еще.

Резюме

Если m - это монада, m a - это действие , которое выдает результат / вывод типа a.

Операторы >> и >>= используются для создания более сложных действий из более простых:

  • a >> b - это макро-действие, которое выполняет действие a, а затем действие b;
  • a >> a выполняет действие a, а затем снова действие a;
  • с >>= второе действие может зависеть от выхода первого.

Точное значение того, что является действием и что выполняет действие, а затем еще одно , зависит от монады: каждая монада определяет императивный подъязык с некоторыми особенностями / эффектами.

Простое секвенирование (>>)

Допустим, у вас есть заданная монада M и некоторые действия incrementCounter, decrementCounter, readCounter:

instance M Monad where ...

-- Modify the counter and do not produce any result:
incrementCounter :: M ()
decrementCounter :: M ()

-- Get the current value of the counter
readCounter :: M Integer

Теперь мы хотели бы сделать что-то интересное с этими действиями. Первое, что мы хотели бы сделать с этими действиями, это упорядочить их. Как и в С скажем, мы хотели бы сделать:

// This is C:
counter++;
counter++;

Мы определяем «оператор секвенирования» >>. Используя этот оператор, мы можем написать:

incrementCounter >> incrementCounter

Какой тип "incrementCounter >> incrementCounter"?

  1. Это действие, выполненное из двух меньших действий, как в C, вы можете написать составные операторы из атомарных операторов:

    // This is a macro statement made of several statements
    {
      counter++;
      counter++;
    }
    
    // and we can use it anywhere we may use a statement:
    if (condition) {
       counter++;
       counter++;     
    }
    
  2. может иметь тот же эффект, что и его подгруппы;

  3. не дает никакого вывода / результата.

Итак, нам бы хотелось, чтобы incrementCounter >> incrementCounter был типа M (): (макро) действие с такими же возможными эффектами, но без вывода.

В целом, с учетом двух действий:

action1 :: M a
action2 :: M b

мы определяем a >> b как макро-действие, которое получается , выполняющим (что бы это ни значило в нашей области действия) a, затем b и выдает в качестве результата результат выполнение второго действия. Тип >>:

(>>) :: M a -> M b -> M b

или более широко:

(>>) :: (Monad m) => m a -> m b -> m b

Мы можем определить большую последовательность действий из более простых:

 action1 >> action2 >> action3 >> action4

Входы и выходы (>>=)

Мы хотели бы иметь возможность увеличивать что-то еще на 1 за раз:

incrementBy 5

Мы хотим внести некоторый вклад в наши действия, для этого мы определяем функцию incrementBy, которая принимает Int и производит действие:

incrementBy :: Int -> M ()

Теперь мы можем написать что-то вроде:

incrementCounter >> readCounter >> incrementBy 5

Но у нас нет способа передать вывод readCounter в incrementBy. Для этого нужна чуть более мощная версия нашего оператора секвенирования. Оператор >>= может передавать выходные данные данного действия в качестве входных данных для следующего действия. Мы можем написать:

readCounter >>= incrementBy

Это действие, которое выполняет действие readCounter, передает свой вывод в функцию incrementBy и затем выполняет результирующее действие.

Тип >>=:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

A (частичный) пример

Допустим, у меня есть монада Prompt, которая может отображать только информацию (текст) для пользователя и запрашивать информацию для пользователя:

-- We don't have access to the internal structure of the Prompt monad
module Prompt (Prompt(), echo, prompt) where

-- Opaque
data Prompt a = ...
instance Monad Prompt where ...

-- Display a line to the CLI:
echo :: String -> Prompt ()

-- Ask a question to the user:
prompt :: String -> Prompt String

Давайте попробуем определить promptBoolean message действия, которые задают вопрос и выдают логическое значение.

Мы используем действие (message ++ "[y/n]") приглашения и передаем его вывод в функцию f:

  • f "y" должно быть действием, которое ничего не делает, но производит True в качестве вывода;

  • f "n" должно быть действием, которое ничего не делаетвыдайте False как вывод;

  • все остальное должно перезапустить действие (повторите действие);

promptBoolean будет выглядеть такthis:

    -- Incomplete version, some bits are missing:
    promptBoolean :: String -> M Boolean
    promptBoolean message = prompt (message ++ "[y/n]") >>= f
      where f result = if result == "y"
                       then ???? -- We need here an action which does nothing but produce `True` as output
                       else if result=="n"
                            then ???? -- We need here an action which does nothing but produce `False` as output
                            else echo "Input not recognised, try again." >> promptBoolean

Создание значения без эффекта (return)

Чтобы заполнить недостающие биты в нашей функции promptBoolean, нам нужен способ представления фиктивных действий безлюбой побочный эффект, но который выдает только заданное значение:

-- "return 5" is an action which does nothing but outputs 5
return :: (Monad m) => a -> m a

, и теперь мы можем выписать promptBoolean функцию:

promptBoolean :: String -> Prompt Boolean
promptBoolean message :: prompt (message ++ "[y/n]") >>= f
  where f result = if result=="y"
                   then return True
                     else if result=="n"
                     then return False
                     else echo "Input not recognised, try again." >> promptBoolean message

, составив эти два простых действия (promptBoolean, echo) мы можем определить любой вид диалога между пользователем и вашей программой (действия программы сдерживаютсяминистическая, так как наша монада не имеет «эффекта случайности»).

promptInt :: String -> M Int
promptInt = ... -- similar

-- Classic "guess a number game/dialogue"
guess :: Int -> m()
guess n = promptInt "Guess:" m -> f
   where f m = if m == n
               then echo "Found"
               else (if m > n
                     then echo "Too big"
                     then echo "Too small") >> guess n       

Операции монады

Монада - это набор действий, которые можно составить с помощью returnи >>= операторы:

  • >>= для композиции действия;

  • return для получения значения без каких-либо (side-)эффект.

Эти два оператора являются минимальными операторами, необходимыми для определения Monad.

В Haskell оператор >> также необходим, но можетфактически получается из >>=:

(>>): Monad m => m a -> m b -> m b
a >> b = a >>= f
 where f x = b

В Haskell также необходим дополнительный оператор fail, но это действительно взлом (и он может быть удален из Monadв будущем ).

Это определение Haskell для Monad:

class Monad m where     
  return :: m a     
  (>>=) :: m a -> (a -> m b) -> m b     
  (>>) :: m a -> m b -> m b  -- can be derived from (>>=)
  fail :: String -> m a      -- mostly a hack

Первоклассные действия

Одна замечательная вещь о монадахявляется то, что действия являются первоклассными.Вы можете взять их в переменную, вы можете определить функцию, которая выполняет действия в качестве входных данных и производит некоторые другие действия в качестве выходных данных.Например, мы можем определить оператор while:

-- while x y : does action y while action x output True
while :: (Monad m) => m Boolean -> m a -> m ()
while x y = x >>= f
  where f True = y >> while x y
        f False = return ()

Сводка

A Monad - это набор действий в некотором домене.Монада / домен определяют виды возможных эффектов.Операторы >> и >>= представляют последовательность действий, и монадическое выражение может использоваться для представления любого типа «императивной (под) программы» в вашей (функциональной) программе на Haskell.

Замечательно то, что:

  • вы можете создать свой собственный Monad, который поддерживает нужные вам функции и эффекты

    • см. Prompt для примераподпрограмма "только диалог",

    • см. пример "подпрограмма только для выборки"

  • вы можете написать свои собственные управляющие структуры (while, throw, catch или более экзотические) как функции, выполняющие действия и сочетающие их каким-либо образом для создания более крупных макро-действий.

MonadRandom

Хорошим способом понимания монад является пакет MonadRandom.Монада Rand состоит из действий, выход которых может быть случайным (эффект - случайность). action в этой монаде является некоторой случайной величиной (или, точнее, процессом выборки):

 -- Sample an Int from some distribution
 action :: Rand Int

Использование Rand для выполнения некоторых алгоритмов выборки / случайной выборки довольно интересно, потому чтоу вас есть случайные величины в качестве значений первого класса:

-- Estimate mean by sampling nsamples times the random variable x
sampleMean :: Real a => Int -> m a -> m a
sampleMean n x = ...

В этом параметре функция sequence из Prelude,

 sequence :: Monad m => [m a] -> m [a]

становится

 sequence :: [Rand a] -> Rand [a]

Создает случайную величину, полученную путем выборки независимо от списка случайных величин.

10 голосов
/ 21 октября 2011

Позвольте мне начать с указания на превосходную статью " Вы могли бы изобрести монады ". Это показывает, как структура Monad может естественным образом проявляться во время написания программ. Но в учебнике не упоминается IO, поэтому здесь я попробую расширить подход.

Давайте начнем с того, что вы, вероятно, уже видели - контейнерной монады. Допустим, у нас есть:

f, g :: Int -> [Int]

Одним из способов рассмотрения этого является то, что он дает нам несколько возможных выходных данных для каждого возможного ввода. Что, если мы хотим получить все возможные выходные данные для композиции обеих функций? Предоставление всех возможностей, которые мы могли бы получить, применяя функции одну за другой?

Ну, для этого есть функция:

fg x = concatMap g $ f x

Если мы сформулируем это более широко, мы получим

fg x     = f x >>= g
xs >>= f = concatMap f xs
return x = [x]

Почему мы хотим обернуть это так? Что ж, написание наших программ, в основном с использованием >>= и return, дает нам несколько приятных свойств - например, мы можем быть уверены, что «забыть» о решениях относительно сложно. Мы явно должны были бы вновь ввести его, скажем, добавив еще одну функцию skip. И теперь у нас есть монада, и мы можем использовать все комбинаторы из библиотеки монад!

Теперь давайте перейдем к вашему более хитрому примеру. Скажем, две функции «побочные». Это не недетерминировано, это просто означает, что в теории весь мир является как их входом (как он может влиять на них), так и их выходом (так как функция может влиять на него). Итак, мы получаем что-то вроде:

f, g :: Int -> RealWorld# -> (Int, RealWorld#)

Если мы теперь хотим, чтобы f получил мир, который g остался позади, мы бы написали:

fg x rw = let (y, rw')  = f x rw
              (r, rw'') = g y rw'
           in (r, rw'')

или обобщенно:

fg x     = f x >>= g
x >>= f  = \rw -> let (y, rw')  = x   rw
                      (r, rw'') = f y rw'
                   in (r, rw'')
return x = \rw -> (x, rw)

Теперь, если пользователь может использовать только >>=, return и несколько предопределенных значений IO, мы снова получим хорошее свойство: пользователь на самом деле не увидит RealWorld# проходя мимо! И это очень хорошая вещь, так как вас не очень интересуют подробности, откуда getLine получает свои данные. И снова мы получаем все хорошие высокоуровневые функции из библиотек монад.

Итак, важные вещи, которые нужно забрать:

  1. Монада фиксирует общие шаблоны в вашем коде, такие как «всегда передавать все элементы контейнера A в контейнер B» или «пропускать этот реальный тег через». Часто, когда вы понимаете, что в вашей программе есть монада, сложные вещи становятся просто приложениями правильного комбинатора монад.

  2. Монада позволяет вам полностью скрыть реализацию от пользователя. Это превосходный механизм инкапсуляции, будь то для вашего внутреннего состояния или для того, как IO удается втиснуть нечистоту в чистую программу относительно безопасным способом.


Приложение

В случае, если кто-то все еще царапает себе голову по RealWorld# так же сильно, как я это делал, когда начинал: очевидно, что происходит больше магии после , когда вся абстракция монады удалена. Тогда компилятор будет использовать тот факт, что может быть только один «реальный мир». Это хорошие новости и плохие новости:

  1. Из этого следует, что компилятор должен гарантировать порядок выполнения между функциями (что и было после!)

  2. Но это также означает, что на самом деле прохождение реального мира не является необходимым, поскольку есть только один, который мы могли бы иметь в виду: тот, который является текущим, когда функция выполняется!

Суть в том, что после исправления порядка выполнения RealWorld# просто оптимизируется. Таким образом, программы, использующие монаду IO, фактически не имеют времени выполнения. Также обратите внимание, что использование RealWorld# - это, очевидно, только один возможный способ поставить IO - но это тот, который GHC использует внутри. Хорошая вещь о монадах в том, что, опять же, пользователю действительно не нужно знать.

4 голосов
/ 21 октября 2011

Существует три основных замечания относительно монады ввода-вывода:

1) Вы не можете получить значения из нее.Другие типы, такие как Maybe, могут позволять извлекать значения, но ни сам интерфейс класса монад, ни тип данных IO не позволяют его.

2) "Inside" IO - это не только реальное значение,также это "RealWorld" вещь.Это фиктивное значение используется для принудительного создания цепочки действий системой типов : если у вас есть два независимых вычисления, использование >>= делает второе вычисление зависимым от первого.

3) Предположим недетерминированную вещь, такую ​​как random :: () -> Int, которая не разрешена в Haskell.Если вы измените подпись на random :: Blubb -> (Blubb, Int), она будет разрешена, если вы убедитесь, что никто никогда не сможет использовать Blubb дважды: потому что в этом случае все входные данные «разные», это не проблемачто выходы также различны.

Теперь мы можем использовать факт 1): Никто не может получить что-то из IO, поэтому мы можем использовать RealWord манекен, скрытый в IO, чтобы служитьBlubb.Во всем приложении есть только один IO (тот, который мы получаем из main), и он заботится о правильной последовательности, как мы видели в 2).Задача решена.

4 голосов
/ 21 октября 2011

Одна вещь, которая часто помогает мне понять природу чего-либо, - исследовать это самым тривиальным способом. Таким образом, я не отвлекаюсь на потенциально не связанные понятия. Имея это в виду, я думаю, что было бы полезно понять природу Identity Monad , так как это наиболее тривиальная реализация монады из возможных (я думаю).

Что интересного в Монаде Идентичности? Я думаю, что это позволяет мне выразить идею оценки выражений в контексте, определенном другими выражениями. И для меня это сущность каждой монады, с которой я сталкивался (пока).

Если вы уже много изучали «основные» языки программирования до того, как начали изучать Haskell (как я это сделал), то это не кажется очень интересным. В конце концов, в основном языке программирования операторы выполняются последовательно, один за другим (конечно, за исключением конструкций потока управления). И, естественно, мы можем предположить, что каждый оператор оценивается в контексте всех ранее выполненных операторов и что эти ранее выполненные операторы могут изменять среду и поведение выполняемого в данный момент оператора.

Все это в значительной степени чуждая концепция в функциональном, ленивом языке, таком как Haskell. Порядок, в котором вычисления вычисляются в Хаскеле, четко определен, но иногда его трудно предсказать и еще сложнее контролировать. И для многих видов проблем это просто отлично. Но другие виды проблем (например, IO) трудно решить без какого-либо удобного способа установить неявный порядок и контекст между вычислениями в вашей программе.

Что касается побочных эффектов, в частности, часто они могут быть преобразованы (через монаду) в простую передачу состояний, что совершенно законно на чистом функциональном языке. Однако, некоторые монады не имеют такой природы. Монады, такие как IO Monad или ST monad, буквально выполняют побочные действия. Есть много способов думать об этом, но я думаю об этом только потому, что только потому, что мои вычисления должны существовать в мире без побочных эффектов, Монада может и не быть. Таким образом, Monad может свободно устанавливать контекст для выполнения моих вычислений, основанный на побочных эффектах, определенных другими вычислениями.

Наконец, я должен отрицать, что я определенно не эксперт по Haskell. Поэтому, пожалуйста, поймите, что все, что я сказал, в значительной степени мои собственные мысли на эту тему, и я вполне могу отречься от них позже, когда пойму монады более полно.

2 голосов
/ 21 октября 2011

С этой концепцией контейнеров язык, по сути, говорит, что все внутри контейнеров недетерминировано

Нет.Хаскель детерминирован.Если вы попросите сложение целых чисел 2 + 2, вы всегда получите 4.

«Недетерминированный» - это всего лишь метафора, способ мышления.Под капотом все детерминировано.Если у вас есть этот код:

do x <- [4,5]
   y <- [0,1]
   return (x+y)

он примерно эквивалентен коду Python

 l = []
 for x in [4,5]:
     for y in [0,1]:
         l.append(x+y)

Вы видите здесь недетерминизм?Нет, это детерминистическая конструкция списка.Запустите его дважды, и вы получите одинаковые числа в одинаковом порядке.

Вы можете описать это так: выберите произвольный x из [4,5].Выберите произвольный y из [0,1].Вернуть х + у.Соберите все возможные результаты.

Кажется, что этот путь связан с недетерминизмом, но это всего лишь вложенный цикл (понимание списка).Здесь нет «настоящего» недетерминизма, он моделируется проверкой всех возможностей.Недетерминизм - это иллюзия.Код только кажется недетерминированным.

Этот код, использующий монаду состояния:

do put 0
   x <- get
   put (x+2)
   y <- get
   return (y+3)

, дает 5 и, по-видимому, включает изменение состояния.Как со списками, это иллюзия.Нет никаких «переменных», которые меняются (как в императивных языках).Под капотом все не меняется.

Вы можете описать код так: положите 0 в переменную.Считайте значение переменной в x.Поместите (x + 2) в переменную.Считайте переменную в y и верните y + 3.

Кажется, что этот путь включает состояние, но это только составные функции, передающие дополнительный параметр.Здесь нет «настоящей» изменчивости, это моделируется композицией.Изменчивость - это иллюзия.Код, похоже, только использует его.

Haskell делает это так: у вас есть функции

   a -> s -> (b,s)

Эта функция принимает и старое значение состояния и возвращает новое значение.Это не включает изменчивость или изменение переменных.Это функция в математическом смысле.

Например, функция «положить» принимает новое значение состояния, игнорирует текущее состояние и возвращает новое состояние:

   put x _ = ((), x)

Так же, как вы можете составить два нормальныхфункции

  a -> b
  b -> c

в

  a -> c

с помощью оператора (.) вы можете объединить «государственные» трансформаторы

  a -> s -> (b,s)
  b -> s -> (c,s)

в одну функцию

  a -> s -> (c,s)

Попробуйте написать оператор композиции самостоятельно.Это то, что действительно происходит, «побочных эффектов» нет, только передача аргументов функциям.

2 голосов
/ 20 октября 2011

Дело в том, что в цепочке функций, контейнеров и побочных эффектов может быть чистая обработка ошибок

Больше или меньше.

какточно ли решена проблема побочных эффектов?

Значение в монаде ввода / вывода, т. е. типа IO a, следует интерпретировать как программу.Значения p >> q on IO могут быть интерпретированы как оператор, который объединяет две программы в одну, которая сначала выполняет p, затем q.Другие операторы монады имеют аналогичные интерпретации.Присваивая программе имя main, вы объявляете компилятору, что это программа, которая должна выполняться ее кодом выходного объекта.

Что касается монады списка, она на самом деле не связана сМонада ввода / вывода, кроме как в очень абстрактном математическом смысле.Монада IO дает детерминированные вычисления с побочными эффектами, в то время как монада списка дает недетерминированный (но не случайный!) Обратный поиск, чем-то похожий на способ действия Пролога.

...