为什么是单子?它如何解决副作用?

发布于 2024-12-11 02:33:12 字数 491 浏览 2 评论 0原文

我正在学习 Haskell 并试图理解 Monad。我有两个问题:

  1. 据我了解,Monad 只是另一个类型类,它声明了与“容器”内的数据交互的方法,包括 MaybeList 和IO。用一个概念来实现这三件事似乎很聪明和干净,但实际上,重点是可以在一系列函数、容器和副作用中进行干净的错误处理。这是正确的解释吗?

  2. 副作用问题到底是如何解决的?通过容器的概念,该语言本质上表示容器内的任何内容都是不确定的(例如 i/o)。因为列表和 IO 都是容器,所以列表与 IO 是等价类的,尽管列表内的值对我来说似乎相当确定。那么什么是确定性的,什么有副作用呢?我无法理解基本值是确定性的这一想法,直到将其放入容器中(这与旁边有其他一些值的相同值没有什么特别,例如 Nothing ),现在可以是随机的。

有人能直观地解释一下 Haskell 如何通过输入和输出改变状态吗?我在这里没有看到魔法。

I am learning Haskell and trying to understand Monads. I have two questions:

  1. From what I understand, Monad is just another typeclass that declares ways to interact with data inside "containers", including Maybe, List, and IO. It seems clever and clean to implement these 3 things with one concept, but really, the point is so there can be clean error handling in a chain of functions, containers, and side effects. Is this a correct interpretation?

  2. How exactly is the problem of side-effects solved? With this concept of containers, the language essentially says anything inside the containers is non-deterministic (such as i/o). Because lists and IOs are both containers, lists are equivalence-classed with IO, even though values inside lists seem pretty deterministic to me. So what is deterministic and what has side-effects? I can't wrap my head around the idea that a basic value is deterministic, until you stick it in a container (which is no special than the same value with some other values next to it, e.g. Nothing) and it can now be random.

Can someone explain how, intuitively, Haskell gets away with changing state with inputs and output? I'm not seeing the magic here.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

夜司空 2024-12-18 02:33:12

关键是可以在函数、容器和副作用链中进行干净的错误处理。这是正确的解释吗?

并不真地。你提到了人们在试图解释 monad 时引用的很多概念,包括副作用、错误处理和非确定性,但听起来你有一种错误的感觉,即所有这些概念都适用于所有 monad。但您提到的一个概念确实有效:链接

它有两种不同的风格,所以我将以两种不同的方式解释它:一种没有副作用,一种有副作用。

无副作用:

采用以下示例:

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

此函数将两个数字相加,但它们被包裹在某个 monad 中。哪个单子?没关系!在所有情况下,特殊的 do 语法将糖化为以下内容:

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

... 或者,明确显示运算符优先级:

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

现在您可以真正看到这是一个由小函数组成的链,所有函数都组合在一起,其行为取决于如何为每个 monad 定义 >>=return。如果您熟悉面向对象语言中的多态性,那么这本质上是同一件事:具有多个实现的一个公共接口。它比一般的 OOP 界面更令人费解,因为该界面代表计算策略,而不是动物或形状之类的东西。

好的,让我们看看 addM 在不同 monad 中的行为方式的一些示例。 Identity monad 是一个不错的起点,因为它的定义很简单:

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 monad。它的定义仅比 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

所以 也许 monad 的 >>= 定义经过调整以解决失败问题。当使用 >>= 将函数应用于 Maybe 值时,您会得到您所期望的结果。

好吧,你提到了非决定论。是的,列表 monad 在某种意义上可以被认为是对非确定性建模......这有点奇怪,但是可以将列表视为代表替代的可能值:[1, 2, 3]不是一个集合,它是一个不确定的数字,可以是一、二或三。这听起来很愚蠢,但是当您考虑如何为列表定义 >>= 时,它就开始有意义了:它将给定的函数应用于每个可能的值。因此,addM [1, 2] [3, 4]实际上是要计算这两个非确定性值的所有可能的和:[4, 5, 5, 6] >。

好的,现在解决你的第二个问题...

副作用:

假设你将 addM 应用于 IO monad 中的两个值,例如:

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

你没有得到任何特殊的东西,IO monad 中只有 3 个。 addM 不会读取或写入任何可变状态,因此它没什么乐趣。对于 StateST 单子也是如此。没有乐趣。因此,让我们使用不同的函数:

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

一个诚实的错误,真的。钥匙靠得很近。幸运的是,由于 fireTheMissilesIO Int 类型而不是简单的 Int 类型,编译器能够避免灾难。

好吧,完全是人为的例子,但重点是,在 IO、ST 和类似的情况下,类型系统将效果隔离到某些特定的上下文。它并没有神奇地消除副作用,使代码具有不应该的引用透明性,但它确实在编译时明确了效果所限制的范围。

那么回到最初的观点:这与函数的链接或组合有什么关系?好吧,在这种情况下,它只是表达一系列效果的便捷方法:

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

摘要:

monad 代表了一些链接计算的策略。 Identity 的策略是纯函数组合,Maybe 的策略是带有失败传播的函数组合,IO 的策略是不纯的。 函数组合等等。

The point is so there can be clean error handling in a chain of functions, containers, and side effects. Is this a correct interpretation?

Not really. You've mentioned a lot of concepts that people cite when trying to explain monads, including side effects, error handling and non-determinism, but it sounds like you've gotten the incorrect sense that all of these concepts apply to all monads. But there's one concept you mentioned that does: chaining.

There are two different flavors of this, so I'll explain it two different ways: one without side effects, and one with side effects.

No Side Effects:

Take the following example:

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

This function adds two numbers, with the twist that they are wrapped in some monad. Which monad? Doesn't matter! In all cases, that special do syntax de-sugars to the following:

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

... or, with operator precedence made explicit:

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

Now you can really see that this is a chain of little functions, all composed together, and its behavior will depend on how >>= and return are defined for each monad. If you're familiar with polymorphism in object-oriented languages, this is essentially the same thing: one common interface with multiple implementations. It's slightly more mind-bending than your average OOP interface, since the interface represents a computation policy rather than, say, an animal or a shape or something.

Okay, let's see some examples of how addM behaves across different monads. The Identity monad is a decent place to start, since its definition is trivial:

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

So what happens when we say:

addM (Identity 1) (Identity 2)

Expanding this, step by step:

(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

Great. Now, since you mentioned clean error handling, let's look at the Maybe monad. Its definition is only slightly trickier than 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

So you can imagine that if we say addM (Just 1) (Just 2) we'll get Just 3. But for grins, let's expand addM Nothing (Just 1) instead:

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

Or the other way around, 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

So the Maybe monad's definition of >>= was tweaked to account for failure. When a function is applied to a Maybe value using >>=, you get what you'd expect.

Okay, so you mentioned non-determinism. Yes, the list monad can be thought of as modeling non-determinism in a sense... It's a little weird, but think of the list as representing alternative possible values: [1, 2, 3] is not a collection, it's a single non-deterministic number that could be either one, two or three. That sounds dumb, but it starts to make some sense when you think about how >>= is defined for lists: it applies the given function to each possible value. So addM [1, 2] [3, 4] is actually going to compute all possible sums of those two non-deterministic values: [4, 5, 5, 6].

Okay, now to address your second question...

Side Effects:

Let's say you apply addM to two values in the IO monad, like:

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

You don't get anything special, just 3 in the IO monad. addM does not read or write any mutable state, so it's kind of no fun. Same goes for the State or ST monads. No fun. So let's use a different function:

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

Clearly the world will be different each time missiles are fired. Clearly. Now let's say you're trying to write some totally innocuous, side effect free, non-missile-firing code. Perhaps you're trying once again to add two numbers, but this time without any monads flying around:

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

and all of a sudden your hand slips, and you accidentally typo:

add a b = a + b + fireTheMissiles

An honest mistake, really. The keys were so close together. Fortunately, because fireTheMissiles was of type IO Int rather than simply Int, the compiler is able to avert disaster.

Okay, totally contrived example, but the point is that in the case of IO, ST and friends, the type system keeps effects isolated to some specific context. It doesn't magically eliminate side effects, making code referentially transparent that shouldn't be, but it does make it clear at compile time what scope the effects are limited to.

So getting back to the original point: what does this have to do with chaining or composition of functions? Well, in this case, it's just a handy way of expressing a sequence of effects:

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

Summary:

A monad represents some policy for chaining computations. Identity's policy is pure function composition, Maybe's policy is function composition with failure propogation, IO's policy is impure function composition and so on.

夏见 2024-12-18 02:33:12

您可以将给定的 monad m 视为操作的集合/系列(或领域、域等)(想想 C 语句)。 monad m 定义了其操作可能产生的(副作用)类型:

  • 使用 [] 您可以定义可以在不同的“独立并行世界”中分叉执行的操作”;
  • 使用Either Foo,您可以定义可能因Foo类型错误而失败的操作;
  • 使用 IO,您可以定义可能对“外部世界”产生副作用的操作(访问文件、网络、启动进程、执行 HTTP GET ...);
  • 你可以有一个 monad,其效果是“随机性”(参见包 MonadRandom);
  • 你可以定义一个 monad,它的动作可以在游戏中移动(比如国际象棋、围棋……)并接收对手的移动,但无法写入你的文件系统或其他任何东西。

总结

如果m是一个monad,那么m a就是一个动作,它产生a类型的结果/输出。

>>>>= 运算符用于从简单的操作中创建更复杂的操作:

  • a >> b 是一个宏操作,它执行操作 a,然后执行操作 b
  • a>> a 执行操作 a,然后再次执行操作 a
  • 使用 >>= 第二个操作可以依赖于第一个操作的输出。

动作的确切含义以及执行一个动作然后执行另一个动作的确切含义取决于 monad:每个 monad 都定义了一种具有某些功能/效果的命令式子语言。

简单排序 (>>)

假设有一个给定的 monad M 和一些 actions incrementCounter,< code>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”的类型是什么?

  1. 它是由两个较小的操作组成的操作,就像在 C 中一样,您可以从原子语句编写组合语句:

    //这是由多条语句组成的宏语句
    {
      计数器++;
      计数器++;
    }
    
    // 我们可以在任何可以使用语句的地方使用它:
    如果(条件){
       计数器++;
       计数器++;     
    }
    
  2. 可以与其子操作具有相同类型的效果;

  3. 它不产生任何输出/结果。

所以我们想要 incrementCounter >> incrementCounterM () 类型:具有相同类型可能效果但没有任何输出的(宏)操作。

更一般地,给定两个动作:

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

我们希望在操作中提供一些输入,为了做到这一点,我们定义了一个采用 IntincrementBy 的函数code> 并产生一个动作:

incrementBy :: Int -> M ()

现在我们可以写东西了就像:

incrementCounter >> readCounter >> incrementBy 5

但是我们无法将 readCounter 的输出输入到 incrementBy 中。为了做到这一点,我们需要一个稍微更强大的测序算子版本。 >>= 运算符可以将给定操作的输出作为下一个操作的输入。我们可以这样写:

readCounter >>= incrementBy

它是一个执行 readCounter 操作的操作,将其输出提供给incrementBy 函数,然后执行结果操作。

>>= 的类型是:

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

一个(部分)示例

假设我有一个 Prompt monad,它只能向用户显示信息(文本)并询问信息对用户:

-- 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) 我们可以定义用户和程序之间的任何类型的对话(程序的操作是确定性的,因为我们的 monad 不具有“随机性效应” ”)。

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       

