web-dev-qa-db-ja.com

純粋に関数型言語についての誤解?

私はしばしば次のステートメント/引数に遭遇します:

  1. 純粋な関数型プログラミング言語は副作用を許可しません(したがって、たとえば外部の世界と相互作用する場合など、有用なプログラムには副作用があるため、実際にはほとんど役に立ちません)。
  2. 純粋な関数型プログラミング言語では、状態を維持するプログラムを作成できません(多くのアプリケーションでは状態を必要とするため、プログラミングが非常に厄介になります)。

私は関数型言語の専門家ではありませんが、これまでこれらのトピックについて私が理解したことをここに示します。

ポイント1に関しては、純粋に関数型の言語で環境と対話できますが、副作用を導入するコード(関数)を明示的にマークする必要があります(たとえば、モナディック型を使用してHaskellで)。また、私が知る限り、副作用による計算(破壊的なデータの更新)も可能です(モナディック型を使用していますか?)。

ポイント2については、私が知る限り、いくつかの計算ステップ(Haskellでも、モナド型を使用)で値をスレッド化することで状態を表すことができますが、これを行う実際的な経験はなく、理解が曖昧です。

では、上記の2つのステートメントはある意味で正しいのですか、それとも純粋に関数型言語についての誤解なのでしょうか?彼らが誤解である場合、彼らはどのようにして生まれましたか? (1)副作用を実装し、(2)状態のある計算を実装するHaskellの慣用的な方法を示す(おそらく小さい)コードスニペットを記述できますか?

39
Giorgio

この答えの目的のために、私は「純粋に関数型の言語」を、関数が参照的に透過的である関数型言語を意味するように定義します。つまり、同じ引数で同じ関数を複数回呼び出すと、常に同じ結果が生成されます。これは、純粋に関数型言語の通常の定義だと思います。

純粋な関数型プログラミング言語は副作用を許可しません(したがって、たとえば外部の世界と相互作用する場合など、有用なプログラムには副作用があるため、実際にはほとんど役に立ちません)。

参照の透明性を実現する最も簡単な方法は、実際には副作用を許可しないことであり、実際にそうなっている言語(主にドメイン固有のもの)があります。ただし、これが唯一の方法ではないことは確かであり、最も一般的な目的の純粋な関数型言語(Haskell、Cleanなど)は副作用を許容します。

また、副作用のないプログラミング言語は実際にはほとんど使用されないというのは、実際には公平ではないと思います。確かに、ドメイン固有の言語ではありませんが、汎用言語であっても、副作用を提供しなくても言語は非常に役立つと思います。おそらくコンソールアプリケーションには向かないかもしれませんが、たとえば、機能的な反応パラダイムでの副作用なしにGUIアプリケーションをうまく実装できると思います。

ポイント1に関しては、純粋に関数型の言語で環境と対話できますが、それらを導入するコード(関数)を明示的にマークする必要があります(たとえば、モナディック型を使用してHaskellで)。

それはそれを単純化することより少しです。副作用のある関数をそのようにマークする必要があるシステム(C++のconst-correctnessに似ていますが、一般的な副作用がある)だけでは、参照の透明性を確保するには不十分です。プログラムが同じ引数で関数を複数回呼び出して、異なる結果が得られないようにする必要があります。あなたはreadLineのようなものを関数ではないものにすることでそれをすることができます(それはHaskellがIOモナドで行うことです))、またはあなたがサイドを呼び出すことを不可能にすることができます-同じ引数で複数回関数に影響を与える(それがCleanが行うことです。)後者の場合、コンパイラーは副作用関数を呼び出すたびに新しい引数でそれを確実に行い、プログラムが拒否されます。同じ引数を副作用のある関数に2回渡します。

純粋な関数型プログラミング言語では、状態を維持するプログラムを作成できません(多くのアプリケーションでは状態を必要とするため、プログラミングが非常に厄介になります)。

繰り返しになりますが、純粋に関数型の言語では変更可能な状態を許可しない場合がありますが、上記の副作用で説明したのと同じ方法で実装すると、純粋でありながら変更可能な状態を維持できる可能性があります。本当に変更可能な状態は、副作用のもう1つの形式です。

とは言っても、関数型プログラミング言語は間違いなく変更可能な状態を阻止します。純粋なものは特にそうです。そして、それがプログラミングを厄介なものにすることはないと思います-全く逆です。場合によっては(ただし、それほど頻繁ではありませんが)パフォーマンスや明瞭さを失うことなく変更可能な状態を回避できない場合もあります(そのため、Haskellのような言語には変更可能な状態の機能があります)。

彼らが誤解である場合、彼らはどのようにして生まれましたか?

多くの人は単に「関数が同じ引数で呼び出されたときに同じ結果を生成する必要がある」と読み、readLineのようなものや変更可能な状態を維持するコードを実装することは不可能であると結論づけています。したがって、純粋な関数型言語が参照の透明性を損なうことなくこれらのものを導入するために使用できる「チート」を単に認識していません。

また、ミュータブルな状態は関数型言語では大いに落胆するため、純粋な関数型ではまったく許可されていないと想定することはそれほど大きな進歩ではありません。

