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

时间:2019-11-25 15:43:24

标签: haskell

最近,我开始学习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有打印字符串的副作用,那么它只会打印一次,并且在主do块中重复的helloWorld变量不会有任何效果。

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

我不了解的

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

让我们用C#代码说吧,我想它看起来像这样:

C# (Fiddle)

using System;

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

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

我确定我会忽略一些非常简单的内容,或者会误解他的术语。任何帮助将不胜感激。

编辑:

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

4 个答案:

答案 0 :(得分: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操作是“真实”值,不需要将其包装在其他“回调”中或阻止其执行的任何种类,而是的唯一方法 使它们执行是将它们放在特定位置(即main内的某个地方或main产生的线程)。

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

答案 1 :(得分: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;
    }
}

只打印一次。

答案 2 :(得分: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动作会“弹出”“隐藏”效果,而随后的动作只会评估生成的(),不会造成任何进一步的影响。

答案 3 :(得分:1)

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

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

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