Haskell是否真正纯净(任何处理系统外输入和输出的语言都是吗)?

39

在涉及到函数式编程中的单子(Monads)后,这种特性是否真的使一个语言变得纯粹,或者它只是计算机系统在现实世界中推理的另一种“get out of jail free card”,超出了黑板数学的范畴?

编辑:

这并不是某些人所说的煽动争端,而是一个真正的问题。我希望有人可以给我提供证据,证明它是纯粹的。

此外,我将与其他不太纯的函数式语言以及使用良好设计的一些面向对象语言进行比较,并评估它们的纯度。到目前为止,在我极其有限的函数式编程世界中,我仍然没有理解单子的纯度。不过你会很高兴知道,我喜欢不可变性的概念,因为它在保持纯度方面更加重要。


5
您以下的评论表明您已经回答了自己的问题,只是希望有人确认您的结论。 - Phil Miller
在阅读了您所有的评论后,看起来您的意思是由于Haskell编译为可以在物理机器上运行的程序,所以它不能是纯粹的...这是荒谬的,并且与单子、IO甚至所有计算机科学家定义的纯度毫无关系。如果这是您的观点,那么没有办法“证明”任何相反的观点,并且根据您扭曲的定义,任何语言都无法是纯粹的,即使Lisp机器也会受到物理故障模式的影响... - Jedai
与单子无关,我同意,但它与 Haskell 声称通过关联数学思想来分配自身纯度有很大关系。在现实世界中,IO 不是纯的,从来没有,也永远不会是。如果我切断电话线,即使我通过 Monad 结构进行通信,对话也不会发生。因此,单子不能阻止这种情况发生,也不能防止它发生,甚至不能更正事件以使输入/输出“纯净”。单子将不会更正 http 错误、API 调用错误或任何其他涉及系统外部的 IO。 - WeNeedAnswers
@Jedai,你应该意识到,尽管你在Haskell中付出了很多努力,但代码最终会被转换成机器码,涉及到许多令人不快的位移、寄存器调用和有状态的特性,这是实际计算机所使用的。现实很残酷 :) - WeNeedAnswers
2
@WeNeedAnswers 所有语言最终都被翻译成二进制指令传递给CPU这一事实,幸运的是与讨论编程语言的优点无关,否则就没有讨论的必要了:尽管大多数理智的程序员都同意用Python或汇编语言开发软件是不同的体验...看来你还没有意识到说一个语言是“纯粹的”与最终如何执行它没有任何关系:请参阅Wikipedia以获得澄清。 - Jedai
1
顺便说一下,位移、寄存器等等都不是什么可怕的东西,对大多数Haskeller来说,机器码也不是“恶心”的。我还想知道你所谓的“数学意识形态”是什么意思:据我所知,并没有这样的东西。你似乎对Haskell声称自己是纯函数式语言感到不满,但这只是客观事实……无论你是否认为这是Haskell作为编程语言的决定性优势甚至是积极因素(但这是一个观点问题,因此超出了StackOverflow的讨论范围)。 - Jedai
8个回答

81

请看下面的迷你语言:

data Action = Get (Char -> Action) | Put Char Action | End

Get f 的意思是:读取一个字符 c,并执行动作 f c

Put c a 的意思是:写入字符 c,并执行动作 a

下面是一个程序,它会打印出 "xy",然后要求输入两个字母并将它们以相反的顺序打印出来:

Put 'x' (Put 'y' (Get (\a -> Get (\b -> Put b (Put a End)))))

你可以操纵这样的程序。例如:
conditionally p = Get (\a -> if a == 'Y' then p else End)

这是一个类型为Action -> Action的程序,它需要一个程序并提供另一个程序,在执行前需要询问确认。下面是另一个例子:

printString = foldr Put End

这是一个类型为String -> Action的函数,它接受一个字符串并返回一个程序,该程序会写入该字符串,例如:Put 'h' (Put 'e' (Put 'l' (Put 'l' (Put 'o' End))))

Haskell中的IO也是类似的。虽然执行需要执行副作用,但你可以以纯粹的方式构建复杂的程序,而不必执行它们。你正在计算程序的描述(IO操作),而不是实际执行它们。

在像C这样的语言中,您可以编写一个函数void execute(Action a)来实际执行程序。在Haskell中,您通过编写main = a来指定该操作。编译器创建一个执行该操作的程序,但您没有其他执行操作的方法(除了脏技巧)。

