web-dev-qa-db-ja.com

関数型の例外処理

関数型プログラミングでは、例外をスローしたり、例外を観察したりすることは想定されていません。代わりに、誤った計算はボトム値として評価されるべきです。 Python(または関数型プログラミングを完全に推奨していない他の言語)では、None(または、Noneはボトム値として扱われる別の代替手段)を返すことができます厳密に定義に準拠しない)何かが「純粋なまま」で問題が発生した場合はいつでも、そうするためには、最初にエラーを観察する必要があります。

def fn(*args):
    try:
        ... do something
    except SomeException:
        return None

これは純度に違反しますか?もしそうなら、それは純粋にPythonでエラーを処理することが不可能であることを意味しますか?

更新

エリックリッパートは彼のコメントで、FPの例外を処理する別の方法を思い出させました。 Pythonで実際にそれが行われるのを見たことはありませんが、FPを1年前に勉強したとき、私はそれを試しました。ここにoptional装飾された関数は、通常の出力および指定された例外のリストに対して、空の場合があるOptional値を返します(指定されていない例外でも実行を終了できます)。Carryは遅延評価を作成しますここで、各ステップ(遅延関数呼び出し)は、前のステップから空でないOptional出力を取得してそれを単に渡すか、それ以外の場合は新しいOptionalを渡してそれ自体を評価します。最後に、最終値通常またはEmptyのいずれかです。ここでtry/exceptブロックはデコレータの背後に隠されているため、指定された例外は戻り型シグネチャの一部と見なすことができます。

class Empty:
    def __repr__(self):
        return "Empty"


class Optional:
    def __init__(self, value=Empty):
        self._value = value

    @property
    def value(self):
        return Empty if self.isempty else self._value

    @property
    def isempty(self):
        return isinstance(self._value, BaseException) or self._value is Empty

    def __bool__(self):
        raise TypeError("Optional has no boolean value")


def optional(*exception_types):
    def build_wrapper(func):
        def wrapper(*args, **kwargs):
            try:
                return Optional(func(*args, **kwargs))
            except exception_types as e:
                return Optional(e)
        wrapper.__isoptional__ = True
        return wrapper
    return build_wrapper


class Carry:
    """
    >>> from functools import partial
    >>> @optional(ArithmeticError)
    ... def rdiv(a, b):
    ...     return b // a
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
    1
    >>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
    1
    >>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
    True
    """
    def __init__(self, steps=None):
        self._steps = Tuple(steps) if steps is not None else ()

    def _add_step(self, step):
        fn, *step_args = step if isinstance(step, Sequence) else (step, )
        return type(self)(steps=self._steps + ((fn, step_args), ))

    def __rshift__(self, step) -> "Carry":
        return self._add_step(step)

    def _evaluate(self, *args) -> Optional:
        def caller(carried: Optional, step):
            fn, step_args = step
            return fn(*(*step_args, *args)) if carried.isempty else carried
        return reduce(caller, self._steps, Optional())

    def __call__(self, *args):
        return self._evaluate(*args).value
24
Eli Korvigo

まず最初に、いくつかの誤解を片付けましょう。 「ボトムバリュー」はありません。下部の型は、言語の他のすべての型のサブタイプである型として定義されます。これから、(少なくとも興味深い型システムでは)一番下の型値がない-であることを証明できます。したがって、ボトム値などはありません。

なぜ下のタイプが便利なのですか?まあ、それが空であることを知って、プログラムの動作についていくつかの推論を行いましょう。たとえば、次の関数があるとします。

_def do_thing(a: int) -> Bottom: ...
_

Bottom型の値を返す必要があるため、_do_thing_は決して戻れないことがわかっています。したがって、2つの可能性しかありません。

  1. _do_thing_は停止しません
  2. _do_thing_は例外をスローします(例外メカニズムのある言語)

Python言語には実際には存在しない型Bottomを作成したことに注意してください。Noneは誤称です。実際にはnit valueであり、 ユニットタイプ、PythonでNoneTypeと呼ばれます(type(None)を実行して確認してください)。

もう1つの誤解は、関数型言語には例外がないということです。これも当てはまりません。たとえば、SMLには非常に優れた例外メカニズムがあります。ただし、SMLでの例外の使用は、たとえばPython。あなたが言ったように、関数型言語のある種の失敗を示すcommon方法は、Option型を返すことです。たとえば、次のように安全な除算関数を作成します。

_def safe_div(num: int, den: int) -> Option[int]:
  return Some(num/den) if den != 0 else None
_

残念ながら、Pythonには実際には合計型がないため、これは実行可能なアプローチではありません。あなたはcouldNoneを貧乏人のオプション型として返し、失敗を示します、しかし、これはNullを返すことと同じです。型の安全性はありません。

したがって、この場合は言語の規則に従うことをお勧めします。 Pythonは、例外を使用して制御フローを処理するために慣用的に使用します(これは悪い設計、IMOですが、それでも標準です)。そのため、自分で記述したコードのみを使用しているのでない限り、以下の標準をお勧めしますこれが「純粋」であるかどうかは関係ありません。

20
gardenhead

過去数日間、純粋さに大きな関心があったので、純粋な関数がどのように見えるかを調べてみませんか。

純粋な関数:

  • 参照透過的です。つまり、特定の入力に対して、常に同じ出力を生成します。

  • 副作用はありません。外部環境の入力、出力、その他は変更されません。戻り値を生成するだけです。

だから自問してみてください。あなたの関数は入力を受け入れて出力を返す以外に何をしますか?

11
Robert Harvey

Haskellのセマンティクスは「ボトムバリュー」を使用してHaskellコードの意味を分析します。これはHaskellのプログラミングで直接使用するものではなく、Noneを返すことはまったく同じことではありません。

一番下の値は、Haskellセマンティクスが通常の値に評価できない計算に起因する値です。 Haskellの計算でできることの1つは、実際には例外をスローすることです。したがって、Pythonでこのスタイルを使用しようとした場合、実際には通常どおりに例外をスローする必要があります。

Haskellは遅延であるため、Haskellのセマンティクスはボトム値を使用します。実際にはまだ実行されていない計算によって返される「値」を操作できます。それらを関数に渡したり、データ構造に貼り付けたりすることができます。このような未評価の計算は、例外をスローするか、永久にループする可能性がありますが、実際に値を調べる必要がない場合、計算はになります。 never実行してエラーに遭遇すると、プログラム全体がうまく定義されて終了することがあります。したがって、実行時のプログラムの正確な動作動作を指定してHaskellコードの意味を説明するのではなく、代わりに、このような誤った計算がボトム値を生成することを宣言し、その値の動作を説明します。基本的に、すべてのボトム値(存在するもの以外)のプロパティに依存する必要がある式は、また結果としてボトム値になります。

「純粋」のままであるためには、ボトム値を生成するすべての可能な方法を同等のものとして扱う必要があります。これには、無限ループを表す「ボトム値」が含まれます。一部の無限ループが実際に無限であることを知る方法がないため(少しだけ実行すると終了する可能性があります)、ボトム値のanyプロパティを調べることはできません。何かが下かどうかをテストしたり、他のものと比較したり、文字列に変換したりすることはできません。 1つでできることは、それをそのまま(関数パラメーター、データ構造の一部など)そのままの場所に置くことです。

Pythonにはすでにこの種の底があります。例外をスローする、または終了しない式から取得する「値」です。 Pythonは遅延ではなく厳格であるため、そのような「ボトム」はどこにも保存できず、調査されない可能性があります。したがって、ボトム値の概念を使用して計算方法を説明する必要はありません。値を返さない場合でも、値があるかのように処理できますが、必要に応じて例外についてこのように考えられなかった理由もありません。

Throwing例外は、実際には「純粋」と見なされます。純粋さを損なうのはcatching例外です。つまり、特定のボトム値について何かを検査することを可能にし、それらをすべて交換可能に処理するのではありません。 Haskellでは、不純なインターフェースを許可するIOの例外のみをキャッチできます(そのため、通常はかなり外側の層で発生します)。 Pythonは純粋さを強制しませんが、純粋な関数ではなく「外部の純粋でない層」の一部である関数を自分で決めることができ、例外をキャッチすることのみを許可します。

