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

Почему монады? Как он разрешает побочные эффекты?

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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 определены для каждой монады. Если вы знакомы с полиморфизмом в объектно-ориентированных языках, это по сути одно и то же: один общий интерфейс с несколькими реализациями. Это немного больше ума, чем ваш средний интерфейс OOP, поскольку интерфейс представляет собой политику вычислений, а не, скажем, животное или фигуру или что-то в этом роде.

Хорошо, посмотрим несколько примеров того, как 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 политика - это нечистая функция и т.д.

Ответ 2

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

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

Резюме

Если 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

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

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

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

incrementCounter >> incrementCounter

Каков тип "incrementCounter → incrementCounter"?

  • Это действие, сделанное из двух меньших действий, таких как 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++;     
    }
    
  • он может иметь такие же эффекты, как и его подделки;

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

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

В более общем плане, учитывая два действия:

action1 :: M a
action2 :: M b

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

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

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

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

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

 action1 >> action2 >> action3 >> action4

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

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

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 будет выглядеть так:

    -- 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 для создания значения без какого-либо (побочного) эффекта.

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

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

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

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

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

class Monad m where     
  return :: m a     
  (>>=) :: m a -> (a -> m b) -> m b     
  (>>) :: m a -> m b -> m b  -- can be derives 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 для примера "только подпрограмма диалога",

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

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

MonadRandom

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

 -- 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]

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

Ответ 3

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

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

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

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


Приложение

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

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

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

Есть три основных замечания относительно монады IO:

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

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

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

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

Ответ 6

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

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

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

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

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

Ответ 7

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

Нет. Хаскелл детерминирован. Если вы попросите целое дополнение 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]. Возвращаем x + y. Соберите все возможные результаты.

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

Этот код использует государственную монаду:

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" принимает новое значение состояния, игнорирует текущее состояние и возвращает новое состояние:

   put x _ = ((), x)

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

  a -> b
  b -> c

в

  a -> c

используя (.) оператор, вы можете составить "государственные" трансформаторы

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

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

  a -> s -> (c,s)

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