Haskellの純粋な関数と副作用を理解する-putStrLn


10

最近、関数型プログラミングに関する知識を広げたかったので、Haskellを学び始めました。これまで、本当にそれを愛していると言わざるを得ません。現在使用しているリソースは、Pluralsightのコース「Haskell Fundamentals Part 1」です。残念ながら、私は次のコードに関する講師の特定の引用を理解するのにいくつかの困難があり、皆さんがトピックにいくつかの光を当てることができることを望んでいました。

付随するコード

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

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

見積もり

doブロックで同じIOアクションが複数回ある場合、それは複数回実行されます。したがって、このプログラムは文字列「Hello World」を3回出力します。この例は、それputStrLnが副作用のある関数ではないことを示しています。変数putStrLnを定義するために関数を1回呼び出しhelloWorldます。putStrLn文字列を出力する副作用がある場合、文字列は1回だけ出力さhelloWorldれ、メインのdoブロックで繰り返される変数は何の効果もありません。

他のほとんどのプログラミング言語では、putStrLn関数が呼び出されたときに印刷が行われるため、このようなプログラムは「Hello World」を一度だけ印刷します。この微妙な違いは、初心者を誤解させることが多いため、これについて少し考えて、このプログラムが「Hello World」を3回印刷する理由と、putStrLn関数が副作用として印刷を行った場合に1回だけ印刷される理由を理解していることを確認してください。

わからないこと

私にとっては、「Hello World」という文字列が3回表示されるのはほぼ自然なことです。私はhelloWorld変数(または関数?)を、後で呼び出される一種のコールバックとして認識します。私が理解していないのは、putStrLn副作用があった場合に文字列が1回だけ出力されるということです。または、なぜ他のプログラミング言語で一度だけ印刷されるのか。

C#コードで、次のようになっているとしましょう。

C# (フィドル)

using System;

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

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

私はかなり単純なものを見落としているか、彼の用語を誤って解釈していると確信しています。どんな助けでも大歓迎です。

編集:

ご回答ありがとうございます!あなたの回答は、これらの概念をよりよく理解するのに役立ちました。まだ完全にクリックされているとは思いませんが、今後このトピックを再検討します。ありがとうございました。


2
helloWorldC#のフィールドや変数などの定数であると考えてください。に適用されているパラメータはありませんhelloWorld
カラミエル

2
putStrLn 副作用はありません。それは単にIOアクションを返します。何回呼び出しても、引数に対する同じ IOアクションです。"Hello World"putStrLn
chepner

1
もしそうならhelloworld、印刷するアクションではないでしょうHello world。それによって返される値であろうputStrLn 、それがプリントHello World(すなわち、())。
chepner

2
この例を理解するには、Haskellで副作用がどのように機能するかをすでに理解している必要があります。それは良い例ではありません。
user253751

C#スニペットでは、好きではありませんhelloWorld = Console.WriteLine("Hello World");。あなただけ含まれているConsole.WriteLine("Hello World");中でHelloWorld機能実行されるたびにHelloWorld呼び出されます。helloWorld = putStrLn "Hello World"では、何が原因かを考えてみましょうhelloWorld。を含むIOモナドに割り当てられます()。バインドすると、>>=それだけでそのアクティビティ(何かを出力する)が実行さ()れ、バインド演算子の右側に表示されます。
Redu、

回答:


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;
}

WriteLineつまり、C#では、引数を出力して何も返しません。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が一般的な制御構造を実際に必要としない理由の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;
    }
}

これは一度だけ印刷されます。


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
unsafePerformIOHaskellの学習のこの段階では絶対に使用してはならないことに注意してください。理由のために名前に「安全でない」とあり、コンテキストでの使用の影響を慎重に検討することができる(そして実際に行った)場合を除き、使用しないでください。danidiazが回答に組み込んだコードは、から発生する可能性のある直感的でない動作を完全に捉えていますunsafePerformIO
Andrew Ray

1

注意すべき点が1つあります。putStrLnを定義するときに、関数を1回だけ呼び出しますhelloWorld。でmain機能しますが、まさにそれの戻り値を使用putStrLn "Hello, World"3回。

講師は、putStrLn電話には副作用がなく、それは本当だと言います。しかし、タイプを見てくださいhelloWorld。これはIOアクションです。putStrLn作成してください。後で、それらの3つをdoブロックでチェーンして、別のIOアクションを作成します- main。その後、プログラムを実行すると、そのアクションが実行され、そこに副作用があります。

this- monadsのベースにあるメカニズム。この強力なコンセプトにより、副作用を直接サポートしていない言語での印刷など、いくつかの副作用を使用できます。いくつかのアクションをチェーンするだけで、そのチェーンはプログラムの開始時に実行されます。Haskellを真剣に使用したい場合は、その概念を深く理解する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.