代わりにNoneを返すことは完全に異なります。 Noneは非ボトム値です。何かがそれと等しいかどうかをテストできます。Noneを返した関数の呼び出し元は、おそらくNoneを不適切に使用して実行を続けます。

したがって、例外をスローすることを考えていて、Haskellのアプローチをエミュレートするために「ボトムに戻る」ことを望む場合は、何もしないでください。例外を伝播させます。 Haskellプログラマがボトム値を返す関数について話すとき、まさにそれが意味します。

しかし、それはではない関数型プログラマが例外を回避するために言うときの意味です。関数型プログラマーは「合計関数」を好みます。これらは常にevery可能な入力に対して、戻り値の型の有効な非ボトム値を返します。したがって、例外をスローする可能性のある関数は、完全な関数ではありません。

トータル関数が好きな理由は、それらを組み合わせて操作すると、「ブラックボックス」として扱うのがはるかに簡単になるためです。タイプAの何かを返す合計関数とタイプAの何かを受け入れる合計関数がある場合、どちらかの実装についてanythingを知らなくても、最初の出力で2番目を呼び出すことができます;どちらの関数のコードが将来どのように更新されても、それらの合計が維持され、同じ型シグネチャを保持している限り、有効な結果が得られます。この懸念の分離は、リファクタリングの非常に強力な助けとなります。

また、信頼性の高い高次関数(他の関数を操作する関数)にもいくらか必要です。 (既知のインターフェースを持つ)完全に任意の関数をパラメーターとして受け取るコードを記述したい場合、Ihaveとしてブラックボックスとして扱います。どの入力がエラーをトリガーするかを知る方法がありません。合計関数が指定されている場合、no入力によりエラーが発生します。同様に、私の高次関数の呼び出し元は、渡された関数を呼び出すために使用する引数を正確に認識しません(実装の詳細に依存したくない場合を除く)。そのため、合計関数を渡しても、心配する必要はありません。私がそれで何をするか。

したがって、例外を回避するようアドバイスする関数型プログラマは、代わりにエラーまたは有効な値のいずれかをエンコードする値を返し、それを使用するために両方の可能性を処理する準備ができていることを好むでしょう。 Either型またはMaybe/Option型のようなものは、より強く型付けされた言語でこれを行う最も簡単な方法の一部です(通常、特別な構文またはより高次の関数で使用され、 Aを必要とするものとMaybe<A>を生成するものを接着します。

None(エラーが発生した場合)または何らかの値(エラーが発生しなかった場合)を返す関数は、次のどちらでもない上記の戦略。

Pythonでダックを入力すると、Either/Maybeスタイルはあまり使用されず、代わりに例外がスローされます。テストは、関数が完全で自動的に結合可能であると信頼するのではなく、コードが機能することを検証するためのテストです。 Pythonは、コードがMaybe型のようなものを適切に使用するように強制する機能がありません。それを分野の問題として使用していたとしても、それを検証するために実際にコードを実行するためのテストが必要です。 。したがって、例外/ボトムアプローチは、おそらくPythonでの純粋な関数型プログラミングにより適しています。

7
Ben

外部から見える副作用がなく、戻り値が入力のみに依存している限り、関数は純粋であり、内部でかなり不純なことを行っていても純粋です。

したがって、それは本当に例外がスローされる原因となるものによって異なります。パスを指定してファイルを開こうとしている場合、ファイルは存在する場合と存在しない場合があり、同じ入力に対して戻り値が変化するため、それは純粋ではありません。

一方、特定の文字列から整数を解析しようとして失敗した場合に例外をスローする場合、例外が関数の外に出てこない限り、それは純粋である可能性があります。

余談ですが、関数型言語は unittype を返す傾向がありますが、それはエラー状態が1つしかない場合に限られます。複数の考えられるエラーがある場合、エラーに関する情報を含むエラータイプを返す傾向があります。

6
8bittree