Haskell真的是纯粹的(任何处理系统外输入和输出的语言)?

WeN*_*ers 35 monads haskell functional-programming referential-transparency

在功能编程方面触及Monads之后,该功能是否实际上使语言变得纯粹,或者它是否只是另一个"从监狱免费卡中获取"来推理现实世界中的计算机系统,在黑板数学之外?

编辑:

这不是有人在这篇文章中所说过的火焰诱饵,而是一个真正的问题,我希望有人可以用枪击我说,证明,这是纯粹的.

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

sdc*_*vvc 74

采用以下迷你语言:

data Action = Get (Char -> Action) | Put Char Action | End
Run Code Online (Sandbox Code Playgroud)

Get f意思是:读取一个字符c,然后执行操作f c.

Put c a意思是:写字符c,并执行操作a.

这是一个打印"xy"的程序,然后请求两个字母并以相反的顺序打印它们:

Put 'x' (Put 'y' (Get (\a -> Get (\b -> Put b (Put a End)))))
Run Code Online (Sandbox Code Playgroud)

你可以操纵这样的程序.例如:

conditionally p = Get (\a -> if a == 'Y' then p else End)
Run Code Online (Sandbox Code Playgroud)

这是类型Action -> Action- 它需要一个程序,并提供另一个程序,首先要求确认.这是另一个:

printString = foldr Put End
Run Code Online (Sandbox Code Playgroud)

这有类型String -> Action- 它需要一个字符串并返回一个写入字符串的程序,如

Put 'h' (Put 'e' (Put 'l' (Put 'l' (Put 'o' End)))).

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

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

显然Get,Put不仅是选项,您还可以向IO数据类型添加许多其他API调用,例如对文件或并发操作.

添加结果值

现在考虑以下数据类型.

data IO a = Get (Char -> Action) | Put Char Action | End a
Run Code Online (Sandbox Code Playgroud)

Action一种类型相当于IO (),即始终返回"单位"的IO值,与"void"相当.

这种类型与Haskell IO非常相似,仅在Haskell IO中是一种抽象数据类型(您无法访问定义,只能访问某些方法).

这些是IO操作,可以以某些结果结束.像这样的值:

Get (\x -> if x == 'A' then Put 'B' (End 3) else End 4)
Run Code Online (Sandbox Code Playgroud)

有类型IO Int并且对应于C程序:

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

评估和执行

评估和执行之间存在差异.您可以评估任何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)
Run Code Online (Sandbox Code Playgroud)

你得到:

Put 'A' (End 0)
Run Code Online (Sandbox Code Playgroud)

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

你会怎么翻译?

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

进入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))
Run Code Online (Sandbox Code Playgroud)

现在,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)
Run Code Online (Sandbox Code Playgroud)

IO动作元组:首先读取一个字符并将其打印为大写,然后读取一个字符并将其返回小写.

 Get (\x -> End (Put x (End 0))) :: IO (IO Int)
Run Code Online (Sandbox Code Playgroud)

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

Haskell具有特殊功能,可以轻松操作IO值.例如:

 sequence :: [IO a] -> IO [a]
Run Code Online (Sandbox Code Playgroud)

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

单子

Monads是一些组合器(conditionally如上所述),它允许您更结构地编写程序.有一个由类型组成的函数

 IO a -> (a -> IO b) -> IO b
Run Code Online (Sandbox Code Playgroud)

给定IO a,函数a - > IO b,返回类型IO b的值.如果你把第一个参数写成C函数a f(),第二个参数b g(a x)返回一个程序g(f(x)).鉴于Action/IO的上述定义,您可以自己编写该函数.

注意monad对纯度不是必不可少的 - 你可以像我上面那样编写程序.

纯度

