1. 概要

開発者として、ビジネスドメインに直接関連付けることができない追加のコンテキストで値をカプセル化する必要が生じることがよくあります。 たとえば、多くの場合、値をシーケンスに含めるか、非同期で計算する必要があります。

モナドは、そのようなニーズを満たすために使用できるメカニズムです。 いくつかの例を見て、それらを理解しましょう。

2. モナドとは何ですか?

モナドは、いくつかの追加機能で拡張された値の周りの計算をシーケンスするメカニズムにすぎません。 モナドの概念は、数学から直接、正確には圏論から来ています。 このため、それはしばしば難しいトピックと見なされます。 ただし、可能な限り簡単にするつもりです。

すでに述べたように、モナドはいくつかの追加機能で価値を高めます。 このような機能はエフェクトと呼ばれます。 いくつかのよく知られている効果は、変数のnull可能性の管理、またはその計算の非同期性の管理です。 Scalaでは、これらのエフェクトに対応するモナドは、 Option[T]タイプとFuture[T]タイプです。

ご覧のとおり、OptionFutureの両方のタイプがタイプパラメーターを定義します。 実際、モナドは、コンテキストをラップする値に効果を追加します。 Scalaでは、モナドを実装する1つの方法は、型にパラメトリッククラスを使用することです。

たとえば、モナドを使用して、一般的なタイプTの値に怠惰の効果を追加してみましょう。 したがって、 Lazy[A]クラスを次のように定義します。

class Lazy[+A](value: => A) {
  private lazy val internal: A = value
}

Lazy 型は、 A 型の値をラップします。これは、「名前による呼び出し」パラメーターによってクラスコンストラクターに提供され、先行評価を回避します。

2.1. モナド内の値のラッピング:Unit関数

まず、モナドは、モナドのコンテキストで汎用値をラップできる関数を提供する必要があります。 通常、このような関数をunitと呼びます。 unit 関数は、モナディックコンテキストで値を持ち上げると言われています。 Scalaでは、コンパニオンオブジェクトの apply メソッドを使用して、unit関数を実装できます。

object Lazy {
  def apply[A](value: => A): Lazy[A] = new Lazy(value)
}

この例では、 unit 関数を使用すると、レイジー初期化の効果を値に追加して、Lazyコンテキスト内にラップすることができます。

val lazyInt: Lazy[Int] = Lazy {
  println("The response to everything is 42")
  42
}

したがって、上記のコードは、実行の怠惰が一項値に引き上げられるため、一度実行されると何も出力しません。

2.2. 値に対する計算の順序付け:flatmap関数

ただし、モナドに効果を追加する唯一の機能は、コードに追加された複雑さの価値がありません。 さらに、モナドのラップされた値を抽出して関数を適用したくありません。 それは面倒で保守不可能です。 モナド内にラップされた値に対して計算をシーケンスするメカニズムが必要です。

この問題を克服するには、モナドがflatMap関数を提供する必要があります。 この関数は、モナドによってラップされたタイプの値から、別のタイプに適用された同じモナドへの別の関数を入力として受け取ります。

def flatMap[B](f: (=> A) => Lazy[B]): Lazy[B] = f(internal)

モナド内の値を抽出を実行せずに別の値に変換して、単純化します。 したがって、lazyInt値をStringに変換する必要がある場合は、flatMap関数を使用できます。

val lazyString42: Lazy[String] = lazyInt.flatMap { intValue =>
  Lazy(intValue.toString)
}

繰り返しになりますが、計算がすべて怠惰であるため、文字列は標準出力に出力されません。 さらに、モナド内の値を抽出せずに変更しました。 したがって、 flatMap 呼び出しのチェーンを使用すると、ラップされた値への変換のシーケンスを作成できます。

2.3. それをより必須にする:理解のための使用

最後になりましたが、 flatMap 関数の観点から、任意のモナドのmap関数を定義できます。

def map[B](f: A => B): Lazy[B] = flatMap(x => Lazy(f(x)))

なぜそれをする必要がありますか? Scalaでmap関数とflatMap関数の両方を提供するタイプの場合は、for-comprehensionコンストラクトを使用できます(記事 A Comprehensive Guide to For-Comprehension in詳細については、Scala を参照してください)。 for-comprehensionは、同じモナドで計算を連結するのに非常に役立ちます

val result: Lazy[Int] = for {
  first <- Lazy(1)
  second <- Lazy(2)
  third <- Lazy(3)
} yield first + second + third

ご覧のとおり、上記のコードは読みやすく、命令型を使用してコーディングしているかのように、Scalaで関数型プログラミングとモナドを使用できます。

完全を期すために、上記の理解は、次の一連の関数を呼び出すことを意味します。

val anotherResult: Lazy[Int] =
  Lazy(1).flatMap { first =>
    Lazy(2).flatMap { second =>
      Lazy(3).map { third =>
        first + second + third
      }
    }
  }

素晴らしい改善ですね。

3. ひどい3つ:モナドの法則

しかし、大きな力には大きな責任が伴います。 実際、unit関数とflatMap関数を型に追加して、モナドにするだけでは不十分です。 複雑な部分には、モナドが満たさなければならない数学的法則が付属しています。

3つのモナド法は次のとおりです。

  • 左のアイデンティティ
  • 正しいアイデンティティ
  • 連想性

モナドが3つの法則を満たしている場合、unitおよびflatMap関数のアプリケーションのシーケンスが有効なモナドにつながることを保証します。つまり、モナドの効果は値はまだ保持されます。

モナドとその法則は、プログラミングの観点からデザインパターンを定義します。これは、一般的な問題を解決する真に再利用可能なコードです。

それらを一つずつ説明していきましょう。

3.1. 左のアイデンティティ

「左単位元」と呼ばれる3つの法則の最初の法則は、flatMap関数を使用して関数fを、によって持ち上げられた値xに適用することを示しています。 ] unit 関数は、関数fを値xに直接適用することと同じです。

Monad.unit(x).flatMap(f) = f(x)

Lazy モナドを使用する場合、次のことが成り立つことを証明する必要があります。

Lazy(x).flatMap(f) == f(x)

したがって、 flatMap(f) f(x)に置き換えることができるので、プロパティは定義上保持されます。

3.2. 正しいアイデンティティ

2番目の単元法は「正しい単位元」と呼ばれます。 ユニット関数を関数fとして使用してflatMap関数を適用すると、元のモナディック値が得られると記載されています。

x.flatMap(y => Monad.unit(y)) = x

Lazyモナドもこの法則を満たしていることを証明するのは簡単です。

Lazy(x).flatMap(y => Lazy(y)) == Lazy(x)

flatMap(y => Lazy(y))という用語をアプリケーションの結果 Lazy(x)に置き換えることができるため、このプロパティは定義上保持されます。

3.3. 連想性

3つのモナディック法則の最後は、対処するのが最も難しく、「結合法則」と呼ばれます。 この法則は、flatMap呼び出しのシーケンスを使用して2つの関数fおよびgをモナド値に適用することは、g fをパラメーターとして使用してflatMap関数を適用した結果:

x.flatMap(f).flatMap(g) = o.flatMap(x => f(x).flapMap(g))

したがって、 Lazy monadの場合、上記のルールは次のようになります。

Lazy(x).flatMap(f).flatMap(g) == f(x).flatMap(g)

「左単位元」の法則から導出された置換を方程式の右辺の項Lazy(x).flatMap(f)に適用すると、正確に f(x)が得られます。 flatMap(g)。 したがって、結合法則はモナドにも当てはまります🙂

4. ボーナスメソッド

これまで、モナドがパターンを順守するために実装しなければならないメソッドの最小セットを提示しました。 ただし、Lazyタイプの使いやすさを向上させるために他の多くのメソッドを定義できます。

たとえば、ネストされたモナドの型の抽象化のレベルを削除するflatten関数を実装できます。

def flatten(m: Lazy[Lazy[A]]): Lazy[A] = m.flatMap(x => x)

さらに、実装するもう1つの興味深い関数は、モナドに含まれる値を抽出する関数です。 それをgetと呼ぶことができます:

def get: A = internal

get 関数を呼び出すと、レイジー値が最終的に評価され、以前にモナドで囲まれていたエフェクトが実行されます。

5. 結論

この記事では、Scalaのモナドの概念を紹介しました。 モナドの簡単な定義から始め、次にモナドが実装しなければならない関数の最小セットであるunitflatMapを紹介しました。 最後に、3つのモナド法について話しました。

最後に、モナドは、Scala標準ライブラリの多くのタイプに浸透している魅力的で便利な概念です。 オプション将来いずれか、およびリストツリー、いくつか例を挙げると、Mapはモナドです。

いつものように、記事の完全なソースコードは、GitHubから入手できます。