Python の Sum 型

Mary-Kate Olsen
リリース: 2024-10-19 06:18:02
オリジナル
746 人が閲覧しました

Sum Types in Python

Python は素敵な言語です。ただし、Python で作業していると、合計型の組み込みサポートが欠けていることによく気づきます。 Haskell や Rust のような言語を使用すると、この種の作業が非常に簡単になります:

data Op = Add | Sub | Mul
  deriving (Show)

data Expr
  = Lit Integer
  | BinOp Op Expr Expr
  deriving (Show)

val :: Expr -> Integer
val (Lit val) = val
val (BinOp op lhs rhs) =
  let x = val lhs
      y = val rhs
   in apply op x y

apply :: Op -> Integer -> Integer -> Integer
apply Add x y = x + y
apply Sub x y = x - y
apply Mul x y = x * y

val (BinOp Add (BinOp Mul (Lit 2) (Lit 3)) (Lit 4))
-- => 10
ログイン後にコピー
ログイン後にコピー

Python はこの種の構築をそのままではサポートしていませんが、それでも Expr のような型を表現することが可能 (そして簡単) であることがわかります。さらに、面倒な定型文をすべて処理してくれるデコレータを作成することもできます。結果は上記の Haskell の例とあまり変わりません:

# The `enum` decorator adds methods for constructing and matching on the
# different variants:
@enum(add=(), sub=(), mul=())
class Op:
    def apply(self, x, y):
        return self.match(
            add=lambda: x + y,
            sub=lambda: x - y,
            mul=lambda: x * y,
        )


# Recursive sum types are also supported:
@enum(lit=(int,), bin_op=lambda: (Op, Expr, Expr))
class Expr:
    def val(self):
        return self.match(
            lit=lambda value: value,
            bin_op=lambda op, lhs, rhs: op.apply(lhs.val(), rhs.val()),
        )


Expr.bin_op(
    Op.add(),
    Expr.bin_op(Op.mul(), Expr.lit(2), Expr.lit(3)),
    Expr.lit(4)
).val()
# => 10
ログイン後にコピー
ログイン後にコピー

合計タイプの表現

「タグ付き共用体」を使用して合計タイプを表します。これは例で簡単に理解できます:

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.value = value
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.op = op
        e.lhs = lhs
        e.rhs = rhs
        return e
ログイン後にコピー
ログイン後にコピー

各バリアントは同じクラス (この場合は Expr) のインスタンスです。それぞれには、どのバリアントであるかを示す「タグ」と、そのバリアントに固有のデータが含まれています。

Expr を使用する最も基本的な方法は、if-else チェーンを使用することです。

class Expr:
    # ...
    def val(self):
        if self.tag == "lit":
            return self.value
        elif self.tag == "bin_op":
            x = self.lhs.val()
            y = self.rhs.val()
            return self.op.apply(x, y)
ログイン後にコピー
ログイン後にコピー

ただし、これにはいくつかの欠点があります。

  • Expr が使用されるすべての場所で、同じ if-else チェーンが繰り返されます。
  • タグの値を変更すると、たとえば「lit」から「literal」に変更すると機能が停止します。 既存のコード。
  • 合計タイプを使用するには、実装の詳細 (つまり、タグと 各バリアントで使用されるフィールドの名前)。

試合の実施

合計タイプの使用に使用される単一のパブリック match メソッドを公開することで、これらすべての問題を回避できます。

class Expr:
    # ...
    def match(self, handlers):
        # ...
ログイン後にコピー
ログイン後にコピー

しかし、最初に、さまざまなバリアントをもう少し均一にする必要があります。データをさまざまなフィールドに保存する代わりに、各バリアントはデータを data:
という名前のタプルに保存するようになります。

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.data = (value,)
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.data = (op, lhs, rhs)
        return e
ログイン後にコピー
ログイン後にコピー

これにより、match を実装できるようになります:

class Expr:
    # ...
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
ログイン後にコピー
ログイン後にコピー

上記の問題をすべて一気に解決しました。別の例として、趣向を変えて、Rust の Option 型を次のように転写してみます。

class Option:
    def some(x):
        o = Option()
        o.tag = "some"
        o.data = (x,)
        return o

    def none():
        o = Option()
        o.tag = "none"
        o.data = ()
        return o

    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")

    def __repr__(self):
        return self.match(
            some=lambda x: f"Option.some({repr(x)})",
            none=lambda: "Option.none()",
        )

    def __eq__(self, other):
        if not isinstance(other, Option):
            return NotImplemented
        return self.tag == other.tag and self.data == other.data

    def map(self, fn):
        return self.match(
            some=lambda x: Option.some(fn(x)),
            none=lambda: Option.none()
        )

Option.some(2).map(lambda x: x**2)
# => Option.some(4)
ログイン後にコピー
ログイン後にコピー

生活の質の小さな利点として、アンダースコア (_) で示される特別なワイルドカードまたは「キャッチオール」ハンドラーを一致でサポートできます。

def match(self, **handlers):
    if self.tag in handlers:
        return handlers[self.tag](*self.data)
    elif "_" in handlers:
        return handlers["_"]()
    else:
        raise RuntimeError(f"missing handler for {self.tag}")
ログイン後にコピー
ログイン後にコピー

これにより、次のように match を使用できるようになります。

def map(self, fn):
    return self.match(
        some=lambda x: Option.some(fn(x)),
        _=lambda: Option.none(),
    )
ログイン後にコピー
ログイン後にコピー

enumの実装

Option クラスが示すように、合計型の作成に必要なコードの多くは同じパターンに従います。

class Foo:
    # For each variant:
    def my_variant(bar, quux):
        # Construct an instance of the class:
        f = Foo()
        # Give the instance a distinct tag:
        f.tag = "my_variant"
        # Save the values we received:
        f.data = (bar, quux)
        return f

    # This is always the same:
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        elif "_" in handlers:
            return handlers["_"]()
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
ログイン後にコピー
ログイン後にコピー

これを自分で書く代わりに、バリアントの説明に基づいてこれらのメソッドを生成する デコレータ を書きましょう。

def enum(**variants):
    pass
ログイン後にコピー
ログイン後にコピー

どんな説明ですか?最も単純なことはバリアント名のリストを提供することですが、予期する引数のタイプも提供することで、もう少しうまく行うことができます。 enum を使用して、次のように Option クラスを自動的に強化します。

# Add two variants:
# - One named `some` that expects a single argument of any type.
# - One named `none` that expects no arguments.
@enum(some=(object,), none=())
class Option:
    pass
ログイン後にコピー

列挙型の基本構造は次のようになります:

data Op = Add | Sub | Mul
  deriving (Show)

data Expr
  = Lit Integer
  | BinOp Op Expr Expr
  deriving (Show)

val :: Expr -> Integer
val (Lit val) = val
val (BinOp op lhs rhs) =
  let x = val lhs
      y = val rhs
   in apply op x y

apply :: Op -> Integer -> Integer -> Integer
apply Add x y = x + y
apply Sub x y = x - y
apply Mul x y = x * y

val (BinOp Add (BinOp Mul (Lit 2) (Lit 3)) (Lit 4))
-- => 10
ログイン後にコピー
ログイン後にコピー

これは 別の 関数を返す関数であり、強化しているクラスを唯一の引数として呼び出します。エンハンス内で、match とともに各バリアントを構築するためのメソッドをアタッチします。

まず、コピーパスタなので一致させます:

# The `enum` decorator adds methods for constructing and matching on the
# different variants:
@enum(add=(), sub=(), mul=())
class Op:
    def apply(self, x, y):
        return self.match(
            add=lambda: x + y,
            sub=lambda: x - y,
            mul=lambda: x * y,
        )


# Recursive sum types are also supported:
@enum(lit=(int,), bin_op=lambda: (Op, Expr, Expr))
class Expr:
    def val(self):
        return self.match(
            lit=lambda value: value,
            bin_op=lambda op, lhs, rhs: op.apply(lhs.val(), rhs.val()),
        )


Expr.bin_op(
    Op.add(),
    Expr.bin_op(Op.mul(), Expr.lit(2), Expr.lit(3)),
    Expr.lit(4)
).val()
# => 10
ログイン後にコピー
ログイン後にコピー

各バリアントを構築するためのメソッドの追加は、少しだけ複雑になります。バリアント辞書を反復処理して、各エントリのメソッドを定義します。

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.value = value
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.op = op
        e.lhs = lhs
        e.rhs = rhs
        return e
ログイン後にコピー
ログイン後にコピー

