機能的なスタイルの例外処理


24

関数型プログラミングでは、例外をスローしたり、観察したりすることは想定されていません。代わりに、誤った計算はボトム値として評価する必要があります。Python(または関数型プログラミングを完全に奨励していない他の言語)Noneでは、None何かが「純粋のまま」に失敗したときはいつでも返すことができますしたがって、最初にエラーを観察する必要があります。つまり、

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

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

更新

彼のコメントで、Eric LippertはFPの例外を処理する別の方法を思い出しました。実際にPythonでそれが行われるのを見たことはありませんが、1年前にFPを勉強したときに、Pythonで遊んでみました。ここで、任意のoptional-decorated関数はOptional、通常の出力および指定された例外のリストに対して空の値を返します(指定されていない例外でも実行を終了できます)。Carry遅延評価を作成します。各ステップ(遅延関数呼び出し)Optionalは、前のステップから空でない出力を取得して単純に渡すか、それ以外の場合はnewを渡して評価しますOptional。最終的に、最終値はnormalまたは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

1
あなたの質問はすでに答えられているので、コメントだけです。関数プログラミングで例外をスローすることがなぜ眉をひそめているの理解できますか?それはarbitrary意的な気まぐれではありません:)
アンドレスF.

6
エラーを示す値を返す別の方法があります。例外処理は制御フローであり、制御フローを具体化する機能的なメカニズムがあることを忘れないでください。メソッドを記述して、成功継続エラー継続の 2つの関数を取得することにより、関数型言語で例外処理をエミュレートできます。関数が最後に行うことは、成功の継続を呼び出して「結果」を渡すか、エラーの継続を呼び出して「例外」を渡すことです。欠点は、この裏返しの方法でプログラムを作成する必要があることです。
エリックリッパー

3
いいえ、この場合の状態はどうなりますか?複数の問題がありますが、ここにいくつかあります:1-型にエンコードされていない可能性のあるフローが1つあります。つまり、型を見ただけでは関数が例外をスローするかどうかを知ることができませんもちろん例外はありますが、それらだけの言語は知りません)。タイプシステムの「外部」で効果的に作業している場合、2機能プログラマーは可能な限り「合計」関数、つまり、すべての入力に対して値を返す関数(非終端を除く)を書き込もうとします。これに対して例外が機能します。
アンドレスF.

3
3-合計関数を使用する場合、他の関数でそれらを構成し、「型にエンコードされていない」エラー結果、つまり例外を心配することなく高次関数で使用できます。
アンドレスF.

1
@EliKorvigo変数を設定すると、定義により状態が完全に停止します。変数の全体的な目的は、変数が状態を保持することです。それは例外とは関係ありません。関数の戻り値を観察し、その観察に基づいて変数の値を設定すると、評価に状態が導入されますか?
user253751

回答:


20

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

ボトムタイプが便利なのはなぜですか?それが空であることを知っているので、プログラムの振る舞いについて推論をしてみましょう。たとえば、次の関数がある場合:

def do_thing(a: int) -> Bottom: ...

do_thingtypeの値を返さなければならないので、それが決して戻れないことを知っていますBottom。したがって、2つの可能性のみがあります。

  1. do_thing 止まらない
  2. do_thing 例外をスローします(例外メカニズムを持つ言語で)

BottomPython言語には実際には存在しないタイプを作成したことに注意してください。None誤称です。これは実際にはユニット値であり、ユニットタイプの唯一の値でありNoneType、Python で呼び出されます(type(None)自分で確認するために行います)。

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

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

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

したがって、この場合は言語の規則に従うことをお勧めします。Pythonは制御フローを処理するために例外を慣用的に使用します(これは悪い設計ですが、IMOですが、それでも標準です)。したがって、自分で書いたコードだけで作業しているのでなければ、標準のプラクティスに従うことをお勧めします。これが「純粋」であるかどうかは関係ありません。


「ボトム値」とは、実際には「ボトムタイプ」を意味Noneします。そのため、定義に準拠していないと書いたのです。とにかく、私を修正してくれてありがとう。Pythonの原則では、実行を完全に停止するため、またはオプションの値を返すためだけに例外を使用しても大丈夫だと思いませんか?つまり、複雑な制御に例外を使用するのを抑制するのはなぜ悪いのでしょうか?
エリKorvigo

@EliKorvigoそれは多かれ少なかれ私が言ったことですよね?例外は慣用的なPythonです。
ガーデンヘッド

1
たとえば、私は学部生がのtry/except/finally別の代替物のように使用することを勧めませんif/else。つまりtry: var = expession1; except ...: var = expression 2; except ...: var = expression 3...、命令型言語で行うのは一般的なことですが(if/elseこれにもブロックを使用することを強く勧めます)。「これはPythonです」から私は不合理であり、そのようなパターンを許可する必要があるということですか?
エリKorvigo

@EliKorvigo私は一般的にあなたに同意します(ところで、あなたは教授ですか?)。制御フローには使用しないtry... catch...ください。何らかの理由で、それがPythonコミュニティが物事を行うことを決定した方法です。たとえば、上で書いた関数は通常書かれます。したがって、一般的なソフトウェアエンジニアリングの原則を教えている場合は、絶対にこれを思いとどまらせる必要があります。また、コードの匂いですが、ここに入るには長すぎます。safe_divtry: result = num / div: except ArithmeticError: result = Noneif ... else...
ガーデンヘッド

2
「ボトム値」(Haskellのセマンティクスについて話す場合などに使用されます)があり、ボトムタイプとはほとんど関係がありません。したがって、それは実際にはOPの誤解ではなく、別のことについて話しているだけです。
ベン

11

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

純粋な関数:

  • 参照透過的です。つまり、指定された入力に対して、常に同じ出力が生成されます。

  • 副作用は発生しません。外部環境の入力、出力、またはその他のものは変更しません。戻り値のみを生成します。

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


3
それで、それは問題ではありません、機能的に動作する限り、それは内部にどれほどwrittenいですか?
エリKorvigo

8
純粋さに興味があるだけなら、正しい。もちろん、考慮すべき他のことがあるかもしれません。
ロバートハーベイ

3
悪魔の擁護者を演じるために、例外をスローすることは単なる出力の形式であり、スローする関数は純粋であると主張します。問題ありますか?
ベルギ

1
@Bergiあなたが悪魔の擁護者を演じていることを私は知りません。それはまさにこの答えが暗示するものだからです:)問題は、純度以外に他の考慮事項があることです。チェックされていない例外(定義により関数の署名の一部ではない)を許可すると、すべての関数の戻り値の型はT + { Exception }T明示的に宣言された戻り値の型)になり、問題があります。関数がソースコードを見ずに例外をスローするかどうかを知ることはできません。これにより、高階関数の記述にも問題が生じます。
アンドレスF.

2
スロー中の@Begriは間違いなく純粋かもしれませんが、例外を使用するIMOは、各関数の戻り値の型を単に複雑にするだけではありません。機能の構成可能性を損ないます。エラーが発生map : (A->B)->List A ->List BするA->B可能性のある場所の実装を検討してください。fに例外をスローmap f L させると、typeの何かを返しますException + List<B>。代わりにoptionalスタイルタイプを返すことを許可すると、map f L代わりにList <Optional <B >> `を返します。この2番目のオプションは、より機能的だと感じています。
マイケルアンダーソン

7

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

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

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

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

Pythonにはすでにこのような底があります。例外をスローする、または終了しない式から取得する「値」です。Pythonは遅延ではなく厳密であるため、このような「ボトム」はどこにも格納できず、潜在的に未検査のままになる可能性があります。そのため、値を返すことに失敗した計算を、値を持っているかのように処理する方法を説明するために、ボトム値の概念を実際に使用する必要はありません。しかし、もし望むなら例外についてこのように考えることができなかった理由もありません。

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

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

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

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

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

また、信頼性の高い高次関数(他の関数を操作する関数)にもある程度必要です。私は、パラメータとして(知られているインターフェイスを持つ)完全に任意の関数を受けて、コードを書きたい場合は私が持っている私は、エラーを誘発する可能性のある入力知る方法はありませんので、ブラックボックスとしてそれを治療すること。合計関数が与えられた場合入力はエラーになりません。同様に、私の高階関数の呼び出し元は、渡される関数を呼び出すために使用する引数を正確に知りません(実装の詳細に依存したい場合を除きます)ので、関数全体を渡しても心配する必要はありません私はそれで何をします。

したがって、例外を避けるように助言する関数型プログラマーは、代わりにエラーまたは有効な値をエンコードする値を返すことを望み、それを使用するためには両方の可能性を処理する準備が必要です。Either型やMaybe/ Option型のようなものは、より強く型付けされた言語でこれを行うための最も簡単なアプローチの一部です(通常、必要Aなものとを生成するものをつなぎ合わせるのに役立つ特別な構文または高次関数で使用されますMaybe<A>)。

None(エラーが発生した場合)または何らかの値(エラーが発生しなかった場合)を返す関数は、上記の戦略のいずれにも従いません

Pythonでカモタイピングを使用する場合、Ether / Maybeスタイルはあまり使用されず、代わりに例外がスローされ、関数がタイプに基づいて完全かつ自動的に結合できると信頼するのではなく、コードが機能することを検証するテストが行​​われます。Pythonには、コードがMaybe型などを適切に使用するよう強制する機能はありません。規律の問題としてそれを使用していたとしても、それを検証するために実際にコードを実行するテストが必要です。したがって、例外/ボトムアプローチは、おそらくPythonの純粋な関数型プログラミングにより適しています。


1
+1素晴らしい答え!また、非常に包括的です。
アンドレスF.

6

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

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

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

サイドノートでは、関数型言語が戻る傾向ユニットのタイプを 1つだけの可能なエラー条件がある場合のみ。複数のエラーが発生する可能性がある場合、エラーに関する情報を含むエラータイプを返す傾向があります。


ボトムタイプを返すことはできません。
ガーデンヘッド

1
@gardenheadそうですね、ユニットのタイプを考えていました。一定。
8ビットツリー

最初の例の場合、ファイルシステムとその中に存在するファイルは、関数への単なる入力の1つではありませんか?
バリティ

2
@Vaility実際には、おそらくファイルへのハンドルまたはパスがあります。関数fが純粋な場合、f("/path/to/file")常に同じ値を返すことが期待されます。2つの呼び出しの間に実際のファイルが削除または変更されるとfどうなりますか?
アンドレスF.

2
@Vailityファイルへのハンドルの代わりに、ファイルの実際のコンテンツ(つまり、バイトの正確なスナップショット)を渡した場合、関数は純粋になる可能性があります。その場合、同じコンテンツが入った場合、同じ出力外出。しかし、ファイルへのアクセスはそうではありません:)
Andres F.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.