https://a.storyblok.com/f/270183/1368x665/95d3cfca7c/the-monad-invasion_pt3.png

モナドの侵略 - パート3:鉄道指向プログラミング

最終更新日 November 14, 2024

所要時間:1 分

こんにちは、友人たち、

前回の記事 投稿、"モナドの侵略 - パート2:モナドの活躍!", では、様々なモナドと、それらが実際のシナリオにどのように適合するかを紹介した。

今日は、エラー処理への関数的アプローチであるROP(Railway-Oriented Programming)に焦点を当てる。

ネタバレ:これまでの記事を読んでくださった方は、実際に意識することなくROPを観察していた。

簡単な総括

私たちは実際の例として二要素認証(2FA)を使用した。この2段階のワークフローでは、まず認証を開始し を開始し、次にユーザーに送られたコードを検証する必要がある。

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
}

先に提案したように、このスニペットはすでにROPを適用している。あなたは今、たくさんの疑問を持っているでしょう。ROPとは何か? どのように機能するのか?なぜ便利なのか?

最初から始めようか?

失敗は想定内

A pile of dynamiteエラーを処理せずにソフトウェアを作ることは、厳密には不可能である。現実の世界では、エラーは実行中のどの時点でも発生する可能性があり、それを無視することはできない。 実行中に発生する可能性があり、それを無視することはできない。

同じワークフローを命令文体で書いたとしたらどうだろう?

var myPhoneNumber = ...
var request = await StartVerificationRequest.Build().WithBrand("Monads Inc.").WithWorkflow(SmsWorkflow.Parse(myPhoneNumber)).Create();  
var response = verifyClient.StartVerificationAsync(request);
var myCode = ... // Receive verification code based on the specified workflow
var verificationRequest = response.BuildVerificationRequest(myCode)
var result = verifyClient.VerifyCodeAsync(verificationRequest);    
this.AuthenticationSuccessful();

見た目は似ているが、何か重要なことが欠けている。
実際、このスニペットは、すべてが期待通りに進むハッピーパスだけを示している。

もちろん、これだけでは不十分で、これらのスニペット間のフィーチャー・パリティを目指している。 現在の状態では、何らかの障害が発生するとシステムがクラッシュしてしまう。 先に述べたように、このフローはさまざまな場所で失敗する可能性がある:

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

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

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

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

では、エラーを処理することにした場合、何が違うのか?

var myPhoneNumber = ...
var request = await StartVerificationRequest.Build().WithBrand("Monads Inc.").WithWorkflow(SmsWorkflow.Parse(myPhoneNumber)).Create();
if (request.IsFailure)
{
    // Can fail when Brand is invalid
    // Can fail when PhoneNumber is invalid
    return this.AuthenticationFailed(new Failure("Invalid input."));
}

var response = verifyClient.StartVerificationAsync(request);
if (response.IsFailure)
{
     // Can fail when process can't be initiated
     return this.AuthenticationFailed(new Failure("Cannot initiate verification process."));
}

var myCode = ... // Receive verification code based on the specified workflow
var verificationRequest = response.BuildVerificationRequest(myCode)
if (response.IsFailure)
{
    // Can fail when the code input is invalid
    return this.AuthenticationFailed(new Failure("Invalid code."));
}

var result = verifyClient.VerifyCodeAsync(verificationRequest);    
if (result.IsFailure)
{
    // Can fail when the verification fails
    return this.AuthenticationFailed(new Failure("Verification failed."));
}

return this.AuthenticationSuccessful();

"That escalated quickly" meme潜在的な失敗に対処するために、重要な定型コードを追加する必要がある。このコードは高密度で読みにくい。 失敗を処理するコードがハッピーパスそのものよりも多くなってしまったが、これは偶然ではない。 ハッピー・パスは1つしかないが、失敗する理由は複数ある。

ここで観察すべき重要なことがある:

  • このスニペットは、クイック総括セクションのものと同じ動作を提供する。 約3倍倍のコードを必要とする。

  • サイクロマティック複雑度も大幅に増加する、 から6まで、瞬く間に増加する。

もう興味は沸きましたか?そろそろ 鉄道指向プログラミング.

鉄道指向プログラミング

Railways

注:このセクションは 講演のリソースを共有します。鉄道指向プログラミング - 関数的な方法でのエラー処理" 講演者 Scott Wlaschin. 詳細は F#ForFunAndProfit.

鉄道指向プログラミングは、エラー処理に対する関数的アプローチである。 主な考え方は、「幸せな道」(緑)を主要な線路と考え、期待される振る舞いを提供することである。

Exposing both success and failure tracks上の例では、ワークフローは3つのステップで構成されている:

  • 入力を検証する。

  • レコードを更新する - 入力が有効な場合。

  • レコードの更新が成功した場合、通知を送信します。

命令形ではこんな感じだ:

if (this.Validate(input))
{
    try
    {
        var record = this.Update(input);
        this.SendNotification(record);
    }
    catch (Exception)
    {
        ...        
    }
}

この3つのステップが成功した場合のみ、ワークフローが成功したとみなす。

エラーに直面し、「失敗の道」(赤)に導かれるまで、私たちはその「幸せな道」を進み続ける。そうなると、私たちは 失敗をフローの最後まで持ち越すことになる。メイン・パスに固執することは非常に重要である。 ワークフローの完全な実行を保証する唯一の方法だからだ。

上の例では、バリデーションが失敗したときに更新ステップをスキップしている。 これは、関数が複数の状態を返すからこそ可能なのです。 成功または 失敗.

Meme skeptical faceピンときただろうか?なんという偶然だろう、私たちはモナドを使っているのだ! まさかこんなことになるとは思わなかったでしょう?

前回の記事 前回の投稿メソッドを使ってモナドの状態を別の状態に変更する方法を見ました。 .Bind()メソッドを使って、モナドの状態をある状態から別の状態に変更する方法を説明した。 この基本的な概念によって、トラック間の分岐を作ることができる。

ここには魔法はない。 分岐が魔法の杖で消えたわけでも何でもない。 分岐はモナドの内部で行われるようになったのだ。

カスタムモナドの Bindを見てみよう:

return this.IsFailure ? Result<TB>.FromFailure(this.failure) : bind(this.success);

我々の決定メカニズム(三項演算子)は、現在の状態が失敗か成功かを検証する。

  • 失敗の場合は、現在の失敗を返し、何も起こらない。

  • 成功すれば bind関数を呼び出す。

このように、オペレーションを連鎖させることができる。 に自動的に切り替わる。

以下は前回と同じワークフローだが、今回はROPを使っている:

var result = input.Bind(Validate).Bind(Update).Bind(SendNotification);

印象的だろう? 同じ機能をカバーしながら、コードを1行に減らしたのだ。

ちょっとしたゲームをしよう

以下は のコードサンプルです。からのコードサンプルです。 線路がどのように見えるかわかりますか? ワークフローはどこで失敗するのでしょうか?

ここで何が起こっているか知る必要はない。流れに集中するんだ。

public Task<Result<AuthenticateResponse>> AuthenticateAsync(Result<AuthenticateRequest> request) =>
    request.Map(BuildAuthorizeRequest)
        .BindAsync(this.SendAuthorizeRequest)
        .Map(BuildGetTokenRequest)
        .BindAsync(this.SendGetTokenRequest)
        .Map(BuildAuthenticateResponse);

答えは簡単で、私たちの鉄道には2つの分岐点がある:

  • authorizeリクエストを送信するとき。

  • トークン取得リクエストを送信するとき。

メソッドに依存しているからだ。 Bindメソッドに依存しているからである。

別の比較のために、同じコードを命令形で書いてみた:

public async Task<Result<AuthenticateResponse>> AuthenticateAsync(AuthenticateRequest request)
{
    var authorizeRequest = BuildAuthorizeRequest(request);
    var authorizeResponse = await this.SendAuthorizeRequest(authorizeRequest);
    if (authorizeResponse.IsFailure)
    {
        return Result<AuthenticateResponse>.FromFailure(authorizeResponse.Failure);
    }

    var getTokenRequest = BuildGetTokenRequest(authorizeResponse.Success);
    var getTokenResponse = await this.SendGetTokenRequest(getTokenRequest);
    if (getTokenResponse.IsFailure)
    {
        return Result<AuthenticateResponse>.FromFailure(getTokenResponse.Failure);
    }

    return BuildAuthenticateResponse(getTokenResponse.Success);
}

