ホームページ  >  記事  >  バックエンド開発  >  Python の Sum 型

Python の Sum 型

Mary-Kate Olsen
Mary-Kate Olsenオリジナル
2024-10-19 06:18:02647ブラウズ

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 サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。