ここで、make_constructor は、タグ (および名前) タグと「型署名」シグネチャを持つバリアントのコンストラクター関数を作成します。

class Expr:
    # ...
    def val(self):
        if self.tag == "lit":
            return self.value
        elif self.tag == "bin_op":
            x = self.lhs.val()
            y = self.rhs.val()
            return self.op.apply(x, y)
ログイン後にコピー
ログイン後にコピー

参考までに enum の完全な定義を示します。

ボーナス機能

その他のダンダーメソッド

__repr__ メソッドと __eq__ メソッドを使用して sum クラスを簡単に拡張できます。

class Expr:
    # ...
    def match(self, handlers):
        # ...
ログイン後にコピー
ログイン後にコピー

このように改良された強化により、最小限の手間でオプションを定義できます。

class Expr:
    def lit(value):
        e = Expr()
        e.tag = "lit"
        e.data = (value,)
        return e

    def bin_op(op, lhs, rhs):
        e = Expr()
        e.tag = "bin_op"
        e.data = (op, lhs, rhs)
        return e
ログイン後にコピー
ログイン後にコピー

再帰的な定義

残念ながら、enum は (まだ) Expr を定義する作業ができていません:

class Expr:
    # ...
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
ログイン後にコピー
ログイン後にコピー

クラス Expr は定義されるに使用しています。ここでの簡単な修正は、クラスを定義した後にデコレータを呼び出すだけです:

class Option:
    def some(x):
        o = Option()
        o.tag = "some"
        o.data = (x,)
        return o

    def none():
        o = Option()
        o.tag = "none"
        o.data = ()
        return o

    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        else:
            raise RuntimeError(f"missing handler for {self.tag}")

    def __repr__(self):
        return self.match(
            some=lambda x: f"Option.some({repr(x)})",
            none=lambda: "Option.none()",
        )

    def __eq__(self, other):
        if not isinstance(other, Option):
            return NotImplemented
        return self.tag == other.tag and self.data == other.data

    def map(self, fn):
        return self.match(
            some=lambda x: Option.some(fn(x)),
            none=lambda: Option.none()
        )

Option.some(2).map(lambda x: x**2)
# => Option.some(4)
ログイン後にコピー
ログイン後にコピー

しかし、これをサポートするためにできる簡単な変更があります。「署名」をタプルを返す 関数 にすることができます:

def match(self, **handlers):
    if self.tag in handlers:
        return handlers[self.tag](*self.data)
    elif "_" in handlers:
        return handlers["_"]()
    else:
        raise RuntimeError(f"missing handler for {self.tag}")
ログイン後にコピー
ログイン後にコピー

これに必要なのは、make_constructor に小さな変更を加えるだけです:

def map(self, fn):
    return self.match(
        some=lambda x: Option.some(fn(x)),
        _=lambda: Option.none(),
    )
ログイン後にコピー
ログイン後にコピー

結論

便利かもしれませんが、私たちの派手な新しい enum デコレータには欠点がないわけではありません。最も明らかなのは、あらゆる種類の「ネストされた」パターン マッチングを実行できないことです。 Rust では、次のようなことができます:

class Foo:
    # For each variant:
    def my_variant(bar, quux):
        # Construct an instance of the class:
        f = Foo()
        # Give the instance a distinct tag:
        f.tag = "my_variant"
        # Save the values we received:
        f.data = (bar, quux)
        return f

    # This is always the same:
    def match(self, **handlers):
        if self.tag in handlers:
            return handlers[self.tag](*self.data)
        elif "_" in handlers:
            return handlers["_"]()
        else:
            raise RuntimeError(f"missing handler for {self.tag}")
ログイン後にコピー
ログイン後にコピー

しかし、同じ結果を得るにはダブルマッチを実行する必要があります:

def enum(**variants):
    pass
ログイン後にコピー
ログイン後にコピー

とはいえ、このようなケースは比較的珍しいようです。

もう 1 つの欠点は、一致には多くの関数を構築して呼び出す必要があることです。これは、同等の if-else チェーンよりもはるかに遅い可能性が高いことを意味します。ただし、ここでは通常の経験則が適用されます。人間工学的な利点が気に入った場合は enum を使用し、遅すぎる場合は「生成された」コードに置き換えます。

以上がPython の Sum 型の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート