最近,我开始学习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();
}
}
我确定我会忽略一些非常简单的内容,或者会误解他的术语。任何帮助将不胜感激。
编辑:
谢谢大家的回答!您的回答使我对这些概念有了更好的理解。我认为还没有完全点击它,但是以后我将重新讨论该主题,谢谢!
答案 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,则需要深入了解该概念。