https://d226lax1qjow5r.cloudfront.net/blog/blogposts/the-monad-invasion-part-2-monads-in-action/the-monad-invasion_part2.png

モナドの侵略 - パート2:行動するモナド!

最終更新日 December 12, 2023

所要時間:1 分

こんにちは、友人たち、

前回の記事 "モナドの侵略-その1:モナドって何?"では、モナドを導入するために実践的なアプローチを選択し、徐々に自分自身のモナドを構築していきました。ここまでで、モナドの概念に慣れ、モナドの値を .Map()または .Bind()で値を変換し .Match().

この記事では、様々なモナドの実用的なアプリケーションを紹介します。 Vonage .NET SDK.その前に、簡単に復習しておきましょう。

簡単な総括

以前、我々は Optionalモナドは2つの状態のどちらかで存在できる: Some値の存在を示す None値がないことを示す

private static SchrodingerBox<int> Half(int value) =>
    value % 2 == 0 
        ? SchrodingerBox<int>.Some(value / 2)
        : SchrodingerBox<int>.None();

private static int Increment(int value) => value + 1;

SchrodingerBox<int>.Some(0)
    .Map(Increment) // Some(0) becomes Some(1)
    .Map(Increment) // Some(1) becomes Some(2)
    .Bind(Half) // Some(2) becomes Some(1)
    .Map(Increment) // Some(1) becomes Some(2)
    .Match(some => $"The value is some {some}!", () => "The value is none")
    .Should()
    .Be("The value is some 2!");

SchrodingerBox<int>.Some(0)
    .Map(Increment) // Some(0) becomes Some(1)
    .Bind(Half) // Some(1) becomes None - the Monad's state has changed
    .Map(Increment) // None remains None
    .Match(some => $"The value is some {some}!", () => "The value is none")
    .Should()
    .Be("The value is none");

我々のボックスは3つの重要な機能を提供する:

  • .Map(operation)を使うと、既存の値に基づいて新しい値を生成し、その新しい値を新しいボックスに入れることができます。この操作は、ボックスが Some状態である場合にのみ実行されます。

  • .Bind(operation)に似た変換メカニズムである。 .Map().ただし、関数は値の代わりにボックスを返すので、ボックスの状態を変更できる点が異なる。.BOXのように .Map()のように、ボックスが Someの状態である場合にのみ実行されます。

  • .Match(some, none)はボックスの状態を評価し、対応する関数を呼び出す。

このボックスを使えば、現在の状態を知らずに値を操作することができる。

要するに、操作の順序が変わらないので、状態は無関係になる。お気づきのように、このコードには分岐構文(if/else)がない。

モナドを使う価値があるのはどんなときか?

An image for exposing two different states

モナドの対極にある状態という性質を考えると、モナドは結果が1つしかない操作のシナリオでは無関係であることがわかる。モナドが適用されるためには、オペレーションは少なくとも2つの異なる結果を提示しなければならない。

これまでは Optionalモナドを扱ったが、他にも多くのモナドが存在する。他のモナドの例を共有するために、ライブラリーがどのモナドを扱っているかを調べてみよう。 言語拡張がどのようなモナドを提供しているのかを調べ、その有用なコンテキストを見つけましょう。

オプション

以前の実装を考えれば、もうこの実装にはかなり慣れているはずだ。オプショナルな値や、存在するかもしれない(あるいは存在しないかもしれない)値をラップするときに威力を発揮する。

private async Task<Option<User>> FindUser(Guid id)
{
    var user = await this.repository.Users.FirstOrDefaultAsync(user => user.Id == id);
    return user ?? Option<User>.None;
}

試す

モナドは Try<T>モナドは失敗する可能性のある操作を表す:

  • Successは操作が成功し、結果が得られたことを示す。 <T>.

  • Exceptionは操作が例外になったことを示す。モナドは例外を投げる代わりに、その例外を返します。

private static Try<decimal> Divide(decimal value, decimal divisor) => 
        Try(() => value / divisor);

上記の例では、潜在的に "リスキー "な操作を Try.

  • を呼び出すと Divide(50,2)を呼び出すと Successを返します。 25.

  • を呼び出すと Divide(50,0)を呼び出すと Exception状態を返します。 DivideByZeroException.

どちらか

モナドは Either<L, R>モナドは、次の2つの異なる型を返すことができる演算を表す。 Leftまたは Right.このモナドは非常に汎用性が高い。 <L, R>はどちらもジェネリックスの型である。とはいえ Left状態は通常、エラーや例外的なケースを表す。

private static Either<Error, decimal> Divide(decimal value, decimal divisor) =>
    divisor == 0
        ? new Error("Cannot divide by 0.")
        : value / divisor;

private record Error(string Message);

前回のシナリオ通りだ:

  • を呼び出すと Divide(50,2)を呼び出すと Rightを返します。 25.

  • を呼び出すと Divide(50,0)を呼び出すと Left状態を返します。 Errorが返される。

バリデーション

モナドは Validation<Fail, Success>モナドは、複数の理由で失敗する可能性のある検証操作を表します。

private record User(string Firstname, string Lastname, MailAddress Email);

private static Validation<string, User> CreateUser(string firstname, string lastname, string email)
{
    var errors = new Seq<string>();
    if (string.IsNullOrWhiteSpace(firstname))
    {
        errors = errors.Add("Firstname cannot be empty.");
    }
    
    if (string.IsNullOrWhiteSpace(lastname))
    {
        errors = errors.Add("Lastname cannot be empty.");
    }

    if (!MailAddress.TryCreate(email, out var address))
    {
        errors = errors.Add("Invalid mail address.");
    }

    return errors.Any()
        ? Validation<string, User>.Fail(errors)
        : Validation<string, User>.Success(new User(firstname, lastname, address));
}

この例では

  • を呼び出すと CreateUser("Jane", "Doe", "jane.doe@email.com")を呼び出すと Success有効な Userインスタンスを返します。

  • を呼び出すと CreateUser(null, null, null)(または無効な値)を呼び出すと、発生したエラーのコレクションとともに Failureを返します。

参照の透明性

An image to illustrate transparency

これらのモナドには共通点がある。 透明性がある失敗したり、例外を投げたり、さまざまな値を返したりすることができる。そのため、メソッドの戻り値型は、目に見えず予測不可能なメカニズム(例外)の代わりに、それらの結果の結果を伝えなければなりません。

ここでは 参照の透明性.これは 常に式をその値で置き換えることができる場合に適用されます。

// Referentially Opaque examples
public User FindUser(Guid id) => this.users.First(user => user.Id == id);
public int Divide(int a, int b) => a/b;
public void SetName(string name)
{
    ArgumentNullException.ThrowIfNull(name);
    this.Name = name;
}

// Referentially Transparent examples
public Maybe<User> FindUser(Guid id) => this.users.FirstOrDefault(user => user.Id == id) ?? Maybe<User>.None;
public Try<decimal> Divide(decimal value, decimal divisor) => Try(() => value / divisor);
public int Add(int a, int b) => a+b;
public Either<Error, Unit> SetName(string name)
{
    if (name is null)
    {
        return new Error("Name cannot be null);
    }
    
    this.Name = name;
    return Unit.Value;
}

お気づきだろうが .SetName()Either<Error, Unit>.のようなライブラリで Unitのようなライブラリで MediatR言語エクストラ.これは、可能な値が1つしかない型を表す単純な構造体である。値を返さないが、別の状態を返す可能性のある操作のプレースホルダとして使用する。この例では .SetName()は値を返しませんが、失敗するかもしれません。.したがって、モナド Either<Error, Unit>は2つの状態を持つ:右(値なし)か左(エラーあり)だ。

参照透過は本質的に特定の問題を解決するものではないが、コードの予測可能性と可読性を大幅に向上させる。メソッドのシグネチャですべてを明示することで、予期せぬことが起こる可能性を最小限に抑えることができる。 ボブおじさんは、著書『Clean Code:アジャイルソフトウェアクラフトマンシップのハンドブック "では、次のように強調している。読む時間と書く時間の比率は、10対1をはるかに超えている。新しいコードを書く努力の一環として、私たちは常に古いコードを読んでいる。したがって、読みやすくすることで書きやすくなる".このことは、理解力と効率的な開発の両方を促進する上で、コードの明確性と透明性が重要であることを強調している。

これが"コードベースから...例外を放り出す".

Vonage SDKの例

ここまでは、比較的単純な例に焦点を当ててきた。しかし、もっと複雑なフローでモナドを使うのはどうでしょうか?私たちの .NET SDK二要素認証API Verify.

カスタムモナド

前回は Optionalを構築し 言語拡張をベースにした既存のセットを紹介したが、様々なライブラリもモナドの実装を提供している。我々の SDKのような外部ライブラリに依存することは意図的に避けました。 言語エクストラ.実際、モナドはSDKのパブリックAPIの一部であり、外部ライブラリに依存することは、私たちのコントロールが制限される依存関係を導入することになります。

私たちのアプローチでは、SDKの特定のニーズに合わせてカスタムモナドの実装を作成しました。この戦略により、外部ライブラリへの依存を避けながら、モナドのデザインと機能の制御を維持することができました。

さらに、私たちの目標は、特定のモナドの軽量でユーザーフレンドリーなバージョンを提示し、私たちのSDKで作業する開発者がより簡単に採用できるようにすることでした。

私たちのSDKは以下のモナドを実装しています:

  • Result<T>モナドに似ている。 Either<IFailure, T>C#はジェネリックに関して冗長である。

  • Maybe<T>モナドに似ている。 Option<T>.

二要素認証の例

2FAは2段階のワークフローです。最初に認証プロセスを開始し、指定されたワークフロー(SMS、WhatsApp、Eメール、Voice、SilentAuth)に基づいた認証コードを顧客に受け取らせます。コードを受信すると、検証のためにAPIに送信します。

var myPhoneNumber = ...
var result = await StartVerificationRequest.Build()
    .WithBrand("Monads Inc.")
    .WithWorkflow(SmsWorkflow.Parse(myPhoneNumber))
    .Create() // Build the authentication request
    .BindAsync(request => verifyClient.StartVerificationAsync(request)) // Process the authentication request
    .BindAsync(VerifyCodeAsync) // Start the second step if it's a success
    .Match(AuthenticationSuccessful, AuthenticationFailed);

private async Task<Result<Unit>> VerifyCodeAsync(StartVerificationResponse response)
{
    var myCode = ... // Receive verification code based on the specified workflow
    return await response
        .BuildVerificationRequest(myCode) // Build the verification request
        .BindAsync(request => verifyClient.VerifyCodeAsync(request)); // Process the verification request
}

ワークフローにはユーザー入力の取得、操作の解析、APIの呼び出しが含まれるため、これまで説明したことはすべてそのまま適用できる。

モナドの状態に関係なく一貫したフローを維持することができる。このプロセスは、5つの異なる場所で失敗する可能性がある:

  • 認証リクエストを作成するとき

  • 認証リクエストを処理するとき

  • 検証リクエストの作成時

  • 検証リクエストを処理する場合

  • 第2ステップの呼び出し時 VerifyCodeAsync

前回の記事で、モナドをできるだけ長くアクティブに保つことについて話したのを覚えているだろうか。このシナリオでは、値はフローの最初から最後まで Result<T>に保持され、一連の操作全体を連鎖させることができる。

純粋であること、あるいは純粋でないこと...純粋か?

An image to illustrate purity

先ほどの例では、モナドは純粋なままではなかった。 純粋なにとどまっていないことにお気づきだろう。実際 純粋でない関数を .Bind()のような request => verifyClient.StartVerificationAsync(request)または request => verifyClient.VerifyCodeAsync(request).問題はないのだろうか?

定義によれば 純粋な関数 はグローバルな状態そして 副作用を発生させない.それは一貫して入力にのみ依存する出力を生成し、特定の入力に対して同じ出力を保証する、言い換えれば高い予測可能性である。

純粋なモナド」という概念が議論されるかもしれないが、モナドを使うからといって、必ずしもモナド自体が純粋である必要はないことを理解しておく必要がある。 純粋.その代わり、モナドはしばしば 不純モナドは、関数型プログラミングの原則を守りながら、不純な操作を含む計算を構造化するために使われることが多い。実際、データベースやAPIのような外部リソースとの相互作用は副作用をもたらす。

不純モナド...例外あり?

デフォルトの動作は、モナド内のパラメータ関数が例外をスローしても、モナドが例外をスローしないことを保証する。この設計は referential transparency.

public Result<TB> Bind<TB>(Func<T, Result<TB>> bind)
{
    try
    {
        return this.IsFailure
            ? Result<TB>.FromFailure(this.failure)
            : bind(this.success);
    }
    catch (Exception exception)
    {
        return SystemFailure.FromException(exception).ToResult<TB>();
    }
}

public Result<TB> Map<TB>(Func<T, TB> map)
{
    try
    {
        return this.IsFailure
            ? Result<TB>.FromFailure(this.failure)
            : Result<TB>.FromSuccess(map(this.success));
    }
    catch (Exception exception)
    {
        return SystemFailure.FromException(exception).ToResult<TB>();
    }
}

しかし、もし例外にこだわるのであれば、値を抽出する代わりに .Match()?私たちは、モナドを多用途に使えるようにしたかったので GetSuccessUnsafe()関数を導入した。この関数は、モナドが Failure状態である場合に例外をスローする。

public T GetSuccessUnsafe() => this.IfFailure(value => throw value.ToException());

例外の型とデータは、基礎となる失敗値に依存する:

  • ResultFailureをスローします。 VonageException

  • ParsingFailureをスローします。 VonageException

  • HttpFailureをスローします。 VonageHttpRequestException

  • AuthenticationFailureをスローします。 VonageAuthenticationException

  • などなど...。

前回と同じ例を使って、モナド・フローに例外を組み込む方法を説明しよう:

try
{
    await StartVerificationRequest.Build()
        .WithBrand("Monads Inc.")
        .WithWorkflow(SmsWorkflow.Parse(myPhoneNumber))
        .Create()
        .BindAsync(request => verifyClient.StartVerificationAsync(request))
        .BindAsync(VerifyCodeAsync)
        .GetSuccessUnsafe(); // Will throw exception if in the Failure state
    AuthenticationSuccessful();
}
catch (Exception exception)
{
    AuthenticationFailed(exception);
}

あなたが標準的なモナド・アプローチを選ぶにせよ、例外処理を選ぶにせよ、私たちの目標は、エラー処理のさまざまなスタイルに対応し、私たちのモナドがあなたのコーディングの好みに沿うようにすることです。

まとめ

モナドの侵略」シリーズの2回目の投稿が終わりました。この記事は、私たちのカスタム実装を含む様々なモナドのセットを実証し、拡張ワークフローでの使用を説明することを目的としています。

この時点で、Monadsがワークフローのエラー処理に別のアプローチを提供していることに気づくはずだ。実際、今度の投稿では、操作の連鎖の背後にある方法論にもっと光を当てる予定です。ご期待ください!

何か質問があったり、おしゃべりしたいことがあれば、遠慮なく私の リンクトインまたは VonageデベロッパーSlack.

ではまた!

シェア:

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

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