【问题标题】:Is print in Haskell a pure function?Haskell 中的 print 是纯函数吗?
【发布时间】:2018-04-30 09:04:03
【问题描述】:

Haskell 中的print 是纯函数吗?为什么或者为什么不?我认为不是,因为它并不总是返回与纯函数应该返回的值相同的值。

【问题讨论】:

  • print 返回什么?
  • 所有内置的不纯函数名称中都有unsafe这个词。所以print 是纯粹的。给定相同的参数,它返回相同的结果。
  • @4castle 不一定,通常unsafe是关于功能不全的。
  • @freestyle 好吧,也就是说还有其他函数的名称中也包含unsafe。这与 4castle 所说的并不完全相反。我也不确定是否有任何 builtin 函数使用您所说的约定(只有提到的 4castle 类型的内置函数,如 unsafeInterleaveIOreallyUnsafePtrEquality#)。

标签: haskell io monads io-monad pure-function


【解决方案1】:

如果您只是阅读纯函数的标签(一个函数在给定相同的参数值的情况下总是计算出相同的结果值并且不会导致任何语义上可观察的副作用或输出,例如 mutable 的突变对象或输出到 I/O 设备。)然后思考打印类型:

putStrLn :: String -> IO ()

你会发现一个技巧,它总是返回IO (),所以... ,它会产生效果。所以就Referential Transparency而言不是纯粹的 例如,getLine 返回IO String,但它也是一个纯函数。 (@interjay 贡献),我想说的是,答案非常接近问题:

就值而言,IO () 将始终是相同输入的相同 IO () 值。

就执行而言,它不是纯粹的,因为执行 IO () 可能有副作用(在屏幕上放一个字符串,在这个 案子看起来很无辜,但有些信息官可能会吃核弹,而且 然后返回 Int 42)

您可以在这里使用@Ben 的好方法更好地理解:

“有几种方法可以解释你是如何“纯粹”操纵的 现实世界。一种是说IO就像一个状态单子,只是 穿过的状态是你之外的整个世界 program;= (所以你的 Stuff -> IO DBThing 函数真的有一个额外的 接收世界的隐藏参数,实际上返回一个 DBThing与另一个世界;它总是以不同的方式调用 世界,这就是为什么它甚至可以返回不同的 DBThing 值 当用相同的东西调用时)。另一种解释是一个 IO DBThing value 本身就是一个命令式程序;你的 Haskell 程序是 一个完全没有 IO 的纯函数,它返回一个不纯的程序 执行 IO,而 Haskell 运行时系统(不纯粹)执行 程序返回。”

还有@Erik Allik:

所以返回 IO a 类型值的 Haskell 函数实际上不是 在运行时执行的函数——执行了什么 IO 本身就是一个值。所以这些函数实际上是纯粹的,但是 它们的返回值表示非纯计算。

你可以在这里找到他们Understanding pure functions in Haskell with IO

【讨论】:

  • 显然它总是返回IO (),因为这是类型所要求的,但问题是它是否总是返回IO ()相同值。
  • @DamianLattenero "IO () 本身就是一个值" 不,不是。 IO () 类型有许多不同的值,甚至 print 也可以产生该类型的不同值(如果你传递不同的参数)。否则所有打印语句都会打印相同的内容。
  • @HFBrowning 在某些方面,它有点像元编程的一种形式。 Haskell 程序的“纯”部分构造了一个 IO 动作(由较小的 IO 动作组成),然后执行。
  • @DamianLattenero 是的,()() 类型的值,它是 () 类型的唯一值。但是 print 不会给你一个() 类型的值,它给你一个IO () 类型的值,其中有许多(实际上是无限多)不同的值,() 不是其中之一。例如,print "hello"print "world" 产生的值是不同的 - 否则 main = print "hello"main = print "world" 需要表现相同,因为 main 在两者中具有相同的值。
  • 它总是为相同的输入返回相同的 IO 操作,但这与它返回 IO () 的事实无关。例如,getLine 返回IO String,但它也是一个纯函数。
【解决方案2】:

IO Int 类型的值并不是真正的Int。它更像是一张纸,上面写着“嘿,Haskell 运行时,请以这样的方式产生一个Int 值”。这张纸是惰性的并且保持不变,即使运行时最终产生的Ints 不同。

您通过将纸张分配给main 将其发送到运行时。如果IO 操作永远不会妨碍main,而是在某个容器中停滞不前,它将永远不会被执行。

返回IO 动作的函数和其他函数一样是纯函数。他们总是返回同一张纸。运行时如何处理这些指令是另一回事。

如果他们不纯,我们在改变之前必须三思而后行

foo :: (Int -> IO Int) -> IO Int
foo f = liftA2 (+) (f 0) (f 0)

到:

foo :: (Int -> IO Int) -> IO Int
foo f = let x = f 0 in liftA2 (+) x x

【讨论】:

  • 在运行时,IO XX 确实以相同的方式表示。只是编译器知道IO X 可能如何表现的特殊事情(以及关于纯值如何表现的特殊事情——可以违反的假设!);它生成使用这些假设优化的代码。将 IO 视为指令(即“纸片”)的观点仍然有用;但不应将其视为与“运行时的值”有关,而是与“作为逻辑对象的值”有关。
  • @user2407038 至少在 GHC 中,IO Xa very different representationX 多。
【解决方案3】:

是的,print 是一个纯函数。它返回的值的类型为IO (),您可以将其视为输出您传入的字符串的一堆代码。对于您传入的每个字符串,它总是返回相同的代码。

【讨论】: