파이썬은 사랑스러운 언어입니다. 그러나 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)
그러나 여기에는 몇 가지 단점이 있습니다.
합계 유형을 사용하는 데 사용되는 단일 공개 일치 방법을 노출하여 이러한 모든 문제를 피할 수 있습니다.
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
이를 통해 일치 항목을 구현할 수 있습니다.
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}")
이를 통해 다음과 같은 일치 항목을 사용할 수 있습니다.
def map(self, fn): return self.match( some=lambda x: Option.some(fn(x)), _=lambda: Option.none(), )
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
어떤 설명인가요? 가장 간단한 방법은 변형 이름 목록을 제공하는 것이지만, 우리가 기대하는 인수 유형도 제공하면 좀 더 나은 결과를 얻을 수 있습니다. 다음과 같이 Option 클래스를 자동으로 향상시키기 위해 enum을 사용합니다.
# 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
enum의 기본 구조는 다음과 같습니다.
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
이것은 또 다른 함수를 반환하는 함수로, 우리가 향상시키고 있는 클래스를 유일한 인수로 사용하여 호출됩니다. 강화 내에서 일치 항목과 함께 각 변형을 구성하는 방법을 첨부하겠습니다.
먼저, 그냥 카피 파스타이기 때문에 일치:
# 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__ 메소드를 사용하여 합계 클래스를 쉽게 향상할 수 있습니다.
class Expr: # ... def match(self, handlers): # ...
이러한 방식으로 향상을 통해 최소한의 조작으로 Option을 정의할 수 있습니다.
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(), )
유용하기는 하지만 새로운 멋진 열거형 데코레이터에는 단점도 있습니다. 가장 분명한 점은 어떤 종류의 "중첩" 패턴 일치도 수행할 수 없다는 것입니다. 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
그래도 이런 경우는 비교적드물어 보입니다.
또 다른 단점은 일치를 위해서는 많은 함수를 구성하고 호출해야 한다는 것입니다. 즉, 동등한 if-else 체인보다 훨씬 느릴 가능성이 높습니다. 그러나 여기에는 일반적인 경험 법칙이 적용됩니다. 인체공학적 이점이 마음에 들면 enum을 사용하고, 너무 느리면 "생성된" 코드로 교체하세요.
위 내용은 Python의 합계 유형의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!