開業医として、なぜ私はハスケルを気にする必要がありますか?モナドとは何ですか、なぜそれが必要なのですか?[閉まっている]


9

彼らがどんな問題を解決するのか分かりません。



2
この編集は少し極端だと思います。あなたの質問は本質的に良いものだったと思います。それはそれのいくつかの部分が少し...議論の的だったというだけです。それはおそらく、あなたが単に見ていなかった何かを学ぼうとする欲求不満のせいだろう。
Jason Baker

@SnOrfus、私はこの質問をやりくりした人でした。私はそれを適切に編集するのが面倒だった。
ジョブ

回答:


34

モナドは良くも悪くもありません。彼らはただです。これらは、プログラミング言語の他の多くの構成要素のような問題を解決するために使用されるツールです。それらの非常に重要なアプリケーションの1つは、純粋に関数型の言語で作業するプログラマの生活を楽にすることです。しかし、これらは非関数型言語で役立ちます。モナドを使用していることに人々が気づくことはほとんどありません。

モナドとは何ですか?モナドを考える最良の方法は、設計パターンとしてです。I / Oの場合、それは、グローバル状態がステージ間で渡されるものである、栄光のあるパイプラインに過ぎないと考えることができます。

たとえば、作成中のコードを見てみましょう。

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

ここでは、目に見える以上のことが起こっています。たとえばputStrLn、次の署名があることに気づくでしょう: putStrLn :: String -> IO ()。どうしてこれなの?

このように考えてみてください(stdoutとstdinが読み取りと書き込みができる唯一のファイルであると(簡単にするために)見せかけましょう)。命令型言語では、これは問題ありません。しかし、関数型言語では、グローバルな状態を変更することはできません。関数は、1つまたは複数の値を取り、1つまたは複数の値を返す単純なものです。これを回避する1つの方法は、各関数に渡される値と各関数から渡される値としてグローバル状態を使用することです。したがって、コードの最初の行を次のようなものに変換できます。

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

...そしてコンパイラは、の2番目の要素に追加されたものをすべて印刷することを知っていますglobal_state。今、あなたのことは知りませんが、そのようにプログラムするのは嫌です。これを簡単にする方法は、モナドを使用することでした。モナドでは、あるアクションから次のアクションへのある種の状態を表す値を渡します。これがputStrLn戻り値の型を持っている理由ですIO ():新しいグローバル状態を返します。

なぜあなたは気にするのですか? さて、命令型プログラムに対する関数型プログラミングの利点はいくつかの場所で議論されてきたので、一般的にはその質問には答えません(ただし、関数型プログラミングの事例を聞きたい場合は、このペーパーを参照してください)。ただし、この特定のケースでは、Haskellが何を達成しようとしているのかを理解していると役立ちます。

多くのプログラマーは、Haskellが命令型コードを記述したり、副作用を使用したりしないようにしようとしていると感じています。それは真実ではありません。このように考えてください。命令型言語とは、デフォルトで副作用を許可する言語ですが、本当に必要な場合(そして必要となるいくつかのゆがみに対処する意思がある場合)に関数コードを記述できます。Haskellはデフォルトで純粋に機能しますが、本当に必要な場合は(プログラムが役立つ場合に行う)命令型コードを記述できます。重要なのは、副作用のあるコードの作成を難しくすることではありません。副作用があることを明示的に確認する必要があります(型システムでこれを強制します)。


6
最後の段落は金です。少し抽出して言い換えると、「命令型言語はデフォルトで副作用を許可しますが、本当に必要な場合は関数型コードを記述できます。関数型言語はデフォルトで純粋に関数型ですが、命令型コードを記述できます。本当にしたいなら」
フランク・シアラー、2011年

あなたがリンクした論文が最初から「関数型プログラミングの美徳としての不変性」の考えを明確に拒否していることは注目に値します。
メイソンウィーラー、

@MasonWheeler:私はそれらの段落を読みました。不変性の重要性を否定するのではなく、関数型プログラミングの優位性を実証するための説得力のある議論として拒否しました。実際、彼はgoto(構造化プログラミングの議論として)論文の少し後の部分での排除について同じことを述べ、そのような議論を「無実」として特徴付けています。しかし、私たちの誰もが密かにgotoの復帰を望んでいません。それをgoto広範に使用する人にとって必要ではないということは簡単ではありません。
ロバートハーベイ

7

噛みます!モナド自体は実際にはHaskellの存在理由ではありません(Haskellの初期のバージョンにはモナドはありませんでした)。

あなたの質問は、「構文を見るとC ++だとうんざりします。しかし、テンプレートはC ++の非常に宣伝されている機能なので、他の言語の実装を調べました」と少し似ています。

Haskellプログラマーの進化は冗談であり、真剣に受け取られることを意図していません。

Haskellのプログラムを目的としたモナドは、モナド型クラスのインスタンスです。つまり、特定の小さな操作セットをサポートする型です。Haskellには、Monad型クラスを実装する型に対する特別なサポート、特に構文上のサポートがあります。実際にこれがもたらすのは、「プログラム可能なセミコロン」と呼ばれているものです。この機能をHaskellの他のいくつかの機能(ファーストクラスの関数、デフォルトでは遅延)と組み合わせると、従来は言語機能と見なされてきたライブラリーとして特定のものを実装する機能が得られます。たとえば、例外メカニズムを実装できます。ライブラリとして継続とコルーチンのサポートを実装できます。Haskell、言語は可変変数をサポートしていません:

「たぶん/ Identity / Safe Divisionモナド???」について尋ねます。Maybeモナドは、ライブラリとして例外処理を実装する方法(非常に単純な、1つの例外のみ)の例です。

そのとおりです。メッセージの書き込みとユーザー入力の読み取りはそれほど独特ではありません。IOは「機能としてのモナド」のひどい例です。

ただし、反復するために、言語の残りの部分から分離された1つの「機能」(たとえばモナド)自体は必ずしもすぐに役立つとは限りません(C ++ 0xの優れた新機能は右辺値参照であり、構文は退屈であり、必然的にユーティリティが表示されるため、C ++のコンテキストから除外します。プログラミング言語は、バケットにたくさんの機能を投入することで得られるものではありません。


実際、haskellは、STモナドを介して可変変数をサポートしています(独自のルールによって実行される言語の数少ない奇妙で不純な魔法の部分の1つ)。
サラ

4

プログラマはみなプログラムを作成しますが、類似点はそこで終わります。プログラマは、ほとんどのプログラマが想像できるよりもはるかに異なると思います。静的変数型vsランタイムのみの型、スクリプトvsコンパイル済み、Cスタイルvsオブジェクト指向など、長年の「戦い」を体験してください。一部のプログラミングシステムでは、意味のない、あるいはまったく使用できないと思われる優れたコードを作成しているため、1つのキャンプが劣っているということを合理的に主張することは不可能です。

人によって考え方が違うだけだと思います。構文糖や、利便性のためだけに存在し、実際に実行時のコストが大きい抽象化に誘惑されないのであれば、そのような言語は避けてください。

ただし、少なくともあきらめている概念に慣れることをお勧めします。ラムダ式の大事なことを実際に理解している限り、激しく純粋なCである誰かに対して私は何もしません。ほとんどがすぐにはファンにならないと思いますが、少なくともラムダで解決する方がはるかに簡単だったであろう完璧な問題を見つけたとき、それは彼らの心の奥にあるでしょう。

そして何よりも、特に自分が話していることを実際に知らない人々による、ファンボーイスピーチに悩まされないようにしてください。


4

Haskellは参照透過性を適用します。同じパラメーターを指定すると、その関数を何回呼び出しても、すべての関数は常に同じ結果を返します。

つまり、たとえば、Haskell(およびMonadsなし)では、乱数ジェネレータを実装できません。C ++またはJavaでは、グローバル変数を使用してこれを行うことができ、ランダムジェネレーターの中間の「シード」値を格納します。

Haskellでは、グローバル変数に対応するものはモナドです。


では、乱数ジェネレータが必要な場合はどうでしょうか。それも機能ではありませんか?そうでない場合でも、どうすれば乱数ジェネレーターを取得できますか?
ジョブ

@ジョブモナド(基本的に状態トラッカー)の内部に乱数ジェネレーターを作成することも、unsafePerformIOを使用することもできます。これは、使用すべきではないHaskellの悪魔です(実際、ランダム性を使用するとプログラムが壊れます)その内部!)
代替案

Haskellでは、基本的にRNGの現在の状態である「RandGen」を渡します。そのため、新しい乱数を生成する関数はRandGenを取り、新しいRandGenと生成された数値を含むタプルを返します。代わりに、最小値と最大値の間の乱数のリストが必要な場所を指定することもできます。これは遅延評価された無限の数のストリームを返すため、新しい乱数が必要なときはいつでもこのリストをウォークスルーできます。
Qqwy 2016年

他の言語でそれらを入手するのと同じ方法!いくつかの疑似乱数ジェネレータアルゴリズムを取得し、それに値をシードして、「乱数」を飛び出します。唯一の違いは、C#やJavaなどの言語は、システムクロックなどを使用してPRNGを自動的にシードすることです。それとhaskellでは「次の」番号を取得するために使用できる新しいPRNGも取得するのに対し、C#/ Javaでは、これはすべてRandomオブジェクトの可変変数を使用して内部で行われます。
サラ

4

古い質問のようなものですが、それは本当に良い質問なので、お答えします。

モナドは、その実行方法を完全に制御できるコードのブロックと考えることができます。コードの各行が返すもの、実行を任意の時点で停止するかどうか、各行間で他の処理を行うかどうか。

モナドが可能にすることのいくつかの例を挙げます。私のHaskellの知識が少し不安定であるという理由だけで、これらの例はHaskellにはありませんが、Haskellがモナドの使用に影響を与えた方法のすべての例です。

パーサー

通常、ある種のパーサーを作成する場合、たとえばプログラミング言語を実装する場合は、BNF仕様を読み取り、それを解析するためにループ状のコード全体を作成するか、コンパイラコンパイラを使用する必要があります。Flex、Bison、yaccなどのようです。しかし、モナドを使用すると、Haskellで一種の「コンパイラパーサー」を作成できます。

パーサーはモナドやyaccやbisonなどの特別な目的の言語なしでは実際に実行できません。

たとえば、私はIRCプロトコルの BNF言語仕様を採用しました。

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

そして、F#(モナドをサポートする別の言語)で約40行のコードにまでクランチしました。

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

F#のモナド構文はHaskellのモナド構文と比較してかなり醜く、私はこれをかなり改善した可能性があります-しかし、家に帰るポイントは、構造的に、パーサーコードがBNFと同一であることです。これはモナド(またはパーサージェネレーター)なしで多くの作業を必要とするだけでなく、仕様にほとんど似ていないため、読み取りも保守もひどいものでした。

カスタムマルチタスク

通常、マルチタスクはOSの機能と見なされますが、モナドを使用すると、各命令モナドの後にプログラムが制御をスケジューラーに渡し、スケジューラーが別のモナドを選択して実行するように、独自のスケジューラーを作成できます。

一人の男が(再びF#で)ゲームループを制御するために「タスク」モナドUpdate()作成したため、すべての命令を各呼び出しで動作するステートマシンとして記述する代わりに、すべての命令を単一の関数であるかのように書くことができました。

つまり、次のようなことをする代わりに、

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

あなたは次のようなことをすることができます:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ to SQL

LINQ to SQLは実際にはモナドの例であり、Haskellに同様の機能を簡単に実装できます。

正確には覚えていないので詳細には触れませんが、Erik Meijerがそれをかなりよく説明しています


1

GoFパターンに精通している場合は、モナドは、放射性アナグマに噛まれたステロイドを組み合わせた、DecoratorパターンとBuilderパターンのようなものです。

上でより良い答えがありますが、私が見る特定の利点のいくつかは次のとおりです。

  • モナドは、コアタイプを変更せずに、いくつかのコアタイプを追加のプロパティで装飾します。たとえば、モナドは文字列を「リフト」し、「isWellFormed」、「isProfanity」、「isPalindrome」などの値を追加します。

  • 同様に、モナドは単純な型をコレクション型にまとめることを可能にします

  • モナドは、この高次空間への関数の遅延バインディングを可能にします

  • モナドにより、高次空間で任意の関数と引数を任意のデータ型と混在させることができます

  • モナドを使用すると、純粋でステートレスな関数と不純でステートフルなベースをブレンドできるため、問題の場所を追跡できます

Javaでのモナドのよく知られた例はリストです。これは、Stringのようないくつかのコアクラスを取り、それをListのモナド空間に「持ち上げ」、リストに関する情報を追加します。次に、get()、getFirst()、add()、empty()などの新しい関数をそのスペースにバインドします。

大規模な場合、プログラムを作成する代わりに、(GoFパターンとして)大きなビルダーを作成し、最後にbuild()メソッドがプログラムが生成するはずの答えを出力すると想像してください。また、元のコードを再コンパイルすることなく、ProgramBuilderに新しいメソッドを追加できます。そのため、モナドは強力な設計モデルです。

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