Monad 的操作

Monad 是一组可以由 return>>= 运算符组成的操作:

  • >> ;= 用于动作组合;

  • return 用于生成没有任何(副作用)效果的值。

这两个运算符是定义 Monad 所需的最少运算符。

在 Haskell 中,还需要 >> 运算符,但它实际上可以从 >>= 派生:

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

在 Haskell 中,额外的 fail 运算符也是需要的,但这确实是一个黑客(并且 它可能将来会从 Monad 中删除)。

这是 Monad 的 Haskell 定义:

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

动作是一流的

monad 的一个伟大之处在于动作是一流的。您可以将它们放入变量中,您可以定义将操作作为输入并产生一些其他操作作为输出的函数。例如,我们可以定义一个 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 ()

摘要

Monad 是某个域中的一组动作。单子/域定义了可能的“效果”类型。 >>>>= 运算符表示操作的顺序,并且一元表达式可用于表示您的程序中的任何类型的“命令(子)程序” (函数式)Haskell 程序。

伟大的事情是:

  • 您可以设计自己的Monad,它支持您想要的功能和效果

    • 请参阅提示以获取“仅对话子程序”的示例,

    • 参见Rand“仅采样子程序”的示例;

  • 您可以编写自己的控制结构(whilethrowcatch 或更奇特的结构)作为执行操作并将其组合的函数某种方式来产生更大的宏动作。

MonadRandom

理解 monad 的一个好方法是 MonadRandom 包。 Rand monad 由输出可以是随机的动作组成(效果是随机的)。这个 monad 中的一个动作是某种随机变量(或者更准确地说是一个采样过程):

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

使用Rand来做一些采样/随机算法是非常有趣的,因为你有< em>随机变量作为第一类值:

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

在此设置中,Prelude 中的 sequence 函数

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

变为

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

它创建一个通过独立于列表进行采样而获得的随机变量随机变量。

You could see a given monad m as a set/family (or realm, domain, etc.) of actions (think of a C statement). The monad m defines the kind of (side-)effects that its actions may have:

  • with [] you can define actions which can fork their executions in different "independent parallel worlds";
  • with Either Foo you can define actions which can fail with errors of type Foo;
  • with IO you can define actions which can have side-effects on the "outside world" (access files, network, launch processes, do a HTTP GET ...);
  • you can have a monad whose effect is "randomness" (see package MonadRandom);
  • you can define a monad whose actions can make a move in a game (say chess, Go…) and receive move from an opponent but are not able to write to your filesystem or anything else.

Summary

If m is a monad, m a is an action which produces a result/output of type a.

The >> and >>= operators are used to create more complex actions out of simpler ones:

  • a >> b is a macro-action which does action a and then action b;
  • a >> a does action a and then action a again;
  • with >>= the second action can depend on the output of the first one.

The exact meaning of what an action is and what doing an action and then another one is depends on the monad: each monad defines an imperative sublanguage with some features/effects.

Simple sequencing (>>)

Let's say with have a given monad M and some actions 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

Now we would like to do something interesting with those actions. The first thing we would like to do with those actions is to sequence them. As in say C, we would like to be able to do:

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

We define an "sequencing operator" >>. Using this operator we can write:

incrementCounter >> incrementCounter

What is the type of "incrementCounter >> incrementCounter"?

  1. It is an action made of two smaller actions like in C you can write composed-statements from atomic statements :

    // 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. it can have the same kind of effects as its subactions;

  3. it does not produce any output/result.

So we would like incrementCounter >> incrementCounter to be of type M (): an (macro-)action with the same kind of possible effects but without any output.

More generally, given two actions:

action1 :: M a
action2 :: M b

we define a a >> b as the macro-action which is obtained by doing (whatever that means in our domain of action) a then b and produces as output the result of the execution of the second action. The type of >> is:

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

or more generally:

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

We can define bigger sequence of actions from simpler ones:

 action1 >> action2 >> action3 >> action4

Input and outputs (>>=)

We would like to be able to increment by something else that 1 at a time:

incrementBy 5

We want to provide some input in our actions, in order to do this we define a function incrementBy taking an Int and producing an action:

incrementBy :: Int -> M ()

Now we can write things like:

incrementCounter >> readCounter >> incrementBy 5

But we have no way to feed the output of readCounter into incrementBy. In order to do this, a slightly more powerful version of our sequencing operator is needed. The >>= operator can feed the output of a given action as input to the next action. We can write:

readCounter >>= incrementBy

It is an action which executes the readCounter action, feeds its output in the incrementBy function and then execute the resulting action.

The type of >>= is:

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

A (partial) example

Let's say I have a Prompt monad which can only display informations (text) to the user and ask informations to the user:

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

Let's try to define a promptBoolean message actions which asks for a question and produces a boolean value.

We use the prompt (message ++ "[y/n]") action and feed its output to a function f:

  • f "y" should be an action which does nothing but produce True as output;

  • f "n" should be an action which does nothing but produce False as output;

  • anything else should restart the action (do the action again);

promptBoolean would look like 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

Producing a value without effect (return)

In order to fill the missing bits in our promptBoolean function, we need a way to represent dummy actions without any side effect but which only outputs a given value:

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

and we can now write out promptBoolean function:

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

By composing those two simple actions (promptBoolean, echo) we can define any kind of dialogue between the user and your program (the actions of the program are deterministic as our monad does not have a "randomness effect").

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       

The operations of a monad

A Monad is a set of actions which can be composed with the return and >>= operators:

  • >>= for action composition;

  • return for producing a value without any (side-)effect.

These two operators are the minimal operators needed to define a Monad.

In Haskell, the >> operator is needed as well but it can in fact be derived from >>=:

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

In Haskell, an extra fail operator is need as well but this is really a hack (and it might be removed from Monad in the future).

This is the Haskell definition of a 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

Actions are first-class

One great thing about monads is that actions are first-class. You can take them in a variable, you can define function which take actions as input and produce some other actions as output. For example, we can define a while operator:

-- 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 ()

Summary

A Monad is a set of actions in some domain. The monad/domain define the kind of "effects" which are possible. The >> and >>= operators represent sequencing of actions and monadic expression may be used to represent any kind of "imperative (sub)program" in your (functional) Haskell program.

The great things are that:

  • you can design your own Monad which supports the features and effects that you want

    • see Prompt for an example of a "dialogue only subprogram",

    • see Rand for an example of "sampling only subprogram";

  • you can write your own control structures (while, throw, catch or more exotic ones) as functions taking actions and composing them in some way to produce a bigger macro-actions.

MonadRandom

A good way of understanding monads, is the MonadRandom package. The Rand monad is made of actions whose output can be random (the effect is randomness). An action in this monad is some kind of random variable (or more exactly a sampling process):

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

Using Rand to do some sampling/random algorithms is quite interesting because you have random variables as first class values:

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

In this setting, the sequence function from Prelude,

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

becomes

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

It creates a random variable obtained by sampling independently from a list of random variables.

记忆之渊 2024-12-18 02:33:12

让我首先指出优秀的“你可以发明 monad”文章。它说明了 Monad 结构如何在您编写程序时自然地体现出来。但本教程没有提及 IO,因此我将尝试扩展该方法。

让我们从您可能已经看到的内容开始——容器 monad。假设我们有:

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。而且我们现在有了一个 monad,可以使用 monad 库中的所有组合器!

现在,让我们跳到你的更棘手的例子。假设这两个函数是“副作用”的。这不是非确定性的,它只是意味着理论上整个世界既是它们的输入(因为它可以影响它们)也是它们的输出(因为函数可以影响它)。所以我们得到这样的结果:

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)