ご覧のように、命令文のスタイルに比べ、流れを読むのも理解するのもはるかに簡単である。 小さくなっている。 必要な技術情報(分岐、ロジック、変数など)の量を減らすことで、コードを「頭の中に収まる」ようにすることができます、 技術的な情報(分岐、ロジック、変数など)を少なくすることで、コードを「頭にフィットさせる」ことができるのです( 参照 「マーク・シーマン著「あなたの頭にフィットするコード).

プロセスが失敗する理由は複数ある

先ほどの例を見ると、ワークフローは2つのポイントで失敗する可能性がある。

私たちの失敗経路は、私たちが定義する Failure状態として定義したものを運びます。 私たちは、モナドに Successまたは Failure値 モナドを実装した。 ジェネリックス. これは Result<TFailure, TSuccess. どこに行こうとしているのかわかるだろうか?

C#ではジェネリックがどのように機能するかによって、インスタンス Resultのインスタンスは常に同じタイプの失敗を背負わなければならない。つまり すべての失敗は 同じ型- これは問題だ。 をAPIの失敗とは異なるものとして扱うことになるかもしれないからだ。

この問題をモナドやROPのせいにすることはできない。それに比べて F#のような言語では、複数の種類の失敗を運ぶことができる識別可能なユニオン型を定義することができるからだ。それでも 差別化ユニオン いずれは いずれはC#に導入されるだろう。

この制限を無視することはできないが、完全に行き詰まっているわけではない。異なる障害を同じジェネリック型の下でグループ化する一つの方法は 同じジェネリックタイプでグループ化する1つの方法は、基底クラスやインターフェイスを使用することです。

.NET SDKでは IResultFailure インターフェース を実装した。これにより、異なる失敗(または失敗する理由-解析、認証など)を同じ失敗パスにグループ化し、専用の動作を定義することができる、 認証など)を同じ失敗パスにグループ化し、プロパティを使って専用の振る舞いを定義することができます。 Typeプロパティを使って専用の振る舞いを定義することができます。

public interface IResultFailure
{
    /// <summary>
    ///     The type of failure.
    /// </summary>
    Type Type { get; }

    /// <summary>
    ///     Returns the error message defined in the failure.
    /// </summary>
    /// <returns>The error message.</returns>
    string GetFailureMessage();

    /// <summary>
    ///     Converts the failure to an exception.
    /// </summary>
    /// <returns>The exception.</returns>
    Exception ToException();

    /// <summary>
    ///     Converts the failure to a Result with a Failure state.
    /// </summary>
    /// <typeparam name="T">The underlying type of Result.</typeparam>
    /// <returns>A Result with a Failure state.</returns>
    Result<T> ToResult<T>();
}

これは完璧な解決策ではないし、おそらく最もエレガントなものでもない。しかし、これは異なるタイプの 例外を処理するのと似ている:

try { ... }
catch (ExceptionA) { ... }
catch (ExceptionB) { ... }
catch (ExceptionC) { ... }

// ---

switch (failure)
{
    case HttpFailure httpFailure:
        DoSomethingWithHttpFailure(httpFailure);
        break;
    case ResultFailure resultFailure:
        DoSomethingWithResultFailure(resultFailure);
        break;
    default:
        DoSomethingWithFailure(failure);
        break;
}

おわかりのように、ROPは例外と同じ機能を使うことができる。 ROPは例外処理と同じ機能を使うことができます。

結論

モナドの侵略」シリーズの3回目です。今回は、ROP(Railway-Oriented Programming:鉄道指向プログラミング)に焦点を当て、モナドを使ってエラーを処理する方法を紹介します。 プログラミング(ROP)に焦点を当て、モナドを使ってどのようにエラーを処理できるかを紹介します。

重要なことは?エラー処理にモナドを使っても、標準的な例外ベースのアプローチと比べて損失はない; 例外を使って行うことは、すべてモナドで行うことができます。例外を使って行うことはすべてモナドを使って行うことができます。 意図がより明確になります。

さて、本当の問題だ。

小さなことから始めて、徐々に慣れていくことをお勧めする。新しいことは何でもそうですが、学習曲線があります。 メリットが見えてくるはずだ。

何か質問があったり、おしゃべりしたいことがあれば、遠慮なく私に連絡してほしい。 私の LinkedInにフィードバックをお寄せください。 をシェアしてください。 .NET SDKリポジトリまたは または Vonage開発者Slack.また オン X の @VonageDev.あなたの声が重要です。

ではまた!

シェア:

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

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