了解Haskell中的纯函数和副作用-putStrLn


10

最近,我开始学习Haskell,因为我想扩大我对函数式编程的了解,我必须说到目前为止我真的很喜欢它。我当前使用的资源是Pluralsight上的“ Haskell基础知识第1部分”课程。不幸的是,我在理解讲师关于以下代码的一句话时遇到了一些困难,希望你们能对此话题有所了解。

随附代码

helloWorld :: IO ()
helloWorld = putStrLn "Hello World"

main :: IO ()
main = do
    helloWorld
    helloWorld
    helloWorld

报价单

如果您在一个do块中多次执行相同的IO操作,则它将多次运行。因此,该程序将字符串“ Hello World”输出三遍。本示例有助于说明这putStrLn不是具有副作用的功能。我们只调用putStrLn一次函数来定义helloWorld变量。如果putStrLn有打印字符串的副作用,它将只打印一次,并且helloWorld在主do-block中重复的变量将没有任何效果。

在大多数其他编程语言中,这样的程序只会打印一次“ Hello World”,因为在putStrLn调用函数时会进行打印。这种微妙的区别通常会使初学者不胜其烦,因此请仔细考虑一下,并确保您了解为什么该程序会打印“ Hello World”三遍,以及如果该putStrLn函数作为副作用打印时只打印一次。

我不明白的

对我来说,字符串“ Hello World”被打印三遍似乎是很自然的。我认为helloWorld变量(或函数?)是一种稍后调用的回调。我不明白的是,如果putStrLn产生副作用,它将导致字符串仅打印一次。或者为什么只用其他编程语言只打印一次。

假设在C#代码中,我想它看起来像这样:

C# (提琴)

using System;

public class Program
{
    public static void HelloWorld()
    {
        Console.WriteLine("Hello World");
    }

    public static void Main()
    {
        HelloWorld();
        HelloWorld();
        HelloWorld();
    }
}

我确信我会忽略一些很简单的东西,或者会误解他的术语。任何帮助将不胜感激。

编辑:

谢谢大家的答案!您的回答使我对这些概念有了更好的理解。我认为尚未完全点击,但以后我将重新讨论该主题,谢谢!


2
考虑一下helloWorld常量,例如C#中的字段或变量。没有适用于的参数helloWorld
卡拉米里尔

2
putStrLn 没有副作用;它只是返回一个IO操作,无论调用多少次,该参数都将执行相同的 IO操作。"Hello World"putStrLn
chepner

1
如果是这样的话,那helloworld将不是一个打印出来的动作Hello world;这将是由返回的值putStrLn 印刷Hello World(即,())。
chepner

2
我认为要理解此示例,您已经必须了解Haskell中的副作用是如何工作的。这不是一个很好的例子。
user253751 '19

在您的C#代码段中,您不会喜欢helloWorld = Console.WriteLine("Hello World");。您只需Console.WriteLine("Hello World");HelloWorld每次HelloWorld调用要执行的函数中包含。现在想想是什么helloWorld = putStrLn "Hello World"helloWorld。它被分配给包含的IO monad ()。一旦绑定它,>>=它将仅执行它的活动(打印一些内容),并()在bind运算符的右侧。
Redu

Answers:


8

如果我们定义helloWorld为局部变量,可能更容易理解作者的含义:

main :: IO ()
main = do
  let helloWorld = putStrLn "Hello World!"
  helloWorld
  helloWorld
  helloWorld

您可以将其与类似C#的伪代码进行比较:

void Main() {
  var helloWorld = {
    WriteLine("Hello World!")
  }
  helloWorld;
  helloWorld;
  helloWorld;
}

也就是说,在C#中,WriteLine是一个过程,它输出其参数,但不返回任何内容。在Haskell中,putStrLn是一个函数,它接受一个字符串并为您提供一个将在执行该字符串时打印该字符串的操作。这意味着写作之间绝对没有区别

do
  let hello = putStrLn "Hello World"
  hello
  hello

do
  putStrLn "Hello World"
  putStrLn "Hello World"

