質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.37%
Haskell

Haskellは高い機能性をもった関数型プログラミング言語で、他の手続き型プログラミング言語では難しいとされている関数でも容易に行うことができます。強い静的型付け、遅延評価などに対応しています。

プログラミング言語

プログラミング言語はパソコン上で実行することができるソースコードを記述する為に扱う言語の総称です。

関数型プログラミング

関数型プログラミングとは、関数を用いて演算子を構築し、算出し、コンピュータプログラムを構成する枠組みです。

意見交換

クローズ

7回答

728閲覧

Haskellは参照透過か?

Aky

総合スコア0

Haskell

Haskellは高い機能性をもった関数型プログラミング言語で、他の手続き型プログラミング言語では難しいとされている関数でも容易に行うことができます。強い静的型付け、遅延評価などに対応しています。

プログラミング言語

プログラミング言語はパソコン上で実行することができるソースコードを記述する為に扱う言語の総称です。

関数型プログラミング

関数型プログラミングとは、関数を用いて演算子を構築し、算出し、コンピュータプログラムを構成する枠組みです。

1グッド

0クリップ

投稿2024/06/14 22:12

1

0

テーマ、知りたいこと

Haskellはよく参照透過であるとか、副作用を持たないといわれています。しかし、標準入力系の関数(getLine等)は副作用があるとまではいかなくても、参照透過ではないんじゃないでしょうか?
どのような立場の意見でも歓迎です。おそらく唯一解はないので、皆さんがどう考えるのかを知りたいです。IOモナド以外はまだ勉強していないので、IOモナドに限った議論をして頂けるとありがたいです。

背景、状況

少し前からHaskellを勉強し始めた大学生です。今までは主に手続き型言語(C++, Javaなど)を勉強してきて、関数型言語を勉強するのは初めてです。プログラミング中級者くらいだと思います。
テーマは上記の通り、Haskellは参照透過なのか、Haskellに副作用はあるのか、です。よりテーマを限定するとすれば、getLine関数が参照透過であることの説明は何なのかを知りたいです。
個人的に調べた範囲では、IO系の処理はIOモナドが担っており、例えば"Hello world!"を出力するときは、関数が出力するわけではなくて、「"Hello world!"を出力するという命令」を関数が返している、といった説明を見かけました。しかしこれは出力に限った話で、入力のことには何も触れていません。
同じ考え方をすれば、getLine関数は「文字列を読み込むという命令」を返していることになります。しかし、モナドは値を格納しているそうなので(これも別のサイトで読みました)、例えば以下のコードでは、一行目のgetLineと二行目のgetLineの返り値は異なる値を格納している可能性がある、ということになります。これは、参照透過性を持たないということにならないでしょうか? 

haskell

1main = do 2 str1 <- getLine 3 str2 <- getLine 4 print str1 5 print str2

あるいは、Haskellは参照透過だが、参照透過性を持たない関数を使うための機能がモナドなのでしょうか? とあるサイトでは、GHC.Base.unIO関数を使うことで、IOモナド内にある隠された関数を取り出すことができると説明されていました。もしモナドが参照透過性のない関数をラッピングして、Haskellには参照透過であるように見せているのだとしたら、ある程度納得できます。
個人的に調べた範囲でも意見が割れていたので、どのような意見でも歓迎です。そもそも関数型言語と手続き型言語では「副作用」という言葉の意味が違うという意見も目にしたので、これに関して説明してくださるとなおありがたいです。
あと、今回初めてteratailを使うのですが、参考にしたサイトURLは貼ったほうがよいのでしょうか?(他人の物を盗んでいるような気分になって途中でやめたのですが...)

igrep👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

回答7

#1

actorbug

総合スコア2381

投稿2024/06/14 23:47

編集2024/06/15 06:03

まず、IOモナドというのは、mainの戻り値として返さなければ、何の副作用も発生しない単なるデータにすぎないということを理解してください。
例えば、以下のようなコードを書いても、入力待ちになることなく1が表示されます。

haskell

1main = print $ length [getLine]

getLineが評価されていないからではないかと思う人もいるかもしれませんが、seqで明示的に評価しても、入力待ちになることはありません。

