Python ialah bahasa yang menarik. Walau bagaimanapun, apabila bekerja dalam Python saya sering mendapati diri saya kehilangan sokongan terbina dalam untuk jenis jumlah. Bahasa seperti Haskell dan Rust menjadikan perkara seperti ini begitu mudah:
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
Walaupun Python tidak menyokong pembinaan seperti ini di luar kotak, kita akan melihat bahawa jenis seperti Expr masih boleh (dan mudah) untuk dinyatakan. Tambahan pula, kita boleh mencipta penghias yang mengendalikan semua boilerplate yang jahat untuk kita. Hasilnya tidak terlalu berbeza daripada contoh Haskell di atas:
# 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
Kami akan mewakili jenis jumlah menggunakan "kesatuan berteg". Ini mudah untuk grok dengan contoh:
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
Setiap varian ialah contoh kelas yang sama (dalam kes ini Expr). Setiap satu mengandungi "teg" yang menunjukkan varian itu, bersama-sama dengan data khusus untuknya.
Cara paling asas untuk menggunakan Expr ialah dengan rantai 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)
Walau bagaimanapun, ini mempunyai beberapa kelemahan:
Kita boleh mengelakkan semua isu ini dengan mendedahkan satu kaedah padanan awam yang digunakan untuk menggunakan jenis jumlah:
class Expr: # ... def match(self, handlers): # ...
Tetapi pertama sekali kita perlu membuat varian yang berbeza sedikit lebih seragam. Daripada menyimpan datanya dalam pelbagai medan, setiap varian kini akan menyimpannya dalam tuple bernama 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
Ini membolehkan kami melaksanakan padanan:
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}")
Dalam satu masa, kami telah menyelesaikan semua masalah yang dinyatakan di atas! Sebagai contoh lain, dan untuk perubahan pemandangan, berikut ialah jenis Rust's Option yang ditranskripsikan dalam fesyen ini:
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)
Sebagai faedah kualiti hidup yang kecil, kami boleh menyokong kad bebas khas atau pengendali "catchall" dalam perlawanan, ditunjukkan dengan garis bawah (_):
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}")
Ini membolehkan kami menggunakan padanan seperti:
def map(self, fn): return self.match( some=lambda x: Option.some(fn(x)), _=lambda: Option.none(), )
Seperti yang ditunjukkan oleh kelas Option, banyak kod yang diperlukan untuk mencipta jenis jumlah mengikut corak yang sama:
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}")
Daripada menulis ini sendiri, mari kita tulis penghias untuk menjana kaedah ini berdasarkan beberapa huraian varian.
def enum(**variants): pass
Perihalan macam mana? Perkara yang paling mudah ialah membekalkan senarai nama varian, tetapi kita boleh melakukan lebih baik sedikit dengan turut menyediakan jenis hujah yang kita jangkakan. Kami akan menggunakan enum untuk meningkatkan kelas Opsyen kami secara automatik seperti ini:
# 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
Struktur asas enum kelihatan seperti ini:
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
Ia adalah fungsi yang mengembalikan fungsi satu lagi, yang akan dipanggil dengan kelas yang kami pertingkatkan sebagai satu-satunya hujahnya. Dalam peningkatan kami akan melampirkan kaedah untuk membina setiap varian, bersama-sama dengan padanan.
Pertama, padankan, kerana ia hanya salinan pasta:
# 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
Menambah kaedah untuk membina setiap varian hanya terlibat sedikit sahaja. Kami mengulangi kamus varian, mentakrifkan kaedah untuk setiap entri:
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
di mana make_constructor mencipta fungsi pembina untuk varian dengan teg (dan nama) dan tanda "tandatangan jenis":
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)
Berikut ialah takrifan penuh enum untuk rujukan.
Kami boleh meningkatkan kelas jumlah kami dengan mudah dengan kaedah __repr__ dan __eq__:
class Expr: # ... def match(self, handlers): # ...
Dengan peningkatan yang dipertingkatkan dalam fesyen ini, kami boleh menentukan Pilihan dengan cruft minimum:
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
Malangnya, enum belum (belum) mencapai tugas untuk mentakrifkan 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}")
Kami menggunakan Expr kelas sebelum ia ditakrifkan. Penyelesaian mudah di sini ialah dengan hanya memanggil penghias selepas menentukan kelas:
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)
Tetapi ada perubahan mudah yang boleh kita lakukan untuk menyokong ini: benarkan "tandatangan" menjadi fungsi yang mengembalikan tuple:
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}")
Semua ini memerlukan perubahan kecil dalam make_constructor:
def map(self, fn): return self.match( some=lambda x: Option.some(fn(x)), _=lambda: Option.none(), )
Walaupun ia mungkin berguna, penghias enum baharu kami yang mewah bukan tanpa kekurangannya. Yang paling ketara ialah ketidakupayaan untuk melakukan apa-apa jenis padanan corak "bersarang". Dalam Rust, kita boleh melakukan perkara seperti ini:
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}")
Tetapi kami terpaksa melakukan perlawanan berganda untuk mencapai keputusan yang sama:
def enum(**variants): pass
Maksudnya, kes seperti ini kelihatan agak jarang berlaku.
Satu lagi kelemahan ialah padanan memerlukan membina dan memanggil banyak fungsi. Ini bermakna ia berkemungkinan jauh lebih perlahan daripada rantai if-else yang setara. Walau bagaimanapun, peraturan biasa digunakan di sini: gunakan enum jika anda menyukai faedah ergonomiknya dan gantikannya dengan kod "dijana" jika ia terlalu perlahan.
Atas ialah kandungan terperinci Jumlah Jenis dalam Python. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!