现在如果用户只能使用 < code>>>=、return 和一些预定义的 IO 值,我们再次获得一个不错的属性:用户实际上永远不会看到RealWorld#通过了 大约!这是一件非常好的事情,因为您对 getLine 从哪里获取数据的详细信息并不真正感兴趣。 再次我们从 monad 库中获得了所有不错的高级函数。

因此,需要注意的重要事项是:

  1. monad 捕获代码中的常见模式,例如“始终将容器 A 的所有元素传递到容器 B”或“传递此真实世界标签”。通常,一旦您意识到程序中有一个 monad,复杂的事情就变成了正确的 monad 组合器的简单应用。

  2. monad 允许您完全向用户隐藏实现。它是一种出色的封装机制,无论是对于您自己的内部状态还是对于 IO 如何以相对安全的方式将非纯粹性压缩到纯程序中。


附录

万一有人仍然像我开始时一样对RealWorld#摸不着头脑:毕竟,显然还有更多的魔力在发生。单子抽象已被删除。然后编译器将利用只能有一个“真实世界”这一事实。这是好消息也是坏消息:

  1. 因此,编译器必须保证函数之间的执行顺序(这就是我们所追求的!)

  2. 但这也意味着实际上传递现实世界是不必要的,因为我们可能意味着只有一个:当前的那个当函数得到但这

最重要的是,一旦执行顺序固定,RealWorld# 就会得到优化。因此,使用 IO monad 的程序实际上具有零运行时开销。另请注意,使用 RealWorld# 显然只是放置 IO一种可能方式 - 但它恰好是 GHC 内部使用的一种方式。 monad 的好处是,用户真的不需要知道。

Let me start by pointing at the excellent "You could have invented monads" article. It illustrates how the Monad structure can naturally manifest while you are writing programs. But the tutorial doesn't mention IO, so I will have a stab here at extending the approach.

Let us start with what you probably have already seen - the container monad. Let's say we have:

f, g :: Int -> [Int]

One way of looking at this is that it gives us a number of possible outputs for every possible input. What if we want all possible outputs for the composition of both functions? Giving all possibilities we could get by applying the functions one after the other?

Well, there's a function for that:

fg x = concatMap g $ f x

If we put this more general, we get

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

Why would we want to wrap it like this? Well, writing our programs primarily using >>= and return gives us some nice properties - for example, we can be sure that it's relatively hard to "forget" solutions. We'd explicitly have to reintroduce it, say by adding another function skip. And also we now have a monad and can use all combinators from the monad library!

Now, let us jump to your trickier example. Let's say the two functions are "side-effecting". That's not non-deterministic, it just means that in theory the whole world is both their input (as it can influence them) as well as their output (as the function can influence it). So we get something like:

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

If we now want f to get the world that g left behind, we'd write:

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

Or generalized:

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)

Now if the user can only use >>=, return and a few pre-defined IO values we get a nice property again: The user will never actually see the RealWorld# getting passed around! And that is a very good thing, as you aren't really interested in the details of where getLine gets its data from. And again we get all the nice high-level functions from the monad libraries.

So the important things to take away:

  1. The monad captures common patterns in your code, like "always pass all elements of container A to container B" or "pass this real-world-tag through". Often, once you realize that there is a monad in your program, complicated things become simply applications of the right monad combinator.

  2. The monad allows you to completely hide the implementation from the user. It is an excellent encapsulation mechanism, be it for your own internal state or for how IO manages to squeeze non-purity into a pure program in a relatively safe way.


Appendix

In case someone is still scratching his head over RealWorld# as much as I did when I started: There's obviously more magic going on after all the monad abstraction has been removed. Then the compiler will make use of the fact that there can only ever be one "real world". That's good news and bad news:

  1. It follows that the compiler must guarantuee execution ordering between functions (which is what we were after!)

  2. But it also means that actually passing the real world isn't necessary as there is only one we could possibly mean: The one that is current when the function gets executed!

Bottom line is that once execution order is fixed, RealWorld# simply gets optimized out. Therefore programs using the IO monad actually have zero runtime overhead. Also note that using RealWorld# is obviously only one possible way to put IO - but it happens to be the one GHC uses internally. The good thing about monads is that, again, the user really doesn't need to know.

怪我鬧 2024-12-18 02:33:12

关于 IO monad,有以下三个主要观察结果:

1) 您无法从中获取值。其他类型(例如Maybe)可能允许提取值,但 monad 类接口本身和IO 数据类型都不允许这样做。

2) “内部”IO 不仅是真正的价值,也是“现实世界”的东西。此虚拟值用于通过类型系统强制执行操作链接:如果您有两个独立的计算,则使用 >>= 会使第二个计算依赖在第一个。

3) 假设一个非确定性的事情,比如 random :: () -> Int,这在 Haskell 中是不允许的。如果将签名更改为random :: Blubb -> (Blubb, Int),如果您确保没有人可以使用 Blubb 两次,那么这是允许的:因为在这种情况下,所有输入都是“不同的” ”,输出也不同也没有问题。