haskell

1main = print $ getLine `seq` length [getLine]

あとは、Haskellの処理系が、mainを以下のように処理していると考えれば、「Haskellの世界で」の評価は参照透過だとみなせます。

  1. 値がIOモナドによるコマンドだった場合は、「Haskellの外で」コマンドを実行する
  2. 値がa >>= fという形だったら、まずaを「Haskellの世界で」評価し、それで返ってきたIOモナドによるコマンドを「Haskellの外で」実行し、その結果を引数に「Haskellの世界で」fを呼び出す
  3. 値がa >> bという形だったら、まずaを「Haskellの世界で」評価し、それで返ってきたIOモナドによるコマンドを「Haskellの外で」実行し、その結果を無視してbを「Haskellの世界で」評価する

「Haskellの世界で」コマンドの生成だけを行って、そのコマンドの実行を「Haskellの外で」行うことで、副作用がHaskellの世界に入り込むことを防いでいるのです。

質問のコードは、doを使わずに書くと以下のようになります。

haskell

1main = getLine >>= (\str1 -> getLine >>= (\str2 -> print str1 >> print str2))

これを先ほどの処理に通すと、以下の動作になります。

  1. まず、>>=の前のgetLineを「Haskellの世界で」評価し、「一行読み込むというコマンド」が返る
  2. 「一行読み込むというコマンド」を「Haskellの外で」実行する
  3. その結果を x とすると、(\str1 -> getLine >>= (\str2 -> print str1 >> print str2)) xが「Haskellの世界で」評価され、getLine >>= (\str2 -> print x >> print str2)が返る
  4. また>>=が現れたので、その前のgetLineを「Haskellの世界で」評価し、「一行読み込むというコマンド」が返る
  5. 「一行読み込むというコマンド」を「Haskellの外で」実行する
  6. その結果を y とすると、(\str2 -> print x >> print str2) yが「Haskellの世界で」評価され、print x >> print yが返る
  7. >>が現れたので、その前のprint xを「Haskellの世界で」評価し、「x を出力するというコマンド」が返る
  8. 「x を出力するというコマンド」を「Haskellの外で」実行する
  9. その結果を無視して、print yを「Haskellの世界で」評価し、「y を出力するというコマンド」が返る
  10. 「y を出力するというコマンド」を「Haskellの外で」実行する

上記の 1.と4.でgetLineを評価した結果が、どちらも「一行読み込むというコマンド」という同一の値である(=参照透過である)ことに注意してください。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#2

Aky

総合スコア0

投稿2024/06/15 07:01

#1
actorbugさん、ご丁寧な回答ありがとうございます。
僕自身、手続型言語に慣れているのでどうしても手続型の考え方を持ち込んでしまうのですが、なんとなく理解できたように思います。
やはりIOモナドはただの「読み込むという命令」なんですね。僕が勘違いしていたのは、IOモナド自体が入力値をもつ、というところだったようです。あくまで値は外部で読み込んでいるのであれば、確かに参照透過ですね。
ということは、Haskellの参照透過性に関して意見が割れているのはただの知識不足か前提条件が違うからであって、関数型言語にある程度詳しいプログラマーの間では、Haskellは参照透過であるという意見で一致しているのでしょうか?
それはともかく、とても参考になりました。ありがとうございます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#3

actorbug

総合スコア2381

投稿2024/06/15 11:28

ということは、Haskellの参照透過性に関して意見が割れているのはただの知識不足か前提条件が違うからであって、関数型言語にある程度詳しいプログラマーの間では、Haskellは参照透過であるという意見で一致しているのでしょうか?

そもそも、Haskellの参照透過性を否定しているサイトを私は見たことがないです。
知っているなら、いくつかURLを教えてもらえませんか?

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#4

Aky

総合スコア0

投稿2024/06/15 11:52

https://togetter.com/li/26809
僕はこのサイトでの議論を見て、意見が割れているんだなあと思いました。正確には、ここで議論されているのは参照透過性というよりは、副作用についてでした。申し訳ありません。
しかし、議論の中で参照透明性についても言及されています。意見が食い違っているのはおそらく、そもそも手続き型言語と関数型言語では副作用の定義が異なっている(手続型言語では入出力自体が副作用である)のが理由だと思います。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#5

