https://d226lax1qjow5r.cloudfront.net/blog/blogposts/the-monad-invasion-part-1-whats-a-monad/the-monad-invasion.png

モナドの侵略 - パート1:モナドとは何か?

最終更新日 November 30, 2023

所要時間:1 分

やあ、

プレゼンテーションの最中に、開発者を逃げ出させる可能性のあることが2つある。ひとつは、ひどく退屈なプレゼンをするスピーカーであり、もうひとつは、「モナド」という言葉を口にしただけのスピーカーである。

モナドは人々を怖がらせるという評判がある。カテゴリー理論 カテゴリー理論に入ると、「モナド」のような複雑な専門用語が入り組んで、シンプルでわかりやすい説明をするのが難しくなる。

この投稿は、モナドの謎を解き明かし、モナドの恩恵を受けられるようにすることを目的としたシリーズの最初の投稿です。 .NET SDK.関連する講演を見るコードベースから...例外を投げ出す"は、タイトルがそうでなくても、例外についてというよりもモナドについてでした。

モナドって何?

がっかりさせて申し訳ないが、理論的な側面には飛び込まない。私はこう言うことができる。 モナドはエンドファンクターのカテゴリーからのモノイドであるしかし、そこからどこへ行くのですか?

簡単に言うと、ファンクターとは関数をマッピングできる箱のようなものだと考えてほしい。モナドはファンクターから一歩進んだものだ。モナドは、関数をマッピングできるだけでなく、操作の順序を制御する特別な機能も備えた、超強力な箱のようなものだ。

わかった?まだですか?大丈夫です!もっと実戦的なシナリオに切り替えて、実例を挙げながら掘り下げていきましょう。

シュレディンガーの猫

An image illustrating Schrodinger's cat

そう、この例は量子力学から来ているのだが、比較的単純なことなので、パニックになる必要はない。

箱と猫がいるシナリオを想像してほしい。しかし、この箱は普通の箱とは違う。というのも、この箱の中には、いつ猛毒を放出するかわからない装置が入っているからだ。

エルヴィン・シュレーディンガーの視点とは、猫を箱の中に入れて閉じた瞬間、その猫が生きているか死んでいるかは、再び箱を開けるまで判断できない 再び箱を開けるまで.猫は 同時に死んでいる 生きている。生きている。 どちらか死んでいるか あるいはしかし、実際の状態を観察できるまでは、両方の可能性を考慮しなければならない。

これは、量子の重ね合わせの概念を説明する方法である。 量子の重ね合わせ観測が可能になるまで、ある状況が複数の状態で存在するという考えを受け入れなければならない。

何を言いたいのかと思うかもしれないが、コードを見てみよう。

猫のいる箱

免責事項:これらのコード・スニペット作成中、猫に危害は加えられなかった。

An imagine illustrating cats with their thumbs Up

このボックスの比較的簡単な実装を紹介しよう:

internal class SchrodingerBox
{
    private readonly Cat? cat;

    // Creates a box with an alive cat inside
    private SchrodingerBox(Cat cat) => this.cat = cat;

    // Creates a box with a dead cat
    private SchrodingerBox()
    {
    }

    // Look inside the box
    public Cat? OpenBox() => this.cat;

    // Shakes the box. The cat doesn't like it. Meow.
    public SchrodingerBox Shake()
    {
        this.cat?.Meow();
        return this;
    }

    // Shakes the box (too hard), then returns a new box with a dead cat
    public SchrodingerBox ShakeTooHard()
    {
        this.Shake();
        return new SchrodingerBox();
    }

    // Creates a box with a cat
    public static SchrodingerBox WithAliveCat(Cat cat) => new(cat);
}

internal readonly struct Cat
{
    private readonly Action<string> log;

    public Cat(Action<string> log) => this.log = log;

    public void Meow() => this.log("Meow");
}

ここでいくつかの重要なポイントを強調したい:

  • パブリックAPIとやりとりする場合、生きている猫のいる箱を作成する唯一の方法は 静的ファクトリーメソッド .WithAliveCat(Cat).

  • Catは構造体であるため、NULLにすることはできない。この文脈では、catフィールドがnull可能であると宣言されたとき、そのインスタンス Catのインスタンスは生きている猫を表し、nullは猫が亡くなったことを表すと理解できます。

  • 猫の状態を示すプロパティがないため、箱の中の猫を直接調べる方法はない。猫が生きているのか死んでいるのかを判断するには、箱の中の .OpenBox()メソッドを使って箱を開ける必要があります。

このボックスは3つの異なるビヘイビアを提供する:

  • 箱を揺する:もし猫が生きていれば、揺さぶりを嫌がり「ニャー」と鳴くかもしれない。ヌル伝搬演算子を使うことで、猫が生きているときだけ .Meow()が呼び出されることを保証する。猫が生きていない場合は何も起こらない。

  • 箱を強く揺する:前の行動と同様、生きていれば「ニャー」と鳴く。しかし、この場合は毒が放出され、猫は不幸な最期を遂げる。

  • 箱を開ける:箱を開けると、生きている(インスタンス)猫か、死んでいる(ヌル)猫を受け取る。

このボックスの動きを見るために、いくつかのコードを見てみよう:

private readonly ITestOutputHelper helper;
private Cat GetAliveCat() => new(value => this.helper.WriteLine(value));

// "Meow" is logged three times in the console.
SchrodingerBox.WithAliveCat(this.GetAliveCat())
    .Shake()
    .Shake()
    .Shake();

// "Meow" is logged twice in the console, given we killed the cat on the second call.
SchrodingerBox.WithAliveCat(this.GetAliveCat())
    .Shake()
    .ShakeTooHard()
    .Shake();

// OpenBox() returns an instance when the cat is alive.
SchrodingerBox.WithAliveCat(this.GetAliveCat())
    .Shake()
    .OpenBox()
    .Should()
    .Be(this.GetAliveCat());

// OpenBox() returns null when the cat is dead.
SchrodingerBox.WithAliveCat(this.GetAliveCat())
    .ShakeTooHard()
    .OpenBox()
    .Should()
    .BeNull();

猫の状態を確認することなく、箱を操作できることに気づくだろう。猫が亡くなっているときに箱を揺すっても、何の結果にもつながらない。

さて、それはそれでいいのだが、現在の形では、この例はあまり実用的ではない。このボックスをコードベースに組み込んでも、実際の問題には対処できない。

しかし、だからといって完全に捨ててしまうわけにはいかない。現在、私たちの箱は猫を収容している。 Catは特定の構造体でしかない。ジェネリックの力を使えば、どんな型にも入れ替えることができる!

Tの箱

An image illustrating a box of tea

キャットをジェネリックタイプに置き換える場合、熟考すべき重要な点がある:箱を振っても意味がない。そのような行動の目的は何だろうか?

この先、私たちはボックスの中にどんな値でも入れることができるようにする。値を変異させたり、値を別の型に変換したりといった機能を提供する必要がある。

ここで .Map<TResult>(Func<T, TResult>)が登場する!

internal class SchrodingerBox<T> where T : struct
{
    private bool IsSome { get; }
    private readonly T value;

    // Constructor for Some
    private SchrodingerBox(T value)
    {
        this.value = value;
        this.IsSome = true;
    }

    // Constructor for None
    private SchrodingerBox() => this.IsSome = false;

    // Applies the function on the value IF our box contains a value
    public SchrodingerBox<TResult> Map<TResult>(Func<T, TResult> map) =>
        this.IsSome
            ? Some(map(this.value))
            : SchrodingerBox<TResult>.None();

    // Creates a box without a value
    public static SchrodingerBox<T> None() => new();

    public T? OpenBox() => this.IsSome ? this.value : null;

    // Creates a box with a value
    public static SchrodingerBox<TResult> Some<TResult>(TResult value) where TResult : struct => new(value);
}

この操作は Map操作は大きな進歩を意味し、基礎となる値を "変更 "できるようになる。値を直接変更するわけではないので、「変更」という表現は適切ではないかもしれない。その代わりに、別のコンテナに入れられた新しい値を生成するために使っているのだ。

この操作は、LinQの .Select(Func<TSource, TResult>)に似ていると考えることができる。

// We start with 3 and perform three increments. The result is then 6.
SchrodingerBox<int>.Some(3)   
    .Map(value => value + 1)
    .Map(value => value + 1)
    .Map(value => value + 1)
    .OpenBox()
    .Should()
    .Be(6);

// The box is empty, and despite three increments, the result remains null.
SchrodingerBox<int>.None()
    .Map(value => value + 1)
    .Map(value => value + 1)
    .Map(value => value + 1)
    .OpenBox()
    .Should()
    .BeNull();

このコンセプトは、前回の .Shake()メソッドと一致している:

ボックスに値がない場合は、何もしない。

An image illustrating the None status

しかし、ボックスに値が入ると、その値に関数が適用され、その結果が新しいボックスに送られる。

An image illustrating the Some status

ソースアディティヤ・バルガバ ブログ- おそらく、このメカニズムを説明するために私が見た中で最高の図面だろう。

簡単な変換操作しかカバーしていないにもかかわらず、ボックスをファンクターに変えることができた!先ほどの説明を覚えているだろうか?

「ファンクターとは、関数をマッピングできる箱のようなもので、箱そのものを変化させることなく、箱の中にあるものに関数を適用することができる。

では、私たちの箱がモナドに進化するためには何が足りないのだろうか?

モナディック・バインド

を使用しているときに .Map<TResult>(Func<T, TResult>)を使用すると、ボックスの内部状態 を使用しても.であろうと Someまたは None状態であろうと、マッピングすればその状態が保持され、それを変更することは不可能である。

しかし、ここで Bindメカニズムにスポットライトが当たる!とは異なり Map値を返す関数を受け取る Bindメソッドは 新しいボックスを返す関数を返す関数を予期している。 Func<T, SchrodingerBox<TResult>>.

との比較を見てみよう。 Map:

public SchrodingerBox<TResult> Map<TResult>(Func<T, TResult> map) where TResult : struct =>
    this.IsSome
        // The outcome of the "map" operation is a result,
        // which means we need to wrap this value within a box
        ? Some(map(this.value))
        : SchrodingerBox<TResult>.None();

public SchrodingerBox<TResult> Bind<TResult>(Func<T, SchrodingerBox<TResult>> bind) where TResult : struct =>
    this.IsSome
        // The outcome of the "bind" operation is already a box,
        // so, we can return it
        ? bind(this.value)
        : SchrodingerBox<TResult>.None();

を使えば Bindを使えば、特定のビジネスロジックに基づいてボックスの状態を変更するビヘイビアを実装できる。あなたはこう思うかもしれない:「猫を入れた最初のボックスはすでにそうしていた。しかし、重要な違いがある、 箱が責任を負っていたロジック(.ShakeTooHard()).しかし今は違う、 関数.我々はこれを呼び出し側に委譲することに成功し、ボックスの可能性を広げることができた。

次の例では、新しいメソッドを紹介する、 .Increment(int)を導入する。しかし、値が3以上であれば、空のボックスを返します。

private static SchrodingerBox<int> Increment(int value) =>
    value < 3 ? SchrodingerBox<int>.Some(value + 1) : SchrodingerBox<int>.None();

//  We successfully incremented our value to three
SchrodingerBox<int>.Some(0)
    .Bind(Increment)
    .Bind(Increment)
    .Bind(Increment)
    .OpenBox()
    .Should()
    .Be(3);

// On the last call, our "Increment" method returns an empty box
// This signifies our box transitioned from a "Some" state to a "None" state.
// The box didn't not produce the change; the ".Increment(int)" function did.
SchrodingerBox<int>.Some(0)
    .Bind(Increment)
    .Bind(Increment)
    .Bind(Increment)
    .Bind(Increment)
    .OpenBox()
    .Should()
    .Be(null);

私たちのボックスがついにモナドになった!これで MapBindの両方の機能を持つようになり、初期状態を知らなくてもラップされた値を操作できるようになった。

しかし、もうひとつ取り組むべきことがある。

箱を開ける

An image illustrating a gift box

私たちのモナドは、しばしばオプショナル(OptionまたはMaybe、あなたの言語の好みに応じて)として知られ、2つの状態のいずれかになることができます:それは値の存在(Some)または値の不在(None)を表すことができます。

現在、箱を開けたとき、中に値がなければnullを返すかもしれない。しかし、これはモナドの意図に沿わない。なぜなら、値がないということはnull値とは異なるからだ。

これは興味深い問題を提起している:価値がないことをどのように表現するのか?その代わりに、フォールバック動作を提供するのだ。

そこで .Match<TResponse>(Func<T, TResponse> some, Func<TResponse> none)が登場する!

// Applies a function depending on the state of the box to return a TResponse
public TResponse Match<TResponse>(Func<T, TResponse> some, Func<TResponse> none) =>
    this.IsSome ? some(this.value) : none();

.Match(some, none)はモナドの状態を評価し、対応する関数を呼び出す。

  • ステートがSomeの場合 someを呼び出す。

  • ステートがNoneの場合は noneを起動し、結果を生成するフォールバック・メカニズムに依存する。

では Matchの代わりに Openboxを適用してみましょう:

// When the stae is Some(3)
// Match will employ the some function
// and return "The value is some 3!"
SchrodingerBox<int>.Some(0)
    .Bind(Increment)
    .Bind(Increment)
    .Bind(Increment)
    .Match(some => $"The value is some {some}!", () => "The value is none")
    .Should()
    .Be("The value is some 3!");

// When the state is None (because Bind returned an empty box in the last call),
// Match will employ the none function
// and return "The value is none"
SchrodingerBox<int>.Some(0)
    .Bind(Increment)
    .Bind(Increment)
    .Bind(Increment)
    .Bind(Increment)
    .Match(some => $"The value is some {some}!", () => "The value is none")
    .Should()
    .Be("The value is none");

当初、私たちの箱には int.ボックスから値を抽出するとき、私たちは「全体像」に焦点を当て、エンドユーザー向けのメッセージを生成した。別のアプローチとして、状態がNoneのときにデフォルトの intを提供するというアプローチもありました。

ケーキの上のサクランボ:我々は無効性を取り除くことに成功した!

注目すべきは、値があってもなくても、ワークフロー全体が一貫していることだ。これは偶然ではなく、このシリーズの後半で根本的な理由を探ることにしよう。

しかし、このシフトには、価値の目的についてより広い考察が必要である。無効という選択肢がなくなったとき、価値の不在はどのような意味を持つのだろうか?

エンド・ツー・エンドのオペレーションという意味では?

いつ箱を開けるべきか?

An image illustrating a family opening christmas gifts

これはしばしば個人が困難に直面するポイントであり、少なくとも私の場合はそうだった。

ボックスの中に値を保持することは、両方の状態が異なる副作用をもたらす限り、理にかなっている。 明確な副作用.

例えば、モナドの寿命が比較的短いのは、かなり早い段階で値を生成できる場合である。 .Match(some, none)の例で示したように。逆に、ユーザーの更新のように、フロー全体が値の存在に依存している場合は、最後までそれにこだわらなければならない。

モナドをできるだけ長くアクティブにしておき、最新のタイミングで値を取り出すことをお勧めする。

州がすべて

An image illustrating traffic lights

モナドは状態の管理と操作の概念を中心に展開する。

もちろん、私たちのモナドは数あるモナドの中の一例に過ぎず、それぞれが様々な状態を扱うように設計されている。以下に、他のモナドをいくつか、それらが扱うさまざまな状態とともに紹介しよう:

  • 結果(成功|失敗)

  • どちらか (左|右)

  • バリデーション (成功|バリデーションエラー)

  • その他

このプレゼンテーションのために、私たちはカスタムボックスを開発しました!

既製のモナドを提供するライブラリーは数多くある。いくつかお勧めを紹介しよう:

まとめ

この記事を楽しんで読んでいただけたなら幸いです。初めてモナドに飛び込むときは厄介かもしれないが、私を信じてほしい。モナドは私のソフトウェアへの取り組み方を変えてくれた。

何を覚えているか ダグラス・クロックフォードかつてダグラス・クロックフォードが言った言葉を思い出してほしい。[...)モナドもまた呪われている。モナドの呪いとは、(...)一度理解してしまうと、それを誰かに説明する能力を失ってしまうことだ。"

先に述べたように、このブログ記事はウォーミングアップに過ぎない。このトピックは非常に膨大なため、1つの記事には収まりきらない。次回の記事では、クールな新ネタを探ろうとしている。それでは、ご期待ください。

何か質問があったり、ただおしゃべりしたいだけなら、気軽に私の リンクトインまたは VonageデベロッパーSlack.

また後で!

シェア:

https://a.storyblok.com/f/270183/384x384/fdffb72c8b/guillaume-faas.png
Guillaume Faasシニア.Netデベロッパー

ギヨームはVonageのシニア.Netデベロッパー・アドボケイト。.Netで15年近く働いているが、ここ数年はSoftware Craftsmanshipの提唱に注力している。好きなトピックは、コード品質、テスト自動化、モビング、コード・カタなど。仕事以外では、妻や娘と過ごす時間、ワークアウト、ゲームを楽しんでいる。