现在我们可以利用事实 1):没有人可以从 IO 中获取任何东西,因此我们可以使用隐藏在 IO 中的 RealWord 虚拟对象来提供服务作为一个Blubb。整个应用程序中只有一个 IO(我们从 main 获得的那个),它负责正确的排序,正如我们在 2) 中看到的那样。问题解决了。

There are three main observations concerning the IO monad:

1) You can't get values out of it. Other types like Maybe might allow to extract values, but neither the monad class interface itself nor the IO data type allow it.

2) "Inside" IO is not only the real value but also that "RealWorld" thing. This dummy value is used to enforce the chaining of actions by the type system: If you have two independent calculations, the use of >>= makes the second calculation dependent on the first.

3) Assume a non-deterministic thing like random :: () -> Int, which isn't allowed in Haskell. If you change the signature to random :: Blubb -> (Blubb, Int), it is allowed, if you make sure that nobody ever can use a Blubb twice: Because in that case all inputs are "different", it is no problem that the outputs are different as well.

Now we can use the fact 1): Nobody can get something out of IO, so we can use the RealWord dummy hidden in IO to serve as a Blubb. There is only one IOin the whole application (the one we get from main), and it takes care of proper sequentiation, as we have seen in 2). Problem solved.

御守 2024-12-18 02:33:12

经常帮助我理解事物本质的一件事就是以尽可能最琐碎的方式检查它。这样,我就不会因为可能不相关的概念而分心。考虑到这一点,我认为理解 Identity Monad,因为它是 Monad 可能最简单的实现(我认为)。

Identity Monad 有什么有趣的地方?我认为它允许我表达在其他表达式定义的上下文中评估表达式的想法。对我来说,这就是我(到目前为止)遇到的每个 Monad 的本质。

如果您在学习 Haskell 之前已经接触过很多“主流”编程语言(就像我一样),那么这看起来一点也不有趣。毕竟,在主流编程语言中,语句是按顺序执行的,一个接一个(当然,控制流结构除外)。当然,我们可以假设每个语句都是在所有先前执行的语句的上下文中进行评估的,并且那些先前执行的语句可能会改变当前执行语句的环​​境和行为。

对于像 Haskell 这样的函数式、惰性语言来说,所有这些几乎都是一个外国概念。 Haskell 中计算的计算顺序是明确定义的,但有时很难预测,甚至更难控制。对于很多类型的问题来说,这样就很好了。但是,如果没有某种方便的方法在程序中的计算之间建立隐式顺序和上下文,其他类型的问题(例如 IO)就很难解决。

具体来说,就副作用而言,它们通常可以(通过 Monad)转换为简单的状态传递,这在纯函数式语言中是完全合法的。然而,有些 Monad 似乎并不具有这种性质。像 IO Monad 或 ST monad 这样的 Monad 实际上会执行副作用操作。有很多方法可以思考这个问题,但我思考的一种方式是,仅仅因为我的计算必须存在于一个没有副作用的世界中,Monad 可能不会。因此,Monad 可以自由地为我的计算建立一个上下文来执行,该上下文基于其他计算定义的副作用。

最后,我必须声明我绝对不是 Haskell 专家。因此,请理解我所说的一切几乎都是我自己对这个主题的想法,当我更充分地理解 Monad 时,我很可能会否认它们。

One thing that often helps me to understand the nature of something is to examine it in the most trivial way possible. That way, I'm not getting distracted by potentially unrelated concepts. With that in mind, I think it may be helpful to understand the nature of the Identity Monad, as it's the most trivial implementation of a Monad possible (I think).

What is interesting about the Identity Monad? I think it is that it allows me to express the idea of evaluating expressions in a context defined by other expressions. And to me, that is the essence of every Monad I've encountered (so far).

If you already had a lot of exposure to 'mainstream' programming languages before learning Haskell (like I did), then this doesn't seem very interesting at all. After all, in a mainstream programming language, statements are executed in sequence, one after the other (excepting control-flow constructs, of course). And naturally, we can assume that every statement is evaluated in the context of all previously executed statements and that those previously executed statements may alter the environment and the behavior of the currently executing statement.

All of that is pretty much a foreign concept in a functional, lazy language like Haskell. The order in which computations are evaluated in Haskell is well-defined, but sometimes hard to predict, and even harder to control. And for many kinds of problems, that's just fine. But other sorts of problems (e.g. IO) are hard to solve without some convenient way to establish an implicit order and context between the computations in your program.

As far as side-effects go, specifically, often they can be transformed (via a Monad) in to simple state-passing, which is perfectly legal in a pure functional language. Some Monads don't seem to be of that nature, however. Monads such as the IO Monad or the ST monad literally perform side-effecting actions. There are many ways to think about this, but one way that I think about it is that just because my computations must exist in a world without side-effects, the Monad may not. As such, the Monad is free to establish a context for my computation to execute that is based on side-effects defined by other computations.

