Monad (functional programming)とは? わかりやすく解説

Weblio 辞書 > 辞書・百科事典 > 百科事典 > Monad (functional programming)の意味・解説 

モナド (プログラミング)

(Monad (functional programming) から転送)

出典: フリー百科事典『ウィキペディア(Wikipedia)』 (2023/07/23 23:27 UTC 版)

関数型プログラミングにおいて、モナドはプログラムを構造化するための汎用的な抽象概念である。対応したプログラム言語では、ボイラープレート的なコードでもモナドを使って除去することが可能となる。これはモナドが、特定の形をした計算を表すデータ型と、それに関する生成と合成の2つの手続きを提供することによって実現されている。生成は任意の基本型の値をモナドに包んでモナド値を生成する手続きであり、合成はモナド値を返す関数(モナド関数)たちを合成する手続きである。[1]

広い範囲の問題をモナドを使うことで単純化できる。例えば、Maybeモナドを使えば未定義値への対処が簡単になり、Listモナドを使えばリストに入った値を柔軟に扱うことができる。複雑に組み合わさった関数は、モナドを使えば、補助データの管理や制御構造や副作用を除去した簡単なパイプライン構造に置き換えることができる[1][2]

モナドの概念や用語は圏論から来ている。圏論においては、モナドは追加の構造を持つ関手として定義されている[注釈 1]。1980年代の後半に研究され始め、1990年代前半には、一見無関係な計算機科学上の問題がモナドによって統一的で関数的にモデル化できることが分かってきた。圏論はモナドの満たすべき形式的な条件を与え、これはモナド則と呼ばれている。モナド則はモナド的なコードの形式的検証にも使用可能である。[3][4]

ある種の計算では、その計算の意味をモナドで明確に表現できることを利用して、プログラム言語の便利機能の実装にモナドを使うことができる。Haskellのように、標準ライブラリですでに汎用のモナド構造といくつかのインスタンスを定義している言語もある[1][5]

歴史

プログラミングにおけるモナドの概念は1980年代のプログラム言語Opal (プログラミング言語)英語版に見られるが、このときは「コマンド」と呼ばれ形式化はされなかった[要出典]

1991年にEugenio Moggiはプログラムの構造化でモナドを初めて汎用的に使用した[6]。この成果を元に、Philip WadlerやSimon Peyton Jonesといったプログラム言語の研究者(二人ともHaskellの言語仕様に取り組んでいた)により実装が行われた。初期のバージョンのHaskellは問題の多い「遅延リスト」を用いた入出力を使っていたが、Haskell 1.3からは遅延評価により柔軟に合成可能なモナドによる入出力が導入された。

入出力に加えて、プログラム言語研究者とHaskellのライブラリ設計者は構文解析器やインタープリタにモナドを適用することに成功した。Haskellのdo記法に現れる性質から、モナドの概念はapplicative functorとarrowへと一般化された。

長い間、Haskellとその派生だけがプログラミングにおけるモナドの主な利用者であった。最近では、F#がコンピュテーション式または「ワークフロー」と呼ばれる機能を導入した。これは、命令型プログラムしか経験のないプログラマにもなじみやすい構文を使ったモナド的構成を提供しようという試みである[7]

動機付けと例

プログラム言語Haskellはモナドを多用する関数型の言語であり、簡単にモナドの合成ができるような構文糖衣を持っている。特に指定がない限り、この記事のコード例はHaskellで書かれている。

モナドの紹介として一般的なふたつの例 Maybe モナドと I/O モナドを取り上げる。もちろん、モナドはHaskellに限られたものではないので、JavaScriptでの Writer モナドの例もその次に扱う。

Maybeモナド

オプション型 Maybe a を考えよう。これは型 a の値をひとつ持つか、全く持たないかのふたつの状態を表現する型である。これらを区別するために、二種類の代数的データ型構成子を持つ。Just tはひとつの値tを保持し、Nothingは値を何も保持しない。

data Maybe t = Just t | Nothing

この型を使って検査例外の簡単なものを作ってみたいとする。計算の途中で失敗した場合は、残りの計算は飛ばして結果Nothingを返し、すべての計算が成功した場合は何か値xを保持したJust xを返すことにする。

以下の例では、addMaybe Int型のふたつの引数を受け取って、同じ型の結果を返す。mxmyがともにJustである場合は、これらの値の和をJustに入れて返したい。もし、mxmyのどちらかがNoneだった場合は、Nothingを返したい。こういった振る舞いをする関数を単純に実装しようとしたら、「if Nothing then Nothing else Just xxに何かする」の形の場合わけを複数ネストすることになり、すぐやっかいなものになる[1]

add :: Maybe Int -> Maybe Int -> Maybe Int
add mx my =
  case mx of
    Nothing -> Nothing
    Just x  -> case my of
                 Nothing -> Nothing
                 Just y  -> Just (x + y)

これを楽にするための、各計算を繋げる演算子を定義することができる。二項演算子 bind (>>=) は失敗する可能性のある計算の結果を、もうひとつの失敗する可能性のある計算へ連鎖する。最初の引数が Nothing だった場合は、二番目の引数(関数引数)は無視されて、単に計算全体が失敗する。もし、最初の引数が Just x だった場合は、この値 x は二番目の関数引数に渡されて、この関数の結果として新しい Maybe の値が返される。これは Just な値か失敗かのどちらかになる。

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing  >>= _  =  Nothing    -- 失敗した計算は Nothing を返す
(Just x) >>= f  =  f x        -- 値 x に関数 f を適用する

計算の状態に影響を与えずに値を投入する構築子は、Just がすでにそうなっている。

return :: a -> Maybe a
return x = Just x       -- 値 x を包んで、型 (Maybe a) の値として返す

これらを使うことで、上の例は次のように書ける。

add :: Maybe Int -> Maybe Int -> Maybe Int
add mx my =             -- (Maybe Int) 型のふたつの値を足す関数。各入力は Nothing の場合がある
  mx >>= (\x ->         -- mx が Nothing でない場合に、値 x を抽出する
    my >>= (\y ->       -- my が Nothing でない場合に、値 y を抽出する
      return (x + y)))  -- 値(x+y)を包んで、(Maybe Int)型の値として和を返す

さらにdo記法という構文糖衣を使うことで、次のように書くことができる。

add :: Maybe Int -> Maybe Int -> Maybe Int
add mx my = do
  x <- mx
  y <- my
  return (x + y)

この種の操作は頻出なので、Haskellの標準関数(liftM2)が用意されている。これはふたつのモナド的な値(ここでは、ふたつのMaybe)と、その中身(ふたつの数値)を組み合わせる関数(加算)を受け取る関数であり、上の例は次のように書ける。

add :: Maybe Int -> Maybe Int -> Maybe Int
add = liftM2 (+)

(上記のdo記法を使ってliftM2の定義を書いてみよう)

I/Oモナド

Haskellのような純粋関数型言語では関数が外部から観測可能な作用をもつことは、関数の意味から外れることになるため許されない。しかし、関数が直接副作用を起こすのではなく、求める副作用を記述する値を構築して返し、呼び出した側が適当な時に実行することは可能である[8]。Haskellの記法では、型 IO a の値がこのようなアクションを表現していて、実行されたときには、型 a の何らかの値を生成する。

IO型の値は「現在の世界の状態を引数に取り、関数の返り値として変化した状態を持つ新しい世界を返す」という意味でアクションとみなすことができる。例えば、関数 doesFileExistremoveFile は次の型を持つ関数である。

doesFileExist :: FilePath -> IO Bool
removeFile :: FilePath -> IO ()

removeFile は関数としては、FilePath を引数にとり、ある IOアクションを返す。このアクションが実行されると世界を、この場合はファイルシステムを FilePath という名前のファイルの存在しないものに変更する。この場合は、IO の持っている値は () 型の値なので、なにも結果は生成されず、呼び出し側はこれを気にする必要はない。一方で、doesFileExist の場合は、関数の返す IOアクションはブール型の値、True または Falseを包んでいて、概念的には、アクションが実行されたときのファイルシステムに FilePath という名前のファイルが存在したかどうかを呼び出し側が知っているという世界の状態を表現している。アクションからアクションへと世界の状態を渡して管理するこの方法により、決まった順序で状態を変化させていくアクションの列を定めることが可能となる。この過程は時相論理において宣言的な命題のみで時間の経過を表現する手法と似ている。以下の例ではプログラム内のアクションを連鎖する方法の詳細を再び Haskell を使って明らかにする。

基本的な種類の入出力操作、標準出力に文字列を書く、標準入力から文字列を読む、ファイルの読み書き、ネットワーク越しのデータ送信等をすべて記述できるようにしたい。さらに、これらのプリミティブを組み合わせて大きなプログラムを作れる必要もある。例えば次のように書けることが望ましい。

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

この直観的な記法はどのように形式化できるだろうか?このためには各入出力アクションに対して、いくつかの基本的な操作を実行できる必要がある。

  • ふたつの入出力アクションをひとつにできなければならない。Haskellでは、中置演算子 >> を用いて、putStrLn "abc" >> putStrLn "def" と書き、コンソールに二行の文字列を表示するひとつのアクションである。演算子 >> の型は IO a → IO b → IO b であり、ふたつの入出力アクションを引数に取り、順番に実行するひとつのアクションにまとめて返す。
  • 何もしないという入出力アクションも必要である。これは値を返すがなんの副作用も持たない。Haskellではこのアクションは return で構築され、この型は a → IO a である。
  • さらに、最初のアクションの結果に基づいて次に実行するアクションを決める必要がある場合がある。Haskellでは、演算子 >>= (bindと読む)を使い、型は IO a → (a → IO b) → IO b である。左側のオペランドは型 a の値を返す入出力アクションであり、右側のオペランドは左側のアクションの生成した値に応じて次に実行する入出力アクションを選択する関数である。結果は、最初のアクションを実行しその値を関数に渡した評価結果をアクションとして実行し、最後にこのアクションの結果を返す合成されたひとつのアクションとなる。
Haskellのこの演算子を使った例は getLine >>= putStrLn であり、これは標準入力から一行文字列を読み込み、それをそのまま標準出力に書き出す。最初の演算子 >> は単にこの演算子の特別な場合である。最初のアクションの結果は無視され、二番目のアクションは常に同じである。

これらの3つの演算子をプリミティブな入出力操作として「いかなるプログラムでも」書けるかというのは、データ変換(ラムダ式使う)やif式やループを含めたとしても自明であるとは言いがたい。上の例は長い式を使って次のように書ける。

main =
  putStrLn "What is your name?" >> 
  getLine >>= \name ->
  putStrLn ("Nice to meet you, " ++ name ++ "!")

bind 演算子の持つパイプライン構造により、getLineputStrLn の命令はそれぞれ一度だけ、書いた順序で評価され、入力ストリームから文字列を取り出して、出力ストリームに出力する副作用は関数型パイプラインの中でも正しく実行される。これは言語が関数をアウト・オブ・オーダー実行遅延評価する場合でも成立する。

明らかに入出力の定義とMaybeの定義は共通の構造を持っているが、異なっている部分も多い。上述した構造や他のよくある似たような多くの構造の抽象化である。モナドの一般化された概念は、計算の一部が副作用を発生させるにもかかわらず、純粋関数的な計算として実行したくなるようなあらゆる状況を包含している。

形式的定義

モナドは、元となる型システムが与えられたとき、その内部に対応する型システム(モナド的型システムと呼ぶ)を埋め込む構成である(つまり、各モナド的型は元のシステムの型でもある)。このモナド的型システムは元の型システムの重要な点は全て保存するが、モナドに固有の特徴を追加する[注釈 2]

モナドは複数の方法で定義できる。そのうちの1つは「モナドとは、ある法則(モナド則)を満たす三つ組みである」[9] で表される定義である。すなわち圏論のクライスリ圏におけるクライスリトリプルである。

三つ組

三つ組は以下の要素からなる。

表. モナドを構成する三つ組
名前 別名 定義式 説明
型構築子M x から派生した別の型を得る(型から型を作る)演算子
unit関数 return
unit :: x -> M x
x の値を型 M x の値へうつす多相な関数
bind関数 >>=
bind :: M x -> (x -> M y) -> M y
M x の値、型 x の値を型 M y の値へ写す関数の2つから M y を得る関数

型構築子M

class Monad M where
...
value :: M x

型構築子Mは、ある型 x から派生した型を得る方法を定める[10]。得られる型はしばしば M x で表記される。

unit 関数

unit関数( return とも[注釈 3])は型 x の引数を型 M x の返り値にうつす関数である。すなわちunit 関数は型 x の値をモナド型 M x の値へうつす多相な関数である。引数値変換の有無は定義されない(一般には値を変換せずに保持する(例:入力 1 =>出力 Maybe(1))。

bind 関数

bind関数( >>= とも[11])は「モナド的値」と「値をモナド的値へうつす関数」を引数にとり、第1引数のモナド的値を第2引数関数の値域へうつす。

モナド則

三つ組 {M, unit(return), bind(>>=)} をモナドと成す規則をモナド則という[12]。以下がモナド則である。

  • return>>= に対して、ほとんど単位元である。
    • (return x) >>= f      f x
      
    • m >>= return      m
      
  • ふたつの関数を続けて bind するのは、これらの関数から決まるひとつの関数を bind することに等しい。
    • (m >>= f) >>= g      m >>= ( \x -> (f x >>= g) )
      

公理を do 記法で表現することもできる。

  • do { f x }      do { v <- return x; f v }
    
  • do { m }      do { v <- m; return v }
    
  • do { x <- m; y <- f x; g y }      do { y <- do { x <- m; f x }; g y }
    

また、モナド合成演算子

(f >=> g) x = (f x) >>= g

を使うと

  • return >=> g      g
    
  • f >=> return      f
    
  • (f >=> g) >=> h      f >=> (g >=> h)
    

fmap と join

Haskellではモナドを returnbind 関数を用いて定義するが、return にふたつの演算子 joinfmap を加えることでも定義可能である。この定式化は圏論における元のモナドの定義により近くなる。fmap 演算子は型 (tu) → M t→M u[13] を持ち、ふたつの型間の関数を受け取って、モナドの中の値に「同じこと」をする関数を返す。join 演算子は型 M (M t)→M t を持ち、二層になったモナド的な情報を平らにしてひとつにする。

ふたつの定式化は以下の関係を持つ。

fmap f m = m >>= (return . f)
join n = n >>= id

m >>= g      join (fmap g m)

ここで、m は M t、n は M (M r)、f は tu、g は t → M v の型を持つ。truv は元の型である。

モナドでなくても、型と関数からなる圏では fmap 関数は任意の函手に対して定義できる。函手は次の法則を満たす。

fmap id      id
fmap (f . g)      (fmap f) . (fmap g)

同じ圏で、return基点付き函手英語版を特徴付ける。これは値を函手の中に「持ち上げる」ことによる。満たすべき法則は次の通りである。

return . f  fmap f . return

さらに、join によってモナドが特徴付けられる。

join . fmap join      join . join
join . fmap return    join . return = id
join . fmap (fmap f)  fmap f . join

加法的モナド

加法的モナドは、モノイド則を満たす二項演算子 mplusとその単位元としてモナド的な値 mzero を備えたモナドである。演算子 mplus の型は M tM tM t であり(ここで、M はモナドの構築子であり t は元の型である)、結合律と左右からの単位元律を満たす。

(a `mplus` b) `mplus` c   =   a `mplus` (b `mplus` c)
m `mplus` mzero              mzero `mplus` m               m

このことから、加法的モナドはモノイドでもある。>>=に対しては、mzeroはヌル要素として振舞う。ゼロを掛けるとゼロになるように、mzero と関数を bind すると結果はゼロとなる。

mzero >>= f                  mzero

同様に、モナド的な値 m とつねにゼロを返す関数を bind すると、結果はゼロである。

m >>= (\x -> mzero)          mzero

直観的には、モナド的値としてのゼロはモナドに関連した構造だけを持ち元の型の値を持たない。Maybe モナドでは、「Nothing」がゼロである。リストモナドでは、空リストがゼロである。

構文糖衣: do記法

bind 演算子 >>= を直接使ってプログラムを書くのがよい場合もあるが、典型的にはdo記法 (OCamlでは perform記法、F#ではコンピュテーション式)を使用し、命令型言語のような見た目にできる。コンパイラはdo記法の式を >>= を使ったものに変換する。例えば

a = do x <- [3..4]
       [1..2]
       return (x, 42)

を変換した結果は以下となる。

a = [3..4] >>= (\x -> [1..2] >>= (\_ -> return (x, 42)))

リストモナドの実装を見てみよう。リストにマップを行い結合を行う(平らにする)この関数は concatMap とも呼ばれる。

instance Monad [] where
  m >>= f  = concat (map f m)
  return x = [x]
  fail s   = []

よって、以下のような変形が行われて、それぞれの式はすべて等価である。

a = [3..4] >>= (\x -> [1..2] >>= (\_ -> return (x, 42)))
a = [3..4] >>= (\x -> concatMap (\_ -> return (x, 42)) [1..2] )
a = [3..4] >>= (\x -> [(x,42),(x,42)] )
a = concatMap (\x -> [(x,42),(x,42)] ) [3..4]
a = [(3,42),(3,42),(4,42),(4,42)]

リスト[1..2]は使われないことに注意しよう。左矢印をつけないことで、連鎖した関数は引数を無視するように変換されて、値ではなくモナド的な構造だけが問題になることを示すことができる。例えば、状態モナドで値を生成せずに状態だけを変化させるようなときに使われる。do記法は >>= の単なる構文糖衣であるので、どんなモナドに対しても使うことができる。

Maybeモナドの値を安全に割り算する以下の定義も等価である。

x // y = do
  a <- x  -- xとyの中に値がある場合は取り出す
  b <- y
  if b == 0 then Nothing else Just (a / b)

x // y = x >>= (\a -> y >>= (\b -> if b == 0 then Nothing else Just (a / b)))

同様にF#ではコンピュテーション式を使うことができる。

let readNum () =
  let s = Console.ReadLine()
  let succ,v = Int32.TryParse(s)
  if (succ) then Some(v) else None

let secure_div = 
  maybe { 
    let! x = readNum()
    let! y = readNum()
    if (y = 0) 
    then None
    else return (x / y)
  }

このmaybeブロックは構文糖衣であり、以下の式に変換される。

maybe.Delay(fun () ->
  maybe.Bind(readNum(), fun x ->
    maybe.Bind(readNum(), fun y ->
      if (y=0) then None else maybe.Return(x / y))))

モナド的な汎用関数

安全な割り算の結果は、値が Nothing であるかを素直に確認せずに(つまり割り算の結果があるかを確認せずに)そのまま計算に使えたほうが便利なことがある。これは「持ち上げ英語版」関数を使うことで可能である。この関数は Maybe に限定されず、任意のモナドに対して定義される。Haskellでは LiftM2 と呼ばれる。

liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
liftM2 op mx my = do
    x <- mx
    y <- my
    return (op x y)

関数型を示す矢印が右結合であることから、liftM2は2引数の関数を引数に取って、さらなる2引数の関数を返す。型シグネチャは m がモナドのとき、任意の2引数関数を「持ち上げ」ることができることを示している。例えば、

(.*.) :: (Monad m, Num a) => m a -> m a -> m a
x .*. y = liftM2 (*) x y

は演算子 (.*.) を定義し、それは引数がどちらも Nothing でないときに、掛け算した結果を返す(どちらかが Nothing の場合は Nothing を返す)。この方法の利点はモナドの実装に深入りする必要がないことである。他の同じような関数やモナドを使う場合でも、liftM2 を使うことで、やりたいことが鮮明になる(コードの再利用を見よ)。

数学的には、liftM2 は次のように定義される。

Haskellのモナドチュートリアル

古いチュートリアル

他の文書

Scala のモナドチュートリアル

他の言語におけるモナド




英和和英テキスト翻訳>> Weblio翻訳
英語⇒日本語日本語⇒英語
  

辞書ショートカット

カテゴリ一覧

すべての辞書の索引



Weblioのサービス

「Monad (functional programming)」の関連用語



Monad (functional programming)のお隣キーワード
検索ランキング

   

英語⇒日本語
日本語⇒英語
   



Monad (functional programming)のページの著作権
Weblio 辞書 情報提供元は 参加元一覧 にて確認できます。

   
ウィキペディアウィキペディア
All text is available under the terms of the GNU Free Documentation License.
この記事は、ウィキペディアのモナド (プログラミング) (改訂履歴)の記事を複製、再配布したものにあたり、GNU Free Documentation Licenseというライセンスの下で提供されています。 Weblio辞書に掲載されているウィキペディアの記事も、全てGNU Free Documentation Licenseの元に提供されております。

©2025 GRAS Group, Inc.RSS