(1)副作用を実装し、(2)状態のある計算を実装するHaskellの慣用的な方法を示す(おそらく小さい)コードスニペットを記述できますか?

これは、ユーザーに名前を要求して挨拶するPseudo-Haskellのアプリケーションです。 Pseudo-Haskellは、私が考案したばかりの言語で、HaskellのIOシステムですが、より従来の構文、よりわかりやすい関数名を使用し、do- notation( IOモナドがどのように機能するか)をそらすだけです:

_greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
_

ここでの手掛かりは、readLineが_IO<String>_型の値であり、composeMonadが_IO<T>_型の引数を取る関数であることです(一部の型T)およびT型の引数を取り、_IO<U>_型の値を返す関数である別の引数(一部の型Uの場合)。 printは、文字列を取り、_IO<void>_型の値を返す関数です。

タイプ_IO<A>_の値は、タイプAの値を生成する特定のアクションを「エンコード」する値です。 composeMonad(m, f)は、IOのアクションとそれに続くf(x)のアクションをエンコードする新しいm値を生成します。ここで、xは値は、mのアクションを実行することによって生成されます。

変更可能な状態は次のようになります。

_counter = mutableVariable(0)
increaseCounter(cnt) =
    setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
    composeMonad(getValue(cnt), setIncreasedValue)

printCounter(cnt) = composeMonad( getValue(cnt), print )

main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
_

ここでmutableVariableは、任意の型Tの値を取り、_MutableVariable<T>_を生成する関数です。関数getValueMutableVariableを取り、現在の値を生成する_IO<T>_を返します。 setValueは_MutableVariable<T>_とTを取り、値を設定する_IO<void>_を返します。 composeVoidMonadcomposeMonadと同じですが、最初の引数が意味のある値を生成しないIOであり、2番目の引数が別のモナドであり、モナド。

Haskellには、この全体的な試練をそれほど痛くないようにする構文上の砂糖がありますが、変更可能な状態が言語が本当にあなたに望まないものであることはまだ明らかです。

26
sepp2k

純粋な言語と純粋な関数の間に違いがあるため、私見で混乱しています。関数から始めましょう。関数はpure(同じ入力が与えられた場合)が常に同じ値を返し、観察可能な副作用を引き起こさない場合。典型的な例は、f(x) = x * xのような数学関数です。次に、この関数の実装について考えます。一般的に純粋な関数型言語と見なされていない言語でも、ほとんどの言語では純粋です。 ML。この動作をするJavaまたはC++メソッドでさえ、純粋と見なすことができます。

では、純粋な言語とは何でしょうか?厳密に言えば、純粋な言語では純粋でない関数を表現できないと考える人もいるかもしれません。これを理想的な定義を純粋な言語と呼びましょう。このような動作は非常に望ましいです。どうして?純粋な関数のみで構成されるプログラムの良い点は、プログラムの意味を変更せずに関数アプリケーションをその値に置き換えることができることです。結果がわかったら、それが計算された方法を忘れることができるので、これはプログラムについて非常に簡単に推論します。純度により、コンパイラーは特定の積極的な最適化を実行することもできます。

では、内部状態が必要な場合はどうでしょうか?計算前の状態を入力パラメーターとして追加し、計算後の状態を結果の一部として追加するだけで、純粋な言語で状態を模倣できます。 _Int -> Bool_の代わりに、Int -> State -> (Bool, State)のようなものが得られます。依存関係を明示的にするだけです(これは、どのプログラミングパラダイムでも良い習慣と見なされています)。ところで、そのような状態模倣関数をより大きな状態模倣関数に組み合わせる特にエレガントな方法であるモナドがあります。このようにして、純粋な言語で「状態を維持」することができます。しかし、それを明示的にする必要があります。

これは、外部と対話できることを意味しますか?結局、有用なプログラムは、有用であるために現実の世界と相互作用しなければなりません。しかし、入力と出力は明らかに純粋ではありません。特定のバイトを特定のファイルに書き込むのは、初めてで問題ない場合があります。ただし、まったく同じ操作をもう一度実行すると、ディスクがいっぱいであるためエラーが返される場合があります。明らかに、ファイルに書き込むことができる(理想的な意味での)純粋な言語は存在しません。

したがって、私たちはジレンマに直面しています。ほとんど純粋な関数が必要ですが、いくつかの副作用は絶対に必要であり、それらは純粋ではありません。これで純粋な言語の現実的な定義は、純粋な部分を他の部分から分離する何らかの手段が必要になるということです。このメカニズムは、不純な操作が純粋な部品に潜入することがないようにする必要があります。

Haskellでは、これはIOタイプで行われます。 IOの結果を破棄することはできません(安全でないメカニズムがない場合)。したがって、IOモジュール自体で定義されている関数を使用して、IOの結果のみを処理できます。幸いにも、非常に柔軟なコンビネータがあり、IO結果を取得して、その関数が別のIO結果を返す限り、その結果を関数で処理できます。このコンビネータはbind(または_>>=_)と呼ばれ、型はIO a -> (a -> IO b) -> IO bです。この概念を一般化すると、モナドクラスに到達し、IOがそのインスタンスになります。

16
scarfridge