Finally, I must disclaim that I am definitely not a Haskell expert. As such, please understand that everything I've said is pretty much my own thoughts on this subject and I may very well disown them later when I understand Monads more fully.

若水微香 2024-12-18 02:33:12

有了容器的概念,该语言本质上表明容器内的任何内容都是不确定的

。Haskell 是确定性的。如果你要求整数加法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)

您在这里看到非确定性了吗?不,这是列表的确定性构建。运行两次,您将以相同的顺序获得相同的数字。

你可以这样描述:从 [4,5] 中选择任意 x。从 [0,1] 中选择任意 y。返回 x+y。收集所有可能的结果。

这种方式似乎涉及不确定性,但它只是一个嵌套循环(列表理解)。这里不存在“真正的”不确定性,它是通过检查所有可能性来模拟的。非决定论是一种幻觉。该代码只是看起来是不确定的。

此代码使用 State monad:

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)

尝试编写组合自己操作。这就是真正发生的事情,没有“副作用”,只是将参数传递给函数。

With this concept of containers, the language essentially says anything inside the containers is non-deterministic

No. Haskell is deterministic. If you ask for integer addition 2+2 you will always get 4.

"Nondeterministic" is only a metaphor, a way of thinking. Everything is deterministic under the hood. If you have this code:

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

it is roughly equivalent to Python code

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

You see nondeterminism here? No, it's deterministic construction of a list. Run it twice, you'll get the same numbers in the same order.

You can describe it this way: Choose arbitrary x from [4,5]. Choose arbitrary y from [0,1]. Return x+y. Collect all possible results.

That way seems to involve nondeterminism, but it's only a nested loop (list comprehension). There is no "real" nondeterminism here, it's simulated by checking all possibilities. Nondeterminism is an illusion. The code only appears to be nondeterministic.

This code using State monad:

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

gives 5 and seems to involve changing state. As with lists it's an illusion. There are no "variables" that change (as in imperative languages). Everything is nonmutable under the hood.

You can describe the code this way: put 0 to a variable. Read the value of a variable to x. Put (x+2) to the variable. Read the variable to y, and return y+3.

That way seems to involve state, but it's only composing functions passing additional parameter. There is no "real" mutability here, it's simulated by composition. Mutability is an illusion. The code only appears to be using it.

Haskell does it this way: you've got functions

   a -> s -> (b,s)

This function takes and old value of state and returns new value. It does not involve mutability or change variables. It's a function in mathematical sense.

For example the function "put" takes new value of state, ignores current state and returns new state:

   put x _ = ((), x)

Just like you can compose two normal functions

  a -> b
  b -> c

into

  a -> c

using (.) operator you can compose "state" transformers

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

into a single function

  a -> s -> (c,s)

Try writing the composition operator yourself. This is what really happens, there are no "side effects" only passing arguments to functions.

诗酒趁年少 2024-12-18 02:33:12

重点是可以在函数、容器和副作用链中进行干净的错误处理

副作用问题到底是如何解决的?

I/O monad 中的值(即IO a 类型之一)应被解释为程序。 p>>然后,IO 值上的 q 可以解释为将两个程序组合成一个首先执行 p,然后执行 q 的运算符。其他 monad 运算符也有类似的解释。通过将程序指定为名称 main,您可以向编译器声明该程序必须由其输出目标代码执行。

至于列表 monad,除了非常抽象的数学意义上外,它与 I/O monad 并没有真正的关系。 IO monad 提供具有副作用的确定性计算,而列表 monad 提供非确定性(但不是随机!)回溯搜索​​,有点类似于 Prolog 的操作方式。

the point is so there can be clean error handling in a chain of functions, containers, and side effects

More or less.

how exactly is the problem of side-effects solved?

A value in the I/O monad, i.e. one of type IO a, should be interpreted as a program. p >> q on IO values can then be interpreted as the operator that combines two programs into one that first executes p, then q. The other monad operators have similar interpretations. By assigning a program to the name main, you declare to the compiler that that is the program that has to be executed by its output object code.

As for the list monad, it's not really related to the I/O monad except in a very abstract mathematical sense. The IO monad gives deterministic computation with side effects, while the list monad gives non-deterministic (but not random!) backtracking search, somewhat similar to Prolog's modus operandi.

女中豪杰 2024-12-18 02:33:12

据我了解,Monad 只是另一个声明与数据交互的方法的类型类 [...]

...为所有具有以下类型的类型提供了一个通用的接口实例。然后,这可以用于提供适用于所有单子类型的通用定义。

用一个概念来实现这三件事似乎聪明又干净[...]

...唯一实现的三件事是这三种类型的实例(列表,也许< /code> 和 IO) - 类型本身在其他地方独立定义。

