モナドのないHaskell

haskellを研究するプログラマーは遅かれ早かれモナドのような理解できない概念に出くわすでしょう。 多くの人にとって、この言語の知識はモナドで終わります。 多くのモナドガイドがあり、新しいガイドが常に表示されます(1)。 モナドを理解している少数の人は、知識を慎重に隠し、モナドを内機能と自然変換の観点から説明します(2)。 経験豊富なプログラマーは、世界の確立された絵の中にモナドの場所を見つけることができません。

その結果、JavaプログラマーはHaskellを笑うだけで、100万行のエンタープライズプロジェクトからは目を離しません。 C ++開発者は、超高速アプリケーションにパッチを適用し、さらにスマートなポインターを作成します。 Web開発者は、css、xml、およびjavascriptの例と巨大な仕様に目を通します。 そして、暇なときにhaskellを勉強する人は、モナドと呼ばれる乗り越えられない障害に直面しています。

それで、 モナドなしでHaskellでプログラムする方法を学びます。


これを行うには、少しの空き時間、おやすみなさい、お気に入りの飲み物のマグカップ、ghcコンパイラが必要です。 Windowsおよびmacosでは、haskellプラットフォーム(3)パッケージに含まれています。Linuxユーザーはリポジトリからghcをインストールできます。 Prelude>で始まるサンプルコードは、インタラクティブなインタープリターghciでチェックできます。

ハブにも同様の記事がありました(4)が、I / Oのすべての入出力については説明していませんが、使用できる既製のテンプレートを提供しています。

次のステップに進む-オペレーター


遠くから始めましょう。 Haskellのすべての計算は、「副作用あり」と「副作用なし」に分けられました。 最初の例には、たとえば、入力/出力デバイスからの書き込み/読み取り、途中でエラーが発生する可能性がある計算などが含まれます。 「副作用なし」には、数値の追加、文字列の接着、数学的計算、「純粋な」関数などの操作が含まれます。

純粋な関数は、他のすべてのプログラミング言語の関数が結合するのと同じ方法で結合します。
Prelude> show (head (show ((1 + 1) -2))) '0' 


副作用のあるプログラムを作成するために、特別な演算子が作成されました
 >>= 

それを「接続」(eng。bind)と呼びましょう。 すべてのI / Oアクションはそれらに接着されています。
 Prelude> getLine >>= putStrLn asdf asdf 

この演算子は、副作用のある2つの関数を入力として受け入れ、左の関数の出力は右への入力として機能します。

インタプリタコマンドで関数のタイプを見てみましょう:t:
 Prelude> :t getLine getLine :: IO String Prelude> :t putStrLn putStrLn :: String -> IO () 


そのため、getLineは何も受け入れず、IO String型を返します。

タイプ名に2つの単語が含まれているという事実は、このタイプが複合であることを示しています。 そして、最初に来る言葉は、タイプビルダーと呼ばれます。他のすべてはこのビルダーのパラメーターです(不協和音が聞こえるのは知っていますが、そうすべきです)。

この場合、IOという単語は単なる副作用を意味し、演算子=によって破棄されます。 副作用の他の「指標」の例として、人気のあるタイプStateを挙げることができます。これは、関数が何らかの種類のステートを持つことを意味します。

putStrLnに進みましょう。 この関数は文字列を入力として受け取り、IO()を返します。 IOを使用すると、すべてが明確になり、副作用があり、()はHassalのsyssal voidの類似物です。 つまり 関数は入力/出力で何かを行い、空の値を返します。 ところで、すべてのHaskellプログラムはこのIO()で終了する必要があります。

そのため、「接続」演算子は、最初の引数から結果を取得し、副作用インジケータを切り取り、2番目の引数で発生したことを渡します。 これは複雑に思えますが、Haskellの半分はこの単一のステートメントでサポートされ、すべての入力/出力はそれを使用してプログラムされます。 それは非常に重要であり、言語のロゴに追加されたほどです。

接着された関数の返された値と受け入れられた値が一致しない場合はどうなりますか? ラムダ関数が助けになります。 たとえば、入力としてパラメーターを受け入れるだけですが、何も行いません。
 Prelude> (putStrLn " 1") >>= (\a -> putStrLn " 2") >>= (\b -> putStrLn " 3")  1  2  3 

今後、「=」演算子の優先度は非常に低く、必要に応じて、この例では括弧なしで実行できます。 さらに、この例のように、引数がラムダ関数内で使用されていない場合は、_で置き換えることができます。

最初の例を完全に同等に書き換えますが、ラムダ関数を使用します。
 Prelude> getLine >>= \a -> putStrLn a asdf asdf 

ラムダ関数を使用して画面に文字列を表示するとき、1つの変数を受け入れることを明示的に示し、その使用方法を明示的に記述しました。

「変数」と言いましたか?


はい、変数について話しましょう。 ご存じのように、Haskellには変数はありません。 ただし、リストを見ると、多くの割り当てが表示されます。

上記のコードでは、aとbは変数に非常に似ています。 他の言語と同様に参照することもできます。 ただし、これらのaとbは、命令型言語の変数とは大きく異なります。

すべての命令型プログラミング言語では、変数は名前付きのメモリ領域です。 Haskellでは、aやbのようなものは名前付きの式と値です。

例を挙げて、これらの違いを示します。 次のCコードを検討してください。
 a = 1; a = a + 1; printf("%d",a) 

すべてが透明で、結果は予測可能です。

Haskellでも同じことを行います。
 Prelude> let a = 1 Prelude> let a = a + 1 Prelude> print a ^CInterrupted. 

コードの実行は終わりません。 1行目では、aを1として定義します。2行目では、a + 1として定義します。2行目を読み取るとき、インタープリターはaの前の値を忘れ、この場合、a自体を再度決定します。 さて、この再帰的な定義は決して計算されません。

記憶の指定された領域に関しては-Haskellにありますが、これはまったく異なる話です。

この設計を使用すると、「connect」演算子のいくつかの呼び出しを通じてパラメーターを渡すことができます。
 Prelude> getLine >>= \a -> putStrLn " :" >>= \_ -> putStrLn a asdf  : asdf 


実際のコード


ここで、秘密の知識を使用して、現実のものを書きます。 つまり、ユーザーからデータを受信するプログラムは、ユーザーに対して何らかのアクションを実行し、結果を画面に表示します。 プログラムを別のファイルに書き込み、マシンコードにコンパイルします。

ファイルにtest.hsという名前を付けます。
 main = putStrLn "  :" >>= \_ ->       getLine >>= \a ->       putStrLn "   :" >>= \_ ->       putStrLn (show ((read a)^2)) 

コンパイル:
 ghc --make test.hs 

実行:
 $ ./test   : 12    : 144 

read関数は、文字列を目的の型の値に解析しようとします。 彼女が推測するタイプは別の話です。 show関数は、任意のタイプの値を文字列に変換します。

読み取り関数は安全ではありません;文字を与えて数字の解析を要求すると、エラーが発生します。 これにこだわるつもりはありません。このケースには安全なモジュールがあることだけに言及します。

純度の混合物


それとは別に、副作用コードから純粋な関数を呼び出す方法についての疑問が生じます。

上記の例では、純粋な関数は単にIO関数の引数として記述されています。 多くの場合、これで十分ですが、常にではありません。

クリーンコードを呼び出す方法は他にもあります。

これらの最初のものは、クリーンなコードから副作用コードへの暴力的な変換です。 実際、純粋なコードは副作用の特殊なケースと見なすことができるため、この変換は危険をもたらしません。 そして、return関数を使用して実装されます:
 main = putStrLn "  :" >>= \_ ->       getLine >>= \a ->       putStrLn "   :" >>= \_ ->       return (show ((read a)^2)) >>= \b ->       putStrLn b 

コンパイル、検証、プログラムは以前と同じように機能します。

もう1つの方法は、let ... in ... Haskelコンストラクトを使用することです。多くのマニュアルでは、十分な注意が払われているので、止めません。既製の例を挙げます。
 main = putStrLn "  :" >>= \_ ->       getLine >>= \a ->       putStrLn "   :" >>= \_ ->       let b = (show ((read a)^2)) in       putStrLn b 


もっと砂糖が必要


言語開発者は、構造が一般的であることを認識しています
 >>= \_ -> 

したがって、それらを示すために、演算子を導入しました
 >> 

コードを書き換えます:
 main = putStrLn "  :" >>       getLine >>= \a ->       putStrLn "   :" >>       let b = (show ((read a)^2)) in       putStrLn b 

それでもう少しきれいになりました。

しかし、もっとクールなトリックがあります-構文糖は「する」
 main = do    putStrLn "  :"    a <- getLine    putStrLn "   :"    let b = (show ((read a)^2))    putStrLn b 

必要なもの! だから、すでに生きることができます。

左揃えに限定されたdoブロック内では、次の置換が行われます。
 a <- abc   abc >>= \a -> abc   abc >> let a = b   let a = b in do 

「do」表記により、構文は最新のすべてのプログラミング言語の構文と非常によく似ています。 それにもかかわらず、内部では、クリーンなコードと副作用コードを分離するためのよく考えられたメカニズムがあります。

興味深い違いは、returnステートメントの使用です。 ブロックの中央に挿入でき、関数の実行を中断せず、混乱を招く可能性があります。 しかし実際には、ブロックの最後でIO関数から純粋な値を返すためによく使用されます。
 get2LinesAndConcat:: IO String get2LinesAndConcat = do    a <- getLine    b <- getLine    return (a + b) 


真空中の球


次に、別の関数でクリーンなコードを取り出します。 同時に、最後に不足しているタイプ署名を整理しましょう。
 main :: IO () main = do    putStrLn "  :"    a <- getLine    putStrLn "   :"    let b = processValue (read a)    putStrLn (show b) processValue :: Integer -> Integer processValue a = a ^ 2 

重要な点は、副作用のI / OコードはI / Oコードからのみ実行できることです。 ただし、クリーンコードはどこからでも実行できます。

したがって、純粋な機能的世界は、副作用に関連するすべてのものから厳密かつ確実に分離されます。 processValueの内部では、あらゆるものを検討し、あらゆるロジックを実装できます。 しかし、100万行のコードのロジックがそこから呼び出されたとしても、どの入力値に対しても、出力は常に同じであると確信できます。 そして、そこに渡されたパラメーターは決して損なわれることはないので、さらに安全に使用できます。

スタイルガイドでは、副作用コードの使用を最小限に抑え、純粋な機能に最大限の機能をもたらすことをお勧めします(5)。 ただし、プログラムがI / Oを実行するように設計されている場合は、必要な場所で使用することを避けないでください。 原則として、そのような場合、補助機能が必要であり、これは純粋な場合があります。 経験豊富なHaskellプログラマーは、命令型言語と比較してIOコードの優れたサポート性を認識しています(このステートメントはSimon Peyton Johnesによるものですが、直接リンクは見つかりませんでした)。

純粋な関数には、パフォーマンスの1つの側面があります。 古典的な例を見てみましょう。多くのフィールドを持つ複雑な「従業員」構造を関数に渡します。 そのため、siとの類推により、コードの効率はポインターでこのパラメーターを渡すことに匹敵し、siではスタックを通過するだけで元の構造の耐性が保証されるため、信頼性はパラメーターをスタックに渡すことに匹敵します。

何言ってるの?


「このコードはひどく、不合理に複雑で、他のすべての言語のウォームチューブセマンティクスとの共通点が少なすぎます。c/ c ++ / c#/ java / pythonなどで十分です。」

まあ、これにはいくつかの真実があります。 ここで、あなたが恐ろしいと思うことを決定する必要があります:きれいなコードまたはこのメカニズムの特定の実装から副作用を分離します。

そのようなメカニズムをよりシンプルで理解しやすいものにする方法を知っているなら、それを世界のコミュニティに伝えてください! Haskellコミュニティは非常にオープンでフレンドリーです。 定期的に採用されている新しい標準のドラフトでは、あらゆる提案が考慮されており、本当に価値がある場合は、確実に受け入れられます。

「Pythonでもすべてが良い、あなたは副作用に執着している!」と思うなら、誰もあなたが好きなツールを使うことを気にしません。 私からは、Haskellが開発を本当に単純化し、コードをより理解しやすくすることを付け加えることができます。 これまたはその逆を確認する唯一の方法は、Haskellで書くことです!

次に行く場所


さらなる研究のため、 またはこの記事の代わりに 、記事「haskellのソフト入門」(6)、特にその翻訳(7)をお勧めします。

さらに、もちろん、他の記事も適しています(8)。 多くのガイドが書かれていますが、それらはすべて異なる観点から同じことを説明しています。 残念ながら、ロシア語に翻訳された情報はほとんどありません。 豊富なマニュアルがあるにもかかわらず、言語は単純であり、その説明と標準ライブラリの説明は270ページしか必要ありません(9)。

かなり多くの情報も標準ライブラリのドキュメントに含まれています(10)。

この記事が誰かを助けたり、単に面白そうだと思ったら、コメントや批判を歓迎します。

ps Haskellの世界で「タイプビルダー」と呼んだものは、「 タイプコンストラクター 」と呼ばます。 これは、OOPから取られた「デザイナー」という言葉の意味を忘れやすくするために行われます。これらは完全に異なるものです。 この状況は、型コンストラクターに加えて、OOPとは関係のないデータコンストラクターもあるという事実によって悪化します。

参照資料


  1. www.haskell.org/haskellwiki/Monad_tutorials_timeline
  2. http://en.wikipedia.org/wiki/Monad_(category_theory)
  3. hackage.haskell.org/platform
  4. habrahabr.ru/blogs/Haskell/80396
  5. www.haskell.org/haskellwiki/Avoiding_IO
  6. www.haskell.org/tutorial
  7. www.rsdn.ru/article/haskell/haskell_part1.xml
  8. www.haskell.org/haskellwiki/Tutorials
  9. www.haskell.org/definition/haskell98-report.pdf
  10. www.haskell.org/ghc/docs/7.0.3/html/libraries


upd:(ネタバレ!)

コメントで正しく促されたので、モナドのマニュアルの名前の選択は完全に成功していません。 モナドのトピックは公開されていないため、控えめな印象が残っています。

そのため、「モナド」という言葉は一連の演算子です
 >>= >> return fail 

そして、それらが定義されているあらゆるタイプのデータ。 たとえば、IO。

この言葉の周りでは、あまり良いオーラは発達していませんが、実際には秘密の意味はありません。 これは、 モナドなしで説明できるプログラミングパターンの名前です。

upd2:
Afiskonは興味深いプレゼンテーションへのリンクを提供しました
Haskellについて

Source: https://habr.com/ru/post/J118167/


All Articles