关于纯度的基本要素是参考透明度,并区分评估和执行.

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

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

  • @Mehrdad:如果它执行IO,那么它的类型为IO Int(或类似),你不能用(+)添加它,因为(+)需要数字.你必须写"执行fx,执行fx,添加结果",如`do a < - fx; b < - fx; 返回(a + b)`.有一个更高阶的函数:`liftM2(+)(fx)(fx)`. (17认同)
  • 参考透明度,我喜欢.听起来比纯粹和不纯的纳粹术语更加聪明.有了像后者这样的术语,我觉得我在哈利波特作为一个额外的主演.很好的答案. (8认同)
  • @mljrg:你不能重新排列IO动作`putStr"hello">> putStr"world"`就像你不能重新排列列表`[1,2]`到`[2,1]`一样.但是,如果`[1,2]`或`putStr"hello">> putStr"world"`是一个复杂计算的结果,你可以优化该计算,指导这些优化的原则是相同的,无论是否您正在计算列表或IO操作.例如,`fx + fx`可以优化为'2*fx`,而在C中你必须知道`f`没有副作用. (2认同)
  • @mljrg:Haskell中的规则不必像"这个函数没有副作用"这样的规定,因为像打印这样的效果是一流的值(我的答案中的`Action`),而不是调用函数的副作用.换句话说,原因不是"因为它不纯粹",而是因为"它改变了非交换操作中的排序". (2认同)

Kon*_*lph 9

重要的是要明白,monad没有任何内在的特殊性 - 所以他们绝对不会代表这方面的"走出监狱"牌.实现或使用monad不需要编译器(或其他)魔法,它们是在Haskell的纯功能环境中定义的.特别是,sdcvvc已经展示了如何以纯粹的功能方式定义monad,而没有任何实现后门的资源.


sol*_*ack 6

关于计算机系统"在黑板数学之外"的理由是什么意思?会是什么样的推理?航位推算?

副作用和纯函数是一个观点问题.如果我们将名义上的副作用函数视为将我们从世界的一个状态带到另一个世界的函数,那么它又是纯粹的.

我们可以通过赋予它第二个论点,一个世界,并且要求它在完成时通过我们一个新的世界,使每个副作用函数变得纯粹.我根本不知道C++但是说read有这样的签名:

vector<char> read(filepath_t)
Run Code Online (Sandbox Code Playgroud)

在我们新的"纯粹风格"中,我们这样处理:

pair<vector<char>, world_t> read(world_t, filepath_t)
Run Code Online (Sandbox Code Playgroud)

事实上,这是每个Haskell IO动作的工作原理.

所以现在我们有了一个纯粹的IO模型.谢天谢地.如果我们不能这样做,那么Lambda微积分和图灵机可能不是等同的形式,然后我们会做一些解释.我们还没有完成,但留给我们的两个问题很简单:

  • 是什么在变world_t结构?每一粒沙子,一片草叶,破碎的心和金色的夕阳的描述?

  • 我们有一个非正式的规则,我们只使用一个世界 - 在每次IO操作之后,我们抛弃我们使用它的世界.然而,随着所有这些世界的流动,我们必然会把它们混为一谈.

第一个问题很简单.只要我们不允许检查世界,事实证明我们不必为将任何东西存入其中而烦恼.我们只需要确保一个新的世界不等于任何以前的世界(以免编制者狡猾地优化一些世界生产的操作,就像它有时一样C++).有很多方法可以解决这个问题.

至于世界变得混乱,我们想隐藏在图书馆内传递的世界,这样就无法进入世界,因此无法将它们混淆起来.事实证明,monads是一种在计算中隐藏"侧通道"的好方法.输入IO monad.

前段时间,在Haskell邮件列表上询问了像你这样的问题,我更详细地进入了"侧通道".这是Reddit线程(链接到我原来的电子邮件):

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

  • Lambda微积分是纯粹的,图灵完整.同样,您是否可以设计一个纯粹"执行"代码的机器与用于描述该程序的语言是否纯粹的问题完全无关.你的论证就像说不可能构造一个正方形一样有效,因为你必须能够构造一个无理长度的对角线. (3认同)
  • 这是你自己弥补的问题.在一种语言中描述的程序运行的环境是否"纯粹"与该语言是否纯粹的问题完全无关. (2认同)