(skip to the heading The Proposal if you’re already familiar with the problem)
The Problem
Consider this code in Swift:
enum BgColor {
case transparent
case name(String)
case rgb(Int, Int, Int)
case hsv(Int, Int, Int)
}
var backgroundColor = BgColor.rgb(39, 127, 168)
switch backgroundColor {
case BgColor.transparent:
print("no color")
case let BgColor.name(colorName):
print("color name: \(colorName)")
case let Barcode.rgb(red, green, blue):
print("RGB: \(red), \(green), \(blue).")
case let Barcode.hsv(hue, saturation, value)
print("HSV: \(hue), \(saturation), \(value).")
}
it let’s you precisely express that there are 4 possibilities and that different values are associated with each possibility.
In Python, you might express this like this:
background_color = {"type": "rgb", "val": (39, 127, 168)}
match background_color:
case {"type": "transparent"}:
print("no color")
case {"type": "name", "val": color_name}:
print(f"color name: {color_name}")
case {"type": "rgb", "val": (red, green, blue)}:
print(f"RGB: {red}, {green}, {blue}")
case {"type": "hsv", "val": (hue, saturation, value)}:
print(f"HSV: {hue}, {saturation}, {value}")
This works if you’re not interested in static type checking (which is fine), but if you are interested in static type checking then you’ll find that the current ways to make this type-safe are not as convenient as the Swift code.
You could type the above code with TypedDict
but I think most would probably do it with NamedTuple
s:
from typing import NamedTuple, TypeAlias
class Transparent:
pass
class Name(NamedTuple):
color_name: str
class Rgb(NamedTuple):
red: int
green: int
blue: int
class Hsv(NamedTuple):
hue: int
saturation: int
value: int
BgColor: TypeAlias = Transparent | Name | Rgb | Hsv
background_color: BgColor = Rgb(39, 127, 168)
assert isinstance(background_color, BgColor)
match background_color:
case Transparent():
print("no color")
case Name(color_name):
print(f"color name: {color_name}")
case Rgb(red, green, blue):
print(f"RGB: {red}, {green}, {blue}")
case Hsv(hue, saturation, value):
print(f"HSV: {hue}, {saturation}, {value}")
As you can see, this has become very verbose.
You can do it a bit shorter, but it’s still not that elegant:
class Transparent: ...
Name = NamedTuple("Name", [("value", str)])
Rgb = NamedTuple("Rgb", [("value", tuple[int, int, int])])
Hsv = NamedTuple("Hsv", [("value", tuple[int, int, int])])
BgColor: TypeAlias = Transparent | Name | Rgb | Hsv
The Proposal
To solve this problem, I would like to propose a new construct: TypeEnum
. It’s like an Enum
but instead of its elements being values, they’re types.
(Alternative names for this concept: NamedUnion
, TaggedUnion
, NamedTupleUnion
.)
It works like this:
from typing import TypeEnum
class BgColor(TypeEnum):
transparent = ()
name = (str,)
rgb = (int, int, int)
hsv = (int, int, int)
background_color = BgColor.rgb(39, 127, 168)
assert isinstance(background_color, BgColor)
assert not isinstance(BgColor.rgb, BgColor) # different from Enum
match background_color:
case BgColor.transparent:
print("no color")
case BgColor.name(color_name):
print(f"color name: {color_name}")
case BgColor.rgb(red, green, blue):
print(f"RGB: {red}, {green}, {blue}")
case BgColor.hsv(hue, saturation, value):
print(f"HSV: {hue}, {saturation}, {value}")
Under the hood, TypeEnum
does something like this:
class BgColor:
transparent = 0
name = NamedTuple("name", [("item0", str)])
rgb = NamedTuple("rgb", [("item0", int) , ("item1", int), ("item2", int)])
hsv = NamedTuple("hsv", [("item0", int) , ("item1", int), ("item2", int)])
However, this doesn’t include the magic necessary to make isinstance(BgColor.name("cerulean"), BgColor)
work.
I’m using NamedTuple
here because I need something that will populate __match_args__
for the pattern matching, and NamedTuple
is an easy way to get that. In the actual implementation, it probably wouldn’t be really NamedTuple
but it needs to be something that you can pattern-match on.
It would also be nice if there was an option for named fields, but it’s not a must from my side:
from typing import TypeEnum
class BgColor(TypeEnum):
name = (str,)
rgb = {"red": int, "green": int, "blue": int} # named args
background_color = BgColor.rgb(red=39, green=127, blue=168)
print(background_color.red)
This syntax with named fields was actually previously suggested in this mypy issue:
So, what do you think?