话虽这么说,在这个示例中,差异并不是特别深远,所以如果您还没有完全了解作者在本节中要尝试的内容,而现在继续进行下去,那很好。

如果将其与python进行比较,它的效果会更好

hello_world = print('hello world')
hello_world
hello_world
hello_world

这里的关键是,在Haskell的IO动作是不需要被包裹在进一步的“回调”或诸如此类的事做“实”的价值观,以防止它们在执行-更确切地说,只有这样,才能做到让他们执行IS将它们放置在特定的位置(例如,内部某处main或生成的线程main)。

这也不只是一个绝招,它最终会对您的代码编写方式产生一些有趣的影响(例如,这是Haskell确实不需要任何您熟悉的通用控制结构的部分原因)使用命令式语言,并且可以使用功能代替所有操作),但我对此不必太担心(类似这样的类比并不总是会立即单击)


4

如果您使用的是实际执行某项功能的函数,而不是,可能会更容易看到上述差异helloWorld。考虑以下几点:

add :: Int -> Int -> IO Int
add x y = do
  putStrLn ("I am adding " ++ show x ++ " and " ++ show y)
  return (x + y)

plus23 :: IO Int
plus23 = add 2 3

main :: IO ()
main = do
  _ <- plus23
  _ <- plus23
  _ <- plus23
  return ()

这将打印出“我要添加2和3” 3次。

在C#中,您可以编写以下代码:

using System;

public class Program
{
    public static int add(int x, int y)
    {
        Console.WriteLine("I am adding {0} and {1}", x, y);
        return x + y;
    }

    public static void Main()
    {
        int x;
        int plus23 = add(2, 3);
        x = plus23;
        x = plus23;
        x = plus23;
        return;
    }
}

这将只打印一次。


3

如果对的评估putStrLn "Hello World"有副作用,则该消息将仅打印一次。

我们可以使用以下代码来近似该场景:

import System.IO.Unsafe (unsafePerformIO)
import Control.Exception (evaluate)

helloWorld :: ()
helloWorld = unsafePerformIO $ putStrLn "Hello World"

main :: IO ()
main = do
    evaluate helloWorld
    evaluate helloWorld
    evaluate helloWorld

unsafePerformIO采取一个IO动作,然后“忘记”它是一个IO动作,将其从动作的组成通常施加的顺序中解脱出来,IO并根据懒惰评估的变化让效果发生(或不发生)。

evaluate接受一个纯值,并确保在评估结果IO操作时就对值进行评估(对我们而言,它将是因为它位于的路径中)main。我们在这里使用它来将某些值的评估与程序的执行联系起来。

此代码仅打印一次“ Hello World”。我们将其helloWorld视为纯价值。但这意味着它将在所有evaluate helloWorld调用之间共享。那么为何不?毕竟这是一个纯价值,为什么要不必要地重新计算呢?第一个evaluate动作“弹出”“隐藏”效果,随后的动作仅评估结果(),不会引起任何进一步的影响。


1
值得注意的是,unsafePerformIO在学习Haskell的这个阶段,您绝对不应该使用。它的名称中有“不安全”的原因,除非您可以(并且确实)仔细考虑了上下文中使用它的含义,否则不应该使用它。danidiaz回答的代码完美地捕捉到了可能导致的直觉行为unsafePerformIO
安德鲁·雷

1

有一个细节需要注意:您putStrLn在定义时仅调用一次函数helloWorld。在main函数中,您只需使用该返回值的putStrLn "Hello, World"三倍即可。

讲师说该putStrLn电话没有副作用,这是事实。但是看看类型helloWorld-这是IO操作。putStrLn为您创建它。稍后,将其中的3个与该do块链接在一起,以创建另一个IO操作- main。稍后,当您执行程序时,将运行该动作,这就是副作用。

这个monads基础的机制。这个强大的概念使您可以使用一些副作用,例如以不直接支持副作用的语言进行打印。您只需链接一些动作,该链接将在程序启动时运行。如果要认真使用Haskell,则需要深入了解该概念。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.