web-dev-qa-db-ja.com

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

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

9
Job

モナドは良くも悪くもありません。彼らはただです。これらは、プログラミング言語の他の多くの構造のような問題を解決するために使用されるツールです。それらの非常に重要なアプリケーションの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
_

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

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

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

34
Jason Baker

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

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

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

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

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

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

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

7
Logan Capaldo

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

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

モナドが可能にすることのいくつかの例を挙げます。私の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の機能と見なされますが、モナドを使用すると、各命令モナドの後にプログラムが制御をスケジューラーに渡し、スケジューラーが別のモナドを選択して実行できるように、独自のスケジューラーを作成できます。

一人の男が "task"モナド を作成して、ゲームループを制御します(これもF#で)。そのため、すべてのUpdate()呼び出しに作用するステートマシンとしてすべてを記述する必要はありません。すべての命令を1つの関数であるかのように書くことができます。

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

_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が詳しく説明しています

4
Rei Miyasaka

Haskellは Referential Transparency を強制します。同じパラメーターを指定すると、その関数を何度呼び出しても、すべての関数は常に同じ結果を返します。

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

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

4
vz0

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

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

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

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

4
Roman Starkov

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

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

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

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

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

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

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

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

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

1
Rob