显然,GetPut不是唯一的选项,您可以将许多其他API调用添加到IO数据类型中,例如对文件或并发性进行操作。

添加结果值

现在考虑以下数据类型。

data IO a = Get (Char -> Action) | Put Char Action | End a

前面提到的Action类型相当于IO (),即一个始终返回"unit"(可视为"void")的IO值。

这个类型与Haskell IO非常相似,只不过在Haskell中,IO是一个抽象数据类型(你无法访问其定义,仅能使用一些方法)。

这些是带有结果的IO操作。一个类似于这样的值:

Get (\x -> if x == 'A' then Put 'B' (End 3) else End 4)

类型为IO Int,对应于一个C程序:

int f() {
  char x;
  scanf("%c", &x);
  if (x == 'A') {
    printf("B");
    return 3;
  } else return 4;
}

评估和执行

评估和执行是有区别的。您可以评估任何Haskell表达式并获得一个值;例如,将2 + 2 :: Int评估为4 :: Int。只有类型为IO a的Haskell表达式才能被执行。这可能会产生副作用;执行Put 'a' (End 3)将字母a放在屏幕上。如果您评估一个IO值,就像这样:

if 2+2 == 4 then Put 'A' (End 0) else Put 'B' (End 2)

你将获得:

Put 'A' (End 0)

但是没有副作用 - 你只进行了一次评估,这是无害的。

你会如何翻译?

bool comp(char x) {
  char y;
  scanf("%c", &y);
  if (x > y) {       //Character comparison
    printf(">");
    return true;
  } else {
    printf("<");
    return false;
  }
}

如何将其转换为IO值?

首先,我们需要修复一些字符,比如 'v'。现在 comp('v') 是一个IO操作,它会将给定的字符与 'v' 进行比较。同样地,comp('b') 是一个IO操作,它会将给定的字符与 'b' 进行比较。通常情况下,comp 是一个接受字符并返回IO操作的函数。

作为一个C程序员,你可能会认为 comp('b') 是一个布尔值。在C语言中,评估和执行是相同的(即它们意味着相同的事情,或者同时发生)。但在Haskell中不是这样的。 comp('b') 评估 为一些IO操作,该操作在 执行 后返回一个布尔值。(确切地说,它评估为上面的代码块,只是将 'b' 替换为 x。)

comp :: Char -> IO Bool
comp x = Get (\y -> if x > y then Put '>' (End True) else Put '<' (End False))

现在,comp 'b' 的评估结果为Get (\y -> if 'b' > y then Put '>' (End True) else Put '<' (End False))。这在数学上也是有意义的。在C中,int f() 是一个函数。对于数学家来说,这是没有意义的-一个没有参数的函数?函数的目的是接受参数。函数int f()应该等价于int f。但实际上并不是这样,因为C语言中的函数将数学函数和IO操作混合在一起。这些IO值是第一类的。就像你可以有一个包含整数元组的列表的列表[[(0,2),(8,3)],[(2,8)]]一样,你也可以使用IO构建复杂的值。
 (Get (\x -> Put (toUpper x) (End 0)), Get (\x -> Put (toLower x) (End 0)))
   :: (IO Int, IO Int)

一个IO操作的元组:第一个操作读取一个字符并将其转换为大写后打印,第二个操作读取一个字符并返回其小写形式。

 Get (\x -> End (Put x (End 0))) :: IO (IO Int)

一个读取字符 x 并结束的 IO 值,返回一个将 x 写入屏幕的 IO 值。

Haskell 有特殊的函数,可以轻松操作 IO 值。例如:

 sequence :: [IO a] -> IO [a]

这个函数接受一个 IO 操作列表,并返回一个按顺序执行它们的 IO 操作。

单子

单子是一些组合子(比如上面的 conditionally),它们允许您更结构化地编写程序。有一个类型为的组合函数

 IO a -> (a -> IO b) -> IO b

给定一个IO a和一个函数a -> IO b,返回类型为IO b的值。如果将第一个参数写成C函数a f(),第二个参数写成b g(a x),则它将返回g(f(x))的程序。根据上述Action / IO的定义,您可以自己编写该函数。

请注意,单子对于纯度并不是必需的-您始终可以像我上面那样编写程序。

纯度

有关纯度的重要事情是引用透明性,并区分评估和执行。

在Haskell中,如果您有f x+f x,则可以将其替换为2*f x。在C中,f(x)+f(x)通常不等同于2*f(x),因为f可能会在屏幕上打印某些内容或修改x

由于纯度,编译器具有更大的自由度,可以更好地进行优化。它可以重新排列计算,而在C中,它必须考虑是否改变了程序的含义。


9
参照透明度,我喜欢这个说法。听起来比“纯洁”和“不纯”的纳粹术语高级得多。用后者这样的词汇,我感觉自己像《哈利·波特》中的一个群众演员。非常好的回答。 - WeNeedAnswers
20
如果它执行IO操作,那么它的类型为IO Int(或类似类型),你不能使用(+)进行相加,因为(+)只接受数字。你需要像这样编写代码:"perform f x, perform f x, add results",即do a <- f x; b <- f x; return (a+b)。有一个高阶函数可以做到这一点:liftM2 (+) (f x) (f x) - sdcvvc
3
您无法重新排列IO操作putStr "hello" >> putStr "world",就像您不能将列表[1,2]重新排列为[2,1]一样。不过,如果[1,2]putStr "hello" >> putStr "world"是复杂计算的结果,则可以优化该计算,并且指导这些优化的原则相同,无论您是计算列表还是IO操作。例如,f x + f x可以优化为2 * f x,而在C语言中,您必须知道f没有副作用。 - sdcvvc
2
@mljrg:在Haskell中,法律不必像“此函数没有副作用”那样做出规定,因为像打印这样的效果是一等值(我的答案中的Action),而不是调用函数的副作用。换句话说,原因不是“因为它不纯”,而是“因为它改变了非交换操作>>的顺序”。 - sdcvvc
@sdcvvc:已编辑。请确认是否符合您的意思。 - Nawaz
显示剩余2条评论

9

重要的是要理解,单子本身并没有什么特别之处-因此它们绝对不代表在这方面的“出狱卡”。实现或使用单子不需要编译器(或其他)魔法,它们在Haskell的纯函数环境中定义。特别地,sdcvvc已经展示了如何以纯函数方式定义单子,而不需要任何实现后门。


6
“在黑板数学之外推理计算机系统”是什么意思?这将是哪种推理?是“死算”吗?
副作用和纯函数取决于观点。如果我们将名义上具有副作用的函数视为将我们从一个世界状态转移到另一个世界状态的函数,那么它就再次成为纯函数。
我们可以通过给每个具有副作用的函数提供第二个参数(即世界)并要求它在完成时传递一个新的世界来使其变得纯净。我已经不再了解C++,但是假设read的签名如下:
vector<char> read(filepath_t)

在我们的新“纯洁风格”中,我们处理方式如下:
pair<vector<char>, world_t> read(world_t, filepath_t)

这实际上是每个Haskell IO操作的工作方式。

现在我们有了一个IO的纯模型。谢天谢地。如果我们不能做到这一点,那么Lambda演算和图灵机可能不是等价的形式化方法,那么我们就需要解释一些问题。我们还没有完成,但我们面临的两个问题很容易:

  • world_t结构中放什么?每一粒沙子、每一片草、每一颗心碎和每一个金色日落的描述?

  • 我们有一个非正式的规则,只使用一次世界——在每个IO操作之后,我们抛弃使用它的世界。然而,由于所有这些世界都在飘荡,我们注定会把它们搞混。

第一个问题很容易解决。只要我们不允许检查世界,就不需要为其存储任何内容。我们只需要确保新的世界不等于以前的任何一个世界(否则编译器可能会狡猾地优化掉一些产生世界的操作,就像在C++中有时会做的那样)。有许多处理这个问题的方法。

至于世界混淆的问题,我们希望将世界传递隐藏在一个库中,这样就没有办法接触到世界,因此也就没有办法混淆它们。事实证明,单子是隐藏计算中的“侧通道”的一种很好的方法。于是就有了IO单子。

一段时间以前,Haskell邮件列表上问过像你这样的问题,我在那里更详细地讨论了“侧通道”。以下是Reddit线程(其中链接到我的原始电子邮件):

http://www.reddit.com/r/haskell/comments/8bhir/why_the_io_monad_isnt_a_dirty_hack/


2
这是你自己制造的问题。一个程序所描述的语言运行的环境是否“纯净”与该语言是否纯粹完全无关。 - Cubic
@ Cubic,这个非常相关。通过将声明性转换为命令式,错误可能会发生。你可以在Monad中包装IO,让自己满意,但在一天结束时,那些真实的实际位将变成真实的,电子将流动。任何虚张声势都不会改善一个纯粹的“假想”语言与现实世界互动的情况,使其比旧的命令式风格获得更好的结果。当你进入位级别时,所有这些都基于工程物理原理变成了一种混沌的灰色泥浆,而不是黑板数学。 - WeNeedAnswers
4
Lambda演算是纯的且图灵完备的。同样,无论您是否能够设计一台“执行”代码的纯净机器,都与描述程序的语言是否纯洁毫不相关。您的论点就像说要构造一个正方形是不可能的,因为这需要能够构造长度是无理数的对角线一样不可取。 - Cubic
1
@WeNeedAnswers Haskell并不需要被编译才能存在。你可以在纸上计算值。 main = putStrLn "Hello"的值是一个动作,它会打印“hello”,如果有人正在编译它并执行它。执行该动作的肮脏技巧在Haskell之外。在Haskell中,您无法执行操作(IO),只能计算操作并希望外部事物为您执行它。 - mb14
1
@WeNeedAnswers 我向你提出挑战,让你基于图灵的数学构建一台计算机,甚至是一台具备64位地址空间的计算机。编译器将从计算模型转化为具体的计算机,而这些计算机包括所有类型。 - solidsnack
显示剩余4条评论

4
我对函数式编程非常陌生,但是我这样理解它:
在Haskell中,您定义了一堆函数。这些函数不会被执行。它们可能会被求值。
有一个特定的函数会被求值。这是一个产生一组“操作”的常量函数。这些操作包括函数的求值和执行IO以及其他“真实世界”的事情。您可以编写函数来创建和传递这些操作,并且除非使用unsafePerformIO求值函数或它们由主函数返回,否则它们将永远不会被执行。
因此,简而言之,Haskell程序是一个由其他函数组成的函数,返回一个命令式程序。Haskell程序本身是纯的。显然,该命令式程序本身不能是纯的。从定义上讲,现实世界的计算机是不纯的。
这个问题还有很多内容,其中很多都是语义问题(人类语言,而不是编程语言)。单子也比我在这里描述的要抽象得多。但是我认为这是一种通用的有用思考方式。

2
这个问题似乎像是不负责任的挑衅,为什么不说相反的话 - 只有 Haskell 能做 IO,其他所有语言都是残缺的呢?例如:当我们使用主流命令式语言编程时,我们很少有机会超越 ArrowChoice 的限制。Haskell 可以成为一种优秀的命令式语言,正是因为单子允许它消除编译和执行之间的边界,因此比我们通常使用的工具集更加表达力强大。 - applicative
2
@WeNeedAnswers 你似乎非常困惑...首先,除了IO和ST可能存在讨论的情况外,所有单子都是完全“纯”的,单子本身的概念本质上并不是不纯的。有一种观点认为,IO Monad本身是纯的,只会产生由运行时(用C编写)执行的命令式程序,但这是无关紧要的,因为你似乎关注的是“由于不纯性可能仍然会出现错误”,而不是Haskell的理论纯度。 - Jedai
当然,如果你在Haskell中进行IO操作,你可能会遇到与其他语言相同的问题:Haskell并不像魔法一样可以立即下载、使网络无延迟、修复损坏的文件等等!但它所能做到的是,保证你的代码片段是无IO的,因此不会有任何你担心的问题。IO单子允许你通过类型将IO代码与程序的纯核心隔离开来,IO代码不能无意中被执行:你的IO代码必须在IO单子中(所以带有类型“IO a”)。 - Jedai
感谢您为Jedai做出的贡献,您所做的只是重申了问题领域,并重新引入了相同的论点,即在数学术语中,单子是纯粹的,但在现实中它们并不是。我并不争论单子不够酷和时尚,在它们正确的位置上,连同无限符号一起,在黑板上涂写时,确实有助于推理不合理的事情。然而,在现实世界中,作为日常程序员,它们并没有做任何新的事情。 - WeNeedAnswers
2
@WeNeedAnswers,我刚看到了你的评论。你认为在计算器上输入“2+2=”时显示的内容是不可能预知的吗?因为计算器不是理论构造,而是由原子制成的实际世界的物品。这完全是无稽之谈。在机器上执行的Haskell仅仅是一台先进的计算器,你可以推理出它将会做什么。 - sdcvvc
显示剩余6条评论

4
我这样想:程序需要与外界交互才能有用。当你编写代码(无论使用什么语言)时,应该努力编写尽可能多的纯净、无副作用的代码,并将IO限制在特定的位置。
在Haskell中,我们更加注重编写严格控制效果的代码。在核心和许多库中,有大量的纯代码。Haskell实际上就是这样的。Haskell中的单子可用于许多事情。其中一件事就是包含处理不纯的代码。
这种设计方式与极大地促进它的语言一起,有助于我们生产更可靠的工作,需要更少的单元测试来明确其行为,并通过组合实现更多的重用。
如果我正确理解你所说的,我认为这不是虚假或只存在于我们头脑中的东西,就像“免费出狱卡”一样。这里的好处是非常真实的。

11
这里的问题部分在于你把Haskell当做告诉物理机器该怎么做的方式。虽然它当然可以这么做,但实际上Haskell表达式具有指示语义——一种独立于执行方式(无论是在机器上还是手动)的意义。因为Haskell的定义不涉及类似内存地址等机器概念,所以这很容易理解;而对于像C语言这样的语言,指示语义则比较棘手。 - sclv
@sclv,没错,我认为你说得很到位,在思维实验中,我们处理数学和逻辑的完美本质时,Haskell确实是一种非常优秀的元语言。但是这里出现了一个问题,单子(Monad)是现实世界和Haskell完美世界之间的接口。单子在Haskell语言中被定义,错误可能会在此处发生,就像任何其他基于计算机的编程语言一样,大多数错误都会因现实的不纯性而发生。 - WeNeedAnswers
5
“纯函数式编程”是指没有副作用的编程,它并不意味着不会出现错误,或者你编写的软件将是无 bug 的。如果使用 Either monad,可以允许你抛出和捕获异常 - 在执行 >>= 时,它检查是 Left(异常)还是 Right(正常值)。虽然它与命令式编程中的异常非常相似,但它是纯的。IO monad 做同样的事情。尝试向我的答案中添加 ThrowCatch 功能。你似乎对 monad 有相当强烈的看法,但还没有完全理解它们 - 这很糟糕! - sdcvvc
@sdcwc,好的,现在我真的很困惑。我已经接受了函数式编程的理念,将我的思维转变了几度,抛弃了面向对象中的模式,转向了证明论和数学的各个方面,这些都是为了实现无误差、线程安全的编程,而现在你告诉我,无论你如何处理IO,都无法避免错误?这有点像薛定谔的猫和可观测问题。我要去找些锤子和扳手,用火花塞制造一个图灵完备的机器 ;) - WeNeedAnswers
@sdcwc 我目前正在研究 F# 和 Haskell,对这两种语言进行比较和对比。我真的很喜欢 F#,但在那之前我非常喜欢 Haskell(本来想学 Lisp 但是那些括号让我眼睛疼)。我是一名命令式程序员,但我诚实地试图快速转向函数式编程,因为我看到了这种风格的许多优势,而且我从未真正接受面向对象编程方面的理念。 - WeNeedAnswers
显示剩余3条评论

4

1

Haskell是真正的纯函数式编程语言吗?

从绝对意义上来说:不是。

你运行程序的固态图灵机器——无论是Haskell还是其他语言——都是一个具有状态和效应的设备。为了让任何程序使用其所有“特性”,程序将不得不使用状态和效应

至于那个带有贬义的术语所归属的所有其他“含义”:

在一台最显著特征是状态的机器上假定一种无状态的计算模型,似乎是一个奇怪的想法,至少可以这么说。模型和机器之间的差距很大,因此弥合起来非常昂贵。没有硬件支持功能可以抹去这个事实:这仍然是一个不好的实践想法。

这也被函数式语言的支持者们认识到了。他们以各种巧妙的方式引入了状态(和变量)。纯函数式的特性已经被牺牲和妥协了。旧的术语变得具有欺骗性。

Niklaus Wirth


使用单子类型是否真的使语言纯粹?

不是。这只是使用类型来划分:

  • 完全没有可见副作用的定义 -
  • 可能具有可见副作用的定义 - 操作

你也可以像Clean一样使用唯一性类型...