tamoto

総合スコア4228

投稿2024/06/17 07:39

こんにちは。

すでに結論は出ているようですが、Haskell のアイデアなどについて軽く書いておきます。

まず、Haskell は100%参照透明だと考えて良いです。
参照透明性については、「そのシンボルを定義 (実装) や実行結果と置き換えても、プログラムの動作が決して変化しない」という点から分かります。
Haskell でコードを書いている限り、任意の変数の参照は、その中身の値を直接書いても動作は変化せず、任意の関数の呼び出しは、その箇所に関数による計算結果を直接書いたとしても問題なく動作します。

getLine は単なる IO String という型を持つ値でしかなく、コード上で参照する限りは副作用などを起こすことはありません。
getLine は「一行読み込むという命令データ (実行はまだしてない)」というイメージですが、これを「一行読み込む関数」のように考えてしまうと間違いになります。なぜなら、このデータは引数を持たない IO String という値でしかなく、関数ではないからです。
例えばこの getLine を3回連続で呼び出したとして、それは「一行読み込む命令を3回連続で行うという命令」でしかないわけです。
質問にあるコードは、「getLine という命令を2回処理して、取得された文字列をそれぞれ出力する」という命令であるので、すなわち、この手順書には副作用が含まれていないのです。

今まで述べた話では全て、「命令を組み合わせて命令を作る」となっていることに気づいていると思います。
この命令というは IO のことで、つまり Haskell では IO を組み合わせて IO を作ることができ、逆にそれ以外のことができないのです。
命令が命令である間は常に参照透明であり、その命令をコード上で遂行することは誰にもできません。

では、その命令が実行されるのはいつか?
それはプログラムの実行時しかありません。
プログラムの実行時には、「環境」という唯一無二の引数が入るため、実行する度に異なる結果が出ることがあるかもしれません。
それは、Haskell のプログラムというたった一つの巨大な関数を実行する際に、「同じ引数を入れることは二度とできない」という点で差別化されているのです。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#6

Aky

総合スコア0

投稿2024/06/17 09:37

ご意見ありがとうございます。

「そのシンボルを定義 (実装) や実行結果と置き換えても、プログラムの動作が決して変化しない」

この視点はありませんでした。確かに、数学の関数でも同じことが言えます。やはり、手続き型の関数と関数型の関数は少し意味合いが異なるようですね。

tamotoさんのご意見を読んで思ったのですが、Haskellにおいての入出力関数は、関数というよりはIOモナド型の変数に近いのでしょうか?(そもそもHaskellでは変数と関数の境界はあいまいなようですが...)

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#7

tamoto

総合スコア4228

投稿2024/06/17 10:51

#6
そうですね。
Haskell では実際の手続きを扱うことはできず、代わりに「手続きについて書かれた手順書」を扱うので、一段階抽象化されているイメージを持つと良いです。
手順書を組み合わせて大きな手順書を作っても、それは手続きではなく手順書のままなのです。

関数というよりはIOモナド型の変数に近いのでしょうか?

このイメージは正しいと思います。正確には、ただの「値」でしかない、と言うべきかもしれません。

ところで、「IO モナド」という呼び方は少し混乱を招くと思っていて、IO はただの IO というデータ型でしかなく、それをモナドの性質で合成できるというだけなので、別の概念を同時に扱ってしまいやすいです。
モナドというのは a -> m b のような、* -> m * の形の関数をいくらでも合成して、たった一つの * -> m * という関数に押しつぶすことができる「性質」のことを指します。
この m が IO である場合を考えると、IO を返す関数、すなわち、手順書 (+ パラメータ) をいくらでも合成して、たった一つの IO を返す関数にできる (手順書を組み合わせてたった一つの大きい手順書を作れる) ことを意味します。
Haskell は main の型が IO であるということは、言い換えると「手順書を作るプログラミング言語」とも言えるのです。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

最新の回答から1ヶ月経過したため この意見交換はクローズされました

意見をやりとりしたい話題がある場合は質問してみましょう!

質問する

関連した質問