38

在谈到函数式编程方面的 Monads 之后,该功能是否真的使一种语言变得纯粹,或者它只是黑板数学之外在现实世界中推理计算机系统的另一张“出狱卡”?

编辑:

这不是有人在这篇文章中所说的火焰诱饵,而是一个真正的问题,我希望有人能用它击倒我并说,证明,它是纯粹的。

此外,我正在研究关于其他不那么纯粹的函数式语言和一些使用良好设计并比较纯度的 OO 语言的问题。到目前为止,在我非常有限的 FP 世界中,我还没有理解 Monads 的纯度,你会很高兴知道,但是我确实喜欢不变性的想法,这在纯度赌注中更为重要。

4

8 回答 8

80

采取以下迷你语言:

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不仅仅是选项,您还可以向 IO 数据类型添加许多其他 API 调用,例如对文件进行操作或并发操作。

添加结果值

现在考虑以下数据类型。

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

之前的Action类型等价于IO (),即一个总是返回“unit”的IO值,相当于“void”。

这种类型与 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 中,求值和执行是相同的(即它们表示相同的事情,或者同时发生)。不在哈斯克尔。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)

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

Haskell 具有允许轻松操作 IO 值的特殊功能。例如:

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

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

单子

Monads 是一些组合子(conditionally如上),它们允许您编写更具结构性的程序。有一个由 type 组成的函数

 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 的定义,您可以自己编写该函数。

注意 monads 对纯度来说不是必需的——你总是可以像我上面那样编写程序。

纯度

纯度的本质是参考透明度,以及区分评估和执行。

在 Haskell 中,如果有f x+f x,可以将其替换为2*f x. 在 C中,f(x)+f(x)一般不一样2*f(x),因为f可以在屏幕上打印一些东西,或者修改x.

由于纯度,编译器有更多的自由并且可以更好地优化。它可以重新安排计算,而在 C 语言中,它必须考虑这是否会改变程序的含义。

于 2010-06-25T13:30:30.033 回答
9

It is important to understand that there is nothing inherently special about monads – so they definitely don’t represent a “get out of jail” card in this regard. There is no compiler (or other) magic necessary to implement or use monads, they are defined in the purely functional environment of Haskell. In particular, sdcvvc has shown how to define monads in purely functional manner, without any recourses to implementation backdoors.

于 2010-06-25T14:39:22.773 回答
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++)。有很多方法可以处理这个问题。

至于世界混淆,我们想将经过的世界隐藏在图书馆中,这样就无法进入世界,因此无法混淆它们。事实证明,monad 是在计算中隐藏“侧通道”的好方法。输入 IO 单子。

前段时间,在 Haskell 邮件列表上提出了一个像你这样的问题,我在那里更详细地进入了“旁道”。这是 Reddit 线程(链接到我的原始电子邮件):

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

于 2010-06-26T05:30:01.193 回答
4

对于 sdcwc 的 IO 结构的扩展版本,可以查看 Hackage 上的 IOSpec 包:http: //hackage.haskell.org/package/IOSpec

于 2010-06-25T14:34:55.320 回答
4

我对函数式编程很陌生,但我是这样理解的:

在 haskell 中,您定义了一堆函数。这些函数不会被执行。他们可能会得到评估。

有一个特别需要评估的功能。这是一个产生一组“动作”的常量函数。这些操作包括评估功能和执行 IO 和其他“真实世界”的东西。您可以拥有创建和传递这些操作的函数,除非使用 unsafePerformIO 评估函数或它们由主函数返回,否则它们将永远不会被执行。

所以简而言之,Haskell 程序是一个由其他函数组成的函数,它返回一个命令式程序。Haskell 程序本身是纯粹的。显然,该命令式程序本身不可能。根据定义,现实世界的计算机是不纯的。

这个问题还有很多,其中很多是语义问题(人类,而不是编程语言)。Monad 也比我在这里描述的要抽象一些。但我认为这是一种有用的思考方式。

于 2010-06-25T12:39:37.657 回答
4

我是这样想的:程序必须与外部世界做一些事情才能有用。当您(以任何语言)编写代码时,正在发生(或应该发生)的事情是,您努力编写尽可能多的纯净、无副作用的代码,并将 IO 限制在特定的位置。

我们在 Haskell 中所拥有的是,你被更多地推向这个写作方向,以严格控制效果。在核心和许多库中也有大量的纯代码。Haskell 真正做到了这一点。Haskell 中的 Monad 对很多事情都很有用。它们被用来做的一件事是包含处理杂质的代码。

这种设计方式与极大地促进它的语言一起设计,总体效果是帮助我们产生更可靠的工作,需要更少的单元测试来清楚它的行为方式,并允许通过组合进行更多的重用。

如果我理解你所说的正确的话,我不认为这是假的,或者只是在我们的脑海中,就像一张“出狱免费卡”。这里的好处是非常真实的。

于 2010-06-25T13:12:10.997 回答
1

Haskell 真的很纯净吗?

从绝对意义上说:不。

你运行程序的固态图灵机——Haskell 或其他——是一个状态和效果设备。对于任何程序要使用其所有“功能”,程序将不得不求助于使用 state 和 effects

至于归于该贬义词的所有其他“含义”:

至少可以说,在最显着特征是状态的机器之上假设一个无状态计算模型,似乎是一个奇怪的想法。模型和机械之间的差距很大,因此弥合成本很高。没有任何硬件支持功能可以将这个事实抛在一边:这对于实践来说仍然是一个坏主意。

这在适当的时候也被函数式语言的主角所认可。他们以各种棘手的方式引入了状态(和变量)。纯粹的功能特性因此受到损害和牺牲。旧术语已变得具有欺骗性。

尼克劳斯·沃斯


使用单子类型真的可以使语言变得纯粹吗?

不,这只是使用类型划分的一种方式:

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

您可以改为使用唯一性类型,就像Clean一样...


单子类型的使用只是黑板数学之外的另一张用于推理现实世界中计算机系统的“出狱卡”吗?

考虑到 Haskell 2010 报告IO中给出的类型描述,这个问题具有讽刺意味:

IO类型用作与外部世界交互的操作(动作)的标记。IO类型是抽象的:用户看不到任何构造函数。IOMonadFunctor类的一个实例。

...借用另一个答案的说法:

[…]IO很神奇(有实现但没有外延)[…]

作为抽象,IO类型绝不是“出狱卡”——需要涉及多种语义的复杂模型来解释 Haskell 中 I/O 的工作原理。有关更多详细信息,请参阅:


并非总是这样——Haskell 最初有一个至少部分可见的 I/O 机制。拥有它的最后一个语言版本是Haskell 1.2。那时,类型main是:

main :: [Response] -> [Request]

通常缩写为:

main :: Dialogue

在哪里:

type Dialogue = [Response] -> [Request]

Response尽管数据类型很大,但也Request很谦虚:

基于对话的 I/O 的请求和响应数据类型的精简定义

在 Haskell 中使用单子接口的 I/O 的出现改变了这一切——不再有可见的数据类型,只是一个抽象的描述。因此,如何真正定义IO,return(>>=)现在特定于 Haskell 的每个实现。

(为什么旧的 I/O 机制被放弃了?“解决尴尬的小队”概述了它的问题。)

这些天来,更相关的问题应该是:

正如 Owen Stephens 在Approaches to Functional I/O中指出的那样:

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

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

于 2020-06-25T09:45:28.397 回答
-2

不,不是。IO monad 是不纯的,因为它具有副作用和可变状态(在 Haskell 程序中可能存在竞争条件,所以?嗯……纯 FP 语言不知道诸如“竞争条件”之类的东西)。真正纯粹的 FP 是具有唯一性类型的 Clean,或者具有 FRP(功能反应式编程)而不是 Haskell 的 Elm。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)
于 2015-05-12T12:35:34.527 回答