Last active
January 25, 2023 08:02
-
-
Save Xophmeister/8d5ed64058e0191378d7f32a88e79564 to your computer and use it in GitHub Desktop.
Python metaclass to enforce "good behaviour" from simulated sum type class hierarchies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from math import pi | |
from .shape import Shape, Circle, Rectangle | |
def area(shape: Shape) -> float: | |
""" | |
Calculate the area of the given shape | |
""" | |
match shape: | |
case Circle(radius): | |
return pi * (radius ** 2) | |
case Rectangle(width, height): | |
return width * height | |
if __name__ == "__main__": | |
print(f"{area(Circle(1))=}") | |
print(f"{area(Rectangle(2, 3))=}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Sum types can be simulated using a class hierarchy. This metaclass | |
enforces that: | |
* The root sum type class cannot be instantiated | |
* The branches of the sum type are direct children of the root class | |
* A simple inheritence structure | |
The last point is overly restrictive and will prevent, for example, | |
using abstract base classes to simulate typeclasses. | |
""" | |
from __future__ import annotations | |
from typing import ClassVar, Type | |
class SumType(type): | |
""" | |
Metaclass to enforce good behaviour from sum type class hierarchies | |
""" | |
sum_types: ClassVar[set[Type[SumType]]] = set() | |
def __new__(meta, name, bases, defns): | |
cls = type.__new__(meta, name, bases, defns) | |
match bases: | |
case (): | |
# Register a new sum type | |
meta.sum_types.add(cls) | |
# Prevent the root class from being instantiated | |
def _fail_to_construct(self): | |
raise TypeError(f"Cannot instantiate root sum type \"{name}\"") | |
cls.__init__ = _fail_to_construct | |
case (base,): | |
# Only allow subclasses of known sum type root classes | |
if base not in meta.sum_types: | |
raise TypeError(f"Sum type \"{name}\" has an invalid root: \"{base.__name__}\"") | |
case _: | |
# Prevent multiple inheritance | |
raise TypeError("Sum types do not support multiple inheritance") | |
return cls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Approximate equivalent to the following Haskell: | |
data Shape = Circle double | |
| Rectangle double double | |
(Modulo using record types here for clarity) | |
""" | |
from dataclasses import dataclass | |
from typing import final | |
from .meta import SumType | |
# Root sum type class | |
class Shape(metaclass=SumType): ... | |
@final | |
@dataclass(frozen=True) | |
class Circle(Shape): | |
radius: float | |
@final | |
@dataclass(frozen=True) | |
class Rectangle(Shape): | |
width: float | |
height: float | |
# Defining a sum type branch with an invalid root will raise a TypeError | |
# class Square(Rectangle): ... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment