web-dev-qa-db-ja.com

例外をキャッチ/スローすると、純粋なメソッドが不純になりますか?

次のコード例は、私の質問のコンテキストを提供します。

Roomクラスはデリゲートで初期化されます。 Roomクラスの最初の実装では、例外をスローするデリゲートに対するガードはありません。このような例外は、デリゲートが評価されるNorthプロパティにバブルアップします(注:Main()メソッドは、Roomインスタンスがクライアントコードでどのように使用されるかを示しています)。

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Northプロパティを読み取るときではなく、オブジェクトの作成時に失敗するので、コンストラクターをプライベートに変更し、Create()という静的ファクトリーメソッドを導入します。このメソッドは、デリゲートによってスローされた例外をキャッチし、ラッパー例外をスローして、意味のある例外メッセージを持ちます。

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Try-catchブロックはCreate()メソッドを不純にしますか?

27

はい。実質的に不純な機能です。副作用が発生します。プログラムの実行は、関数が戻ると予想される場所以外の場所で続行されます。

純粋な関数にするために、returnは、関数からの期待値と、Maybeオブジェクトや作業単位オブジェクトなどの考えられるエラー条件を示す値をカプセル化する実際のオブジェクトです。

26
Robert Harvey

ええ、そう...そしていいえ。

純粋な関数には Referential Transparency が必要です。つまり、プログラムの動作を変更せずに、純粋な関数への可能な呼び出しをすべて戻り値に置き換えることができます。* 関数は常に特定の引数に対してスローすることが保証されているため、関数呼び出しを置き換える戻り値はないので、代わりにignoreしましょう。このコードを考えてみましょう:

_{
    var ignoreThis = func(arg);
}
_

funcが純粋な場合、オプティマイザはfunc(arg)をその結果に置き換えることができると判断できます。結果はまだわかりませんが、使用されていないことがわかります。そのため、このステートメントは効果がないと推測して削除できます。

しかし、func(arg)がスローされた場合、このステートメントは何かを行います-例外をスローします!したがって、オプティマイザはそれを削除できません。関数が呼び出されるかどうかは重要です。

だが...

実際には、これはほとんど問題になりません。例外-少なくともC#では-は例外的なものです。これを通常の制御フローの一部として使用することは想定されていません-試行してキャッチする必要があります。何かをキャッチした場合は、エラーを処理して、実行していたことを元に戻すか、なんとかしてそれを達成します。失敗する可能性のあるコードが最適化されているためにプログラムが適切に機能しない場合は、例外を間違って使用しています(テストコードでない限り、テスト用にビルドする場合は例外を最適化しないでください)。

言われていること...

キャッチされることを意図して、純粋な関数から例外をスローしないでください。関数型言語がスタックの巻き戻しの例外の代わりにモナドを使用することを好むのには十分な理由があります。

C#にJava(および他の多くの言語)のようなErrorクラスがある場合、ErrorではなくExceptionをスローすることをお勧めします。これは、関数のユーザーが何か間違ったことをした(スローする関数を渡した)ことを示し、そのようなことは純粋な関数で許可されています。ただし、C#にはErrorクラスがなく、使用エラー例外はExceptionから派生しているようです。私の提案は、ArgumentExceptionをスローして、関数が不正な引数で呼び出されたことを明確にすることです。


*計算的に言えば。単純な再帰を使用して実装されたフィボナッチ関数は、大きな数の場合に長い時間がかかり、マシンのリソースを使い果たす可能性がありますが、無限の時間とメモリがあるため、関数は常に同じ値を返し、(割り当て以外の)副作用はありません。メモリとそのメモリを変更する)-それはまだ純粋と見なされます。

12
Idan Arye

1つの考慮事項は、try-catchブロックが問題ではないことです。 (上記の質問に対する私のコメントに基づく)。

主な問題は、NorthプロパティがI/O呼び出しであることです。

コード実行のその時点で、プログラムはクライアントコードによって提供されるI/Oをチェックする必要があります。 (入力がデリゲートの形式であること、または入力が名目上既に渡されていることは関係ありません)。

入力の制御を失うと、関数が純粋であることを保証できなくなります。 (特に関数がスローできる場合)。


Move [Check] Roomの呼び出しを確認したくない理由がわかりません。質問への私のコメントに従って:

はい。デリゲートを実行するときに、最初にしか機能しない、または2番目の呼び出しで別の部屋を返すことができるのは何ですか。デリゲートを呼び出すことは、考慮される/副作用自体を引き起こすことができますか?

バートファンインゲンシェナウが上で述べたように、

Create関数は、プロパティの取得時に例外が発生するのを防ぎません。デリゲートがスローする場合、実際には、特定の条件下でのみスローされる可能性が非常に高くなります。おそらく、建設中に投げる条件は存在しませんが、プロパティを取得するときに存在します。

一般に、あらゆるタイプの遅延読み込みはその時点までエラーを暗黙的に延期します


Move [Check] Roomメソッドの使用をお勧めします。これにより、純粋でないI/Oの側面を1つの場所に分離できます。

Robert Harvey's に似ています:答え:

純粋な関数にするために、Maybeオブジェクトや作業単位オブジェクトなど、関数からの期待値と起こり得るエラー状態を示す値をカプセル化する実際のオブジェクトを返します。

入力からの(可能性のある)例外を処理する方法を決定するのは、コードの作成者次第です。その後、このメソッドはRoomオブジェクトまたはNull Roomオブジェクトを返すか、例外をバブルアウトすることができます。

それはこの点に依存します:

  • ルームドメインはルームの例外をNullか何か悪いものとして扱いますか?.
  • Null/Exception RoomでNorthを呼び出すクライアントコードに通知する方法。 (保釈/状態変数/グローバル状態/モナドを返す/何でも;いくつかは他よりも純粋です:))。
1
SH-