Skip to content

Instantly share code, notes, and snippets.

@Xophmeister
Last active January 25, 2023 08:02
Show Gist options
  • Save Xophmeister/8d5ed64058e0191378d7f32a88e79564 to your computer and use it in GitHub Desktop.
Save Xophmeister/8d5ed64058e0191378d7f32a88e79564 to your computer and use it in GitHub Desktop.
Python metaclass to enforce "good behaviour" from simulated sum type class hierarchies
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))=}")
"""
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
"""
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