这个问题有讽刺意味,考虑到在Haskell 2010报告中对IO类型的描述:
IO类型用作与外部世界交互的操作(动作)的标记。 IO类型是抽象的:用户看不到任何构造函数。 IOMonadFunctor类的实例。”
借鉴另一个答案的说法:
“[…] IO是神奇的(具有实现但没有表示)[…]”

作为抽象类型,IO类型绝不是一个“免费出狱卡”——需要复杂的模型来解释Haskell中I/O的工作方式。更多详情请参见:


这并非一直如此 - Haskell 最初拥有一个至少部分可见的 I/O 机制;最后一个拥有它的语言版本是 Haskell 1.2。当时,main 的类型为:

main :: [Response] -> [Request]

通常缩写为:

main :: Dialogue

where:

type Dialogue = [Response] -> [Request]

然而,ResponseRequest虽然是庞大的数据类型,但却十分谦逊:

pared-down definitions of request and response datatypes for dialogue-based I/O

在Haskell中使用单子界面的I/O的出现改变了一切,不再有可见数据类型,只有一个抽象描述。因此,IO, return, (>>=)等的真正定义现在是针对每个Haskell实现具体而言的。
(为什么旧的I/O机制会被废弃呢?"解决尴尬局面" 概述了它的问题。)
这些天,更相关的问题应该是:
  • I/O在你的Haskell实现中是否是引用透明的?
正如Owen Stephens在函数式I/O方法中指出的那样:

I/O不是一个特别活跃的研究领域,但仍然在发现新的方法...

Haskell语言可能会有一个引用透明的I/O模型,这个模型不会引起太多争议...


-2
不,IO单子并不是纯的,因为它具有副作用和可变状态(在Haskell程序中可能存在竞争条件,所以...纯FP语言不知道什么是“竞争条件”)。真正的纯FP是带有唯一类型的Clean,或者带有FRP(函数响应式编程)的Elm,而不是Haskell。Haskell是一个大谎言。
证明:
import Control.Concurrent 
import System.IO as IO
import Data.IORef as IOR

import Control.Monad.STM
import Control.Concurrent.STM.TVar

limit = 150000
threadsCount = 50

-- Don't talk about purity in Haskell when we have race conditions 
-- in unlocked memory ... PURE language don't need LOCKING because
-- there isn't any mutable state or another side effects !!

main = do
    hSetBuffering stdout NoBuffering
    putStr "Lock counter? : "
    a <- getLine
    if a == "y" || a == "yes" || a == "Yes" || a == "Y"
    then withLocking
    else noLocking

noLocking = do
    counter <- newIORef 0
    let doWork = 
        mapM_ (\_ -> IOR.modifyIORef counter (\x -> x + 1)) [1..limit]
    threads <- mapM (\_ -> forkIO doWork) [1..threadsCount]
    -- Sorry, it's dirty but time is expensive ...
    threadDelay (15 * 1000 * 1000)
    val <- IOR.readIORef counter
    IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ 
        " but it is " ++ show val) 

withLocking = do
    counter <- atomically (newTVar 0)
    let doWork = 
        mapM_ (\_ -> atomically $ modifyTVar counter (\x -> 
            x + 1)) [1..limit]
    threads <- mapM (\_ -> forkIO doWork) [1..threadsCount]
    threadDelay (15 * 1000 * 1000)
    val <- atomically $ readTVar counter
    IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ 
        " but it is " ++ show val)

好的,有一个逃生门(主要是IORef和FFI),它允许Haskell变得不纯,但如果需要,这个逃生门可以关闭,而Haskell仍然是Haskell。此外,那些被标记为不纯的函数并不影响纯函数。在Haskell中,纯函数是纯的。 - mb14
纯函数在任何地方都是纯的。无论是在 C++、Java、Python 中...而纯函数式语言是禁止非纯功能的。最后,IO 单子是一个大的逃生门,不仅仅是 IORef 和其他类似的东西。 - dev1223
该操作以纯方式进行评估。它是引用透明的。竞争条件发生在执行期间的调度中。你的论点不比这更好:我可以打印一个随机数,因此语言需要是不纯的。正如@sdcvvc的答案所解释的那样,这是错误的。你的操作评估为“创建一个可变计数器,在每个线程中分叉限制:读取该计数器增加一并将新值写入其中”,这总是相同的。在非确定性调度程序上执行它会产生竞争条件。这不是语言的错。 - jan.vogt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接