JavaScript、Python、Ruby、Swift、ScalaのOption / Maybe、Either、Future Monads
公開: 2022-03-11このモナドチュートリアルでは、モナドについて簡単に説明し、5つの異なるプログラミング言語で最も便利なモナドを実装する方法を示します。JavaScriptのモナド、Pythonのモナド、Rubyのモナド、Swiftのモナド、および/またはモナドを探している場合Scalaで、または実装を比較するために、あなたは正しい記事を読んでいます!
これらのモナドを使用すると、nullポインター例外、未処理の例外、競合状態などの一連のバグを取り除くことができます。
これは私が以下でカバーするものです:
- 圏論入門
- モナドの定義
- Option(“ Maybe”)モナド、Eitherモナド、Futureモナドの実装に加えて、JavaScript、Python、Ruby、Swift、Scalaでそれらを活用するサンプルプログラム
始めましょう! 最初に立ち寄るのは、モナドの基礎となる圏論です。
圏論入門
圏論は、20世紀半ばに活発に開発された数学の分野です。 今では、モナドを含む多くの関数型プログラミングの概念の基礎となっています。 ソフトウェア開発の用語に合わせて調整された、いくつかの圏論の概念を簡単に見てみましょう。
したがって、カテゴリを定義する3つのコアコンセプトがあります。
- 型は、静的に型付けされた言語で見られるのと同じです。 例:
Int
、String
、Dog
、Cat
など。 - 関数は2つのタイプを接続します。 したがって、それらは、あるタイプから別のタイプへ、またはそれ自体への矢印として表すことができます。 タイプ$T$からタイプ$U$への関数$f$は、$ f:T \ toU$として表すことができます。 これは、$ T $型の引数を取り、$U$型の値を返すプログラミング言語関数と考えることができます。
- 構成は、$ \ cdot $演算子で示される操作であり、既存の関数から新しい関数を作成します。 カテゴリでは、$ f:T \ to U$および$g:U \ to V $のすべての関数に対して、常に保証されています。一意の関数$ h:T \ toV$が存在します。 この関数は$f\ cdotg$として表されます。 この操作は、関数のペアを別の関数に効果的にマップします。 もちろん、プログラミング言語では、この操作は常に可能です。 たとえば、文字列の長さを返す関数($ strlen:String \ to Int $)と、数値が偶数かどうかを通知する関数($even:Int \ to Boolean $)がある場合、次のように作成できます。 function $even {\ _} strlen:String \ to Boolean $は、
String
の長さが偶数かどうかを示します。 この場合、$even {\ _}strlen=偶数\cdotstrlen$です。 構成には2つの機能があります。- 結合性:$ f \ cdot g \ cdot h =(f \ cdot g)\ cdot h = f \ cdot(g \ cdot h)$
- 恒等関数の存在:$ \ forall T:\ examples f:T \ to T $、または平易な英語では、すべてのタイプ$ T $に対して、$T$をそれ自体にマップする関数が存在します。
それでは、簡単なカテゴリを見てみましょう。
補足:ここでは、 Int
、 String
、およびその他すべてのタイプがnull以外であることが保証されている、つまりnull値が存在しないことを前提としています。
補足2:これは実際にはカテゴリの一部にすぎませんが、必要なすべての重要な部分があり、図がこのように整理されているため、これで説明します。 実際のカテゴリには、カテゴリの構成句を満たすために、$ roundToString:Double \ to String = intToString \ cdotround$のようなすべての構成された関数も含まれます。
このカテゴリの機能は非常に単純であることに気付くかもしれません。 実際、これらの関数にバグがあることはほとんど不可能です。 nullや例外はなく、算術演算とメモリの操作だけです。 したがって、発生する可能性のある唯一の悪いことは、プロセッサまたはメモリの障害です。この場合、とにかくプログラムをクラッシュさせる必要がありますが、それはめったに発生しません。
すべてのコードがこのレベルの安定性で機能するだけでいいのではないでしょうか。 絶対! しかし、たとえばI/Oについてはどうでしょうか。 私たちは間違いなくそれなしでは生きられません。 ここで、モナドソリューションが役に立ちます。すべての不安定な操作を非常に小さく、非常によく監査されたコードに分離します。これにより、アプリ全体で安定した計算を使用できます。
モナドを入力してください
I/Oのような不安定な動作を副作用と呼びましょう。 ここで、この副作用が存在する場合に、 length
やString
などのタイプなど、以前に定義したすべての関数を安定した方法で操作できるようにしたいと考えています。
それでは、空のカテゴリ$ M [A] $から始めて、特定の種類の副作用のある値と副作用のない値を持つカテゴリにします。 このカテゴリを定義し、空であると仮定します。 今のところ、これを使ってできることは何もありません。そのため、これを便利にするために、次の3つの手順を実行します。
-
String
、Int
、Double
などのカテゴリ$ A $のタイプの値を入力します(下の図の緑色のボックス) - これらの値を取得しても、それでも意味のあることは何もできないため、各関数$ f:T \ toU$を$A$から取得し、関数$ g:M [T] \toMを作成する方法が必要です。 [U] $(下の図の青い矢印)。 これらの関数を取得すると、カテゴリ$A$で実行できたカテゴリ$M[A]$の値を使用してすべてを実行できます。
- まったく新しい$M[A] $カテゴリができたので、署名$ h:T \ to M [U] $(下の図の赤い矢印)を持つ新しいクラスの関数が出現します。 これらは、ステップ1でコードベースの一部として値をプロモートした結果として出現します。つまり、必要に応じて値を記述します。 これらは、$ M[A]$での作業と$A$での作業を区別する主なものです。 最後のステップは、これらの関数を$ M [A] $の型でもうまく機能させることです。つまり、$ h:T\から関数$m:M [T] \ to M[U]$を導出できるようにします。 M[U]$へ
それでは、$A$タイプの値を$M[A] $タイプの値にプロモートする2つの方法を定義することから始めましょう。1つは副作用のない関数で、もう1つは副作用があります。
- 1つ目は$pure$と呼ばれ、安定したカテゴリの値ごとに定義されます:$ pure:T \ to M[T]$。 結果の$M[T] $値には副作用がないため、この関数は$pure$と呼ばれます。 たとえば、I / Oモナドの場合、$pure$は失敗の可能性なしにすぐに値を返します。
- 2番目は$constructor$と呼ばれ、$ pure $とは異なり、いくつかの副作用を伴う$ M[T]$を返します。 非同期I/Oモナドのこのような$constructor$の例は、Webからデータをフェッチし、それを
String
として返す関数です。 この場合、$コンストラクター$によって返される値のタイプは$M[String]$になります。
値を$M[A] $に昇格させる方法が2つあるので、プログラムの目標に応じて、使用する関数を選択するのはプログラマーの責任です。 ここで例を考えてみましょう。https://www.toptal.com/javascript/option-maybe-ether-future-monads-jsのようなHTMLページをフェッチし、このために関数$fetch$を作成します。 ネットワーク障害など、フェッチ中に問題が発生する可能性があるため、この関数の戻り型として$ M[String]$を使用します。 したがって、$ fetch:String \ to M [String] $のようになり、関数本体のどこかで$M$に$constructor$を使用します。
ここで、テスト用のモック関数を作成するとします。$ fetchMock:String \ to M[String]$。 それでも同じ署名がありますが、今回は、不安定なネットワーク操作を実行せずに、結果のHTMLページを$fetchMock$の本体内に挿入するだけです。 したがって、この場合、$fetchMock$の実装で$pure$を使用するだけです。
次のステップとして、任意の関数$f$をカテゴリ$A$から$M[A] $に安全に昇格させる関数が必要です(図の青い矢印)。 この関数は$map:(T \ to U)\ to(M [T] \ to M [U])$と呼ばれます。
これで、カテゴリ($コンストラクター$を使用すると副作用が発生する可能性があります)ができました。これには、安定したカテゴリのすべての関数も含まれています。つまり、$ M[A]$でも安定しています。 $ f:T \ to M[U]$のような別のクラスの関数を明示的に導入したことに気付くかもしれません。 たとえば、$pure$と$constructor$は、$ U = T $の場合のそのような関数の例ですが、$pure$を使用してから$map$を使用する場合のように、明らかにもっと多くの関数が存在する可能性があります。 したがって、一般に、$ f:T \ to M[U]$の形式で任意の関数を処理する方法が必要です。
$ M[T]$に適用できる$f$に基づいて新しい関数を作成したい場合は、$map$を使用してみてください。 しかし、それは関数$ g:M [T] \ to M [M [U]] $になります。これは、もう1つのカテゴリ$ M [M [A]] $が必要ないため、適切ではありません。 この問題に対処するために、最後の関数$ flatMap:(T \ to M [U])\ to(M [T] \ to M [U])$を導入します。
しかし、なぜそれをしたいのでしょうか? ステップ2の後、つまり$ pure $、$コンストラクター$、および$map$があると仮定します。 toptal.comからHTMLページを取得し、そこにあるすべてのURLをスキャンして、それらをフェッチするとします。 1つのURLだけをフェッチしてHTMLページを返す関数$fetch:String \ to M[String]$を作成します。
次に、この関数をURLに適用し、toptal.comから$ x:M[String]$のページを取得します。 ここで、$ x $で変換を行い、最終的にURL $ u:M[String]$に到達します。 関数$fetch$を適用したいのですが、$ M[String]$ではなく$String$型を使用するため、適用できません。 そのため、$ fetch:String \ to M [String]$を$m_fetch:M [String] \ to M[String]$に変換するには$flatMap$が必要です。
3つのステップをすべて完了したので、実際に必要な値変換を作成できます。 たとえば、タイプ$ M [T]$の値$x$と$f:T \ to U $がある場合、$map$を使用して$f$を値$x$に適用し、値$y$を取得できます。タイプ$M[U]$の。 そうすれば、$ pure $、$コンストラクター$、$ map $、および$ flatMap $の実装にバグがない限り、値の変換を100%バグのない方法で実行できます。
したがって、コードベースで厄介な影響に遭遇するたびに対処するのではなく、これら4つの関数のみが正しく実装されていることを確認する必要があります。 プログラムの最後に、$ M [X] $が1つだけ表示され、値$ X $を安全にアンラップして、すべてのエラーケースを処理できます。
これがモナドです。$pure$、$ map $、および$flatMap$を実装するものです。 (実際、$map$は$pure$と$flatMap$から派生できますが、非常に便利で広く使用されている関数なので、定義から省略しませんでした。)
オプションモナド、別名多分モナド
では、モナドの実際の実装と使用法について詳しく見ていきましょう。 最初の本当に役立つモナドはオプションモナドです。 古典的なプログラミング言語を使用している場合は、悪名高いnullポインタエラーが原因で多くのクラッシュが発生した可能性があります。 nullの発明者であるTonyHoareは、この発明を「10億ドルの間違い」と呼んでいます。
これにより、無数のエラー、脆弱性、およびシステムクラッシュが発生し、過去40年間でおそらく10億ドルの苦痛と損害が発生しました。
それでは、それを改善してみましょう。 Optionモナドは、null以外の値を保持するか、値を保持しません。 null値と非常によく似ていますが、このモナドがあれば、nullポインターの例外を恐れることなく、明確に定義された関数を安全に使用できます。 さまざまな言語での実装を見てみましょう。
JavaScript-オプションモナド/多分モナド
class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }
Python-オプションモナド/多分モナド
class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()
Ruby-オプションモナド/多分モナド
class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()
Swift-オプションモナド/多分モナド
import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }
Scala-オプションモナド/多分モナド
import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]
まず、すべてのモナド実装のベースとなるMonad
クラスを実装します。 このクラスがあると非常に便利です。特定のモナドに対してpure
とflatMap
の2つのメソッドを実装するだけで、多くのメソッドを無料で入手できるからです(この例では、単純にmap
メソッドに限定していますが、一般的には多くのメソッドがあります。 Monad
の配列を操作するためのsequence
やtraverse
などの他の便利な方法)。
map
は、 pure
とflatMap
の合成として表現できます。 flatMap
の署名$flatMap:(T \ to M [U])\ to(M [T] \ to M [U])$から、$ map:(T \ to U)\に非常に近いことがわかります。 to(M [T] \ to M [U])$。 違いは、中央に追加の$ M $がありますが、 pure
関数を使用して$U$を$M[U]$に変換できます。 このようにして、 flatMap
とpure
の観点からmap
を表現します。
高度な型システムを備えているため、これはScalaに適しています。 また、動的に型付けされるため、JS、Python、Rubyでもうまく機能します。 残念ながら、Swiftでは静的に型付けされており、より高度な型の機能がないため、Swiftでは機能しません。そのため、Swiftでは、モナドごとにmap
を実装する必要があります。
また、OptionモナドはすでにSwiftやScalaなどの言語の事実上の標準であるため、モナドの実装にはわずかに異なる名前を使用していることに注意してください。
基本Monad
クラスができたので、Optionモナドの実装に取り掛かりましょう。 前述のように、基本的な考え方は、Optionが何らかの値を保持する( Some
と呼ばれる)か、まったく値を保持しない( None
)というものです。
pure
メソッドは単に値をSome
にプロモートしますが、flatMapメソッドはOption
の現在の値をチェックしますflatMap
の場合はNone
を返し、 None
Some
場合は基になる値を返し、 f()
をに適用します。それと結果を返します。
これらの2つの関数とmap
を使用するだけでは、nullポインター例外に陥ることは不可能であることに注意してください。 (問題はflatMap
メソッドの実装で発生する可能性がありますが、それはコード内の数行で一度チェックします。その後、コード全体でOptionモナド実装を数千の場所で使用します。 nullポインタ例外をまったく恐れる必要があります。)
どちらかのモナド
2番目のモナドに飛び込みましょう:どちらか。 これは基本的にOptionモナドと同じですが、 Some
はRight
と呼ばれ、 None
はLeft
と呼ばれます。 ただし、今回は、 Left
に基本的な値を設定することもできます。
例外のスローを表現するのは非常に便利なので、これが必要です。 例外が発生した場合、 Either
の値はLeft(Exception)
になります。 値がLeft
の場合、 flatMap
関数は進行しません。これは、例外をスローするセマンティクスを繰り返します。例外が発生した場合、それ以上の実行を停止します。
JavaScript-いずれかのモナド
import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure
Python-どちらかのモナド
from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False
Ruby-どちらかのモナド
require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end
スウィフト—どちらかのモナド
import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }
Scala-どちらかのモナド
package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }
また、例外をキャッチするのは簡単であることに注意してください。あなたがしなければならないのは、 Left
からRight
にマップすることだけです。 (ただし、簡潔にするために、例ではこれを行いません。)
未来のモナド
必要な最後のモナドであるFutureモナドを調べてみましょう。 Futureモナドは基本的に、現在利用可能であるか、近い将来利用可能になる値のコンテナです。 最初に解決される値に依存する次のコードを実行する前に、Future値が解決されるのを待つ、 map
とflatMap
を使用してFutureのチェーンを作成できます。 これは、JSのPromisesの概念と非常によく似ています。

現在の設計目標は、さまざまな言語の既存の非同期APIを1つの一貫したベースにブリッジすることです。 最も簡単な設計アプローチは、$コンストラクター$でコールバックを使用することです。
コールバックの設計により、JavaScriptやその他の言語でコールバック地獄の問題が発生しましたが、モナドを使用しているため、問題にはなりません。 実際、 Promise
オブジェクト(地獄をコールバックするJavaScriptのソリューションの基礎)は、モナドそのものです。
Futureモナドのコンストラクターはどうですか? この署名があります:
constructor :: ((Either err a -> void) -> void) -> Future (Either err a)
それを細かく分割しましょう。 まず、次のように定義しましょう。
type Callback = Either err a -> void
したがって、 Callback
は、エラーまたは解決された値のいずれかを引数として取り、何も返さない関数です。 これで、署名は次のようになります。
constructor :: (Callback -> void) -> Future (Either err a)
したがって、非同期計算がエラーまたは何らかの値に解決されるとすぐに、何も返さず、コールバックをトリガーする関数を提供する必要があります。 どんな言語への架け橋にもなるほど簡単に見えます。
フューチャーモナド自体のデザインについては、その内部構造を見てみましょう。 重要なアイデアは、Futureモナドが解決された場合に値を保持するか、それ以外の場合は何も保持しないキャッシュ変数を持つことです。 値が解決されるとすぐにトリガーされるコールバックを使用してFutureにサブスクライブできます。解決されない場合は、コールバックがサブスクライバーリストに追加されます。
Futureが解決されると、このリストの各コールバックは、別のスレッドで解決された値を使用して1回だけトリガーされます(JSの場合は、イベントループで実行される次の関数として)。同期プリミティブを慎重に使用してください。そうしないと、競合状態が発生する可能性があります。
基本的なフローは次のとおりです。コンストラクター引数として提供される非同期計算を開始し、そのコールバックを内部コールバックメソッドにポイントします。 それまでの間、Futureモナドにサブスクライブして、コールバックをキューに入れることができます。 計算が完了すると、内部コールバックメソッドはキュー内のすべてのコールバックを呼び出します。 Reactive Extensions(RxJS、RxSwiftなど)に精通している場合は、非同期処理に非常によく似たアプローチを使用します。
FutureモナドのパブリックAPIは、以前のモナドと同様に、 pure
、 map
、およびflatMap
で構成されています。 また、いくつかの便利な方法が必要になります。
-
async
は、同期ブロッキング関数を受け取り、それを別のスレッドで実行します。 -
traverse
は、値の配列と、値をFuture
にマップする関数を受け取り、解決された値の配列のFuture
を返します。
それがどのように機能するか見てみましょう:
JavaScript-将来のモナド
import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )
Python-将来のモナド
from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()
Ruby-将来のモナド
require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end
Swift-Future Monad
import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }
Scala-Future Monad
package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }
ここで、 Future
のパブリックAPIに、スレッド、セマフォなどの低レベルの詳細が含まれていないことに注目してください。 基本的に必要なのは、コールバックで何かを提供することだけです。それだけです。
モナドからのプログラムの作成
では、モナドを使って実際のプログラムを作ってみましょう。 URLのリストを含むファイルがあり、これらの各URLを並行してフェッチするとします。 次に、簡潔にするために応答をそれぞれ200バイトにカットし、結果を出力します。
まず、既存の言語APIをモナディックインターフェイスに変換します(関数readFile
およびfetch
を参照してください)。 これで、それらを構成して、1つのチェーンとして最終結果を得ることができます。 すべての厄介な詳細がモナドに含まれているため、チェーン自体は非常に安全であることに注意してください。
JavaScript-サンプルモナドプログラム
import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)
Python—Sample Monad Program
import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()
Ruby—Sample Monad Program
require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop
Swift—Sample Monad Program
import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()
Scala—Sample Monad Program
import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }
There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.
Overhead: Done. Benefits: Ongoing
For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! 素晴らしい!