[...] 但实际上,重点是可以在函数、容器和副作用链中进行干净的错误处理。

不仅仅是错误处理,例如考虑ST - 如果没有单子接口,将必须直接正确地传递封装状态...a令人厌烦的任务。


副作用问题到底是如何解决的?

简短的回答:Haskell 解决通过使用类型来指示它们的存在来管理它们。

有人可以直观地解释一下 Haskell 如何通过输入和输出改变状态吗?

“直观地”...就像这里有什么?让我们尝试一个简单的直接比较:

  • 来自 如何声明命令作者:Philip Wadler:

    <前><代码>(* 第 26 页 *)
    输入 'a io = 单位 -> '一个

    中缀>>=
    val >>= : 'a io * ('a -> 'b io) -> 'b io
    fun m>>= k = fn()==>让
    val x = m ()
    val y = kx ()

    y
    结尾

    val 返回 : 'a -> 'a io
    fun return x = fn() =>; x

    val putc : char ->;单位io
    fun putc c = fn () =>;普特克ML

    val getc : char io
    val getc = fn() =>;获取cML()

    有趣的 getcML() =
    valOf(TextIO.input1(TextIO.stdIn))

    (* 第 25 页 *)
    有趣的 putcML c =
    TextIO.output1(TextIO.stdOut,c)

  • 基于 两个答案我的,这是我的 Haskell 翻译:

    类型 IO a = OI ->一个
    
    (>>=)::IO a -> (a→IO b)→ IOb
    m>>=k=\u->让 !(u1, u2) = u 的一部分
                         让!x = m u1 in
                         让 !y = kx u2 输入
                         y
    
    返回::a-> IOa
    返回x=\u->让 !_ = x 中的 u 部分
    
    putc :: 字符 -> IO()
    putc c=\u->;腐烂铜
    
    getc :: IO 字符
    getc=\u->;得到cOI u
    
     -- 基元
    数据OI
    部分OI::OI-> (奥伊,奥伊) 
    putcOI :: 字符 -> OI-> ()
    getcOI :: OI - >查尔
    

现在还记得关于副作用的简短回答吗?

Haskell 通过使用类型来指示它们的存在来管理它们。

Data.Char.chr :: Int -> Char    -- no side effects

getChar       :: IO Char        -- side effects at
           {- :: OI -> Char -}  -- work: beware!

From what I understand, Monad is just another typeclass that declares ways to interact with data [...]

...providing an interface common to all those types which have an instance. This can then be used to provide generic definitions which work across all monadic types.

It seems clever and clean to implement these 3 things with one concept [...]

...the only three things that are implemented are the instances for those three types (list, Maybe and IO) - the types themselves are defined independently elsewhere.

[...] but really, the point is so there can be clean error handling in a chain of functions, containers, and side effects.

Not just error handling e.g. consider ST - without the monadic interface, you would have to pass the encapsulated-state directly and correctly...a tiresome task.


How exactly is the problem of side-effects solved?

Short answer: Haskell solves manages them by using types to indicate their presence.

Can someone explain how, intuitively, Haskell gets away with changing state with inputs and output?

"Intuitively"...like what's available over here? Let's try a simple direct comparison instead:

  • From How to Declare an Imperative by Philip Wadler:

    (* page 26 *)
    type 'a io  = unit -> 'a
    
    infix >>=
    val >>=     : 'a io * ('a -> 'b io) -> 'b io
    fun m >>= k = fn () => let
                             val x = m ()
                             val y = k x ()
                           in
                             y
                           end
    
    val return  : 'a -> 'a io
    fun return x = fn () => x
    
    val putc    : char -> unit io
    fun putc c = fn () => putcML c
    
    val getc    : char io
    val getc  = fn () => getcML ()
    
    fun getcML () =
       valOf(TextIO.input1(TextIO.stdIn))
    
    (* page 25 *)
    fun putcML c =
      TextIO.output1(TextIO.stdOut,c)
    
  • Based on these two answers of mine, this is my Haskell translation:

    type IO a  =  OI -> a
    
    (>>=)      :: IO a -> (a -> IO b) -> IO b
    m >>= k    =  \ u -> let !(u1, u2) = part u in
                         let !x = m u1 in
                         let !y = k x u2 in
                         y
    
    return     :: a -> IO a
    return x   =  \ u -> let !_ = part u in x
    
    putc       :: Char -> IO ()
    putc c     =  \ u -> putcOI c u
    
    getc       :: IO Char
    getc       =  \ u -> getcOI u
    
     -- primitives
    data OI
    partOI :: OI -> (OI, OI) 
    putcOI :: Char -> OI -> ()
    getcOI :: OI -> Char
    

Now remember that short answer about side-effects?

Haskell manages them by using types to indicate their presence.

Data.Char.chr :: Int -> Char    -- no side effects

getChar       :: IO Char        -- side effects at
           {- :: OI -> Char -}  -- work: beware!
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文