@@ -25,7 +25,7 @@ from __future__ import annotations
25
25
from dataclasses import dataclass
26
26
from typing import Generic, Tuple, TypeVar, final
27
27
28
- from expression import Tag, TaggedUnion, match
28
+ from expression import case, tag, tagged_union
29
29
30
30
_T = TypeVar("_T")
31
31
```
@@ -50,18 +50,20 @@ cannot. With tagged unions each of the union cases produces the same type which
50
50
we use a static create method for to create each of the union cases.
51
51
52
52
``` {code-cell} python
53
- @final
54
- class Shape(TaggedUnion):
55
- RECTANGLE = Tag[Rectangle]()
56
- CIRCLE = Tag[Circle]()
53
+ @tagged_union
54
+ class Shape:
55
+ tag: Literal["rectangle", "circle"] = tag()
56
+
57
+ rectangle: Rectangle = case()
58
+ circle: Circle = case()
57
59
58
60
@staticmethod
59
- def rectangle (width: float, length: float) -> Shape:
60
- return Shape(Shape.RECTANGLE, Rectangle(width, length))
61
+ def Rectangle (width: float, length: float) -> Shape:
62
+ return Shape(rectangle= Rectangle(width, length))
61
63
62
64
@staticmethod
63
- def circle (radius: float) -> Shape:
64
- return Shape(Shape.CIRCLE, Circle(radius))
65
+ def Circle (radius: float) -> Shape:
66
+ return Shape(circle= Circle(radius))
65
67
```
66
68
67
69
A more complex type modelling example:
@@ -74,97 +76,102 @@ from expression import TaggedUnion, match
74
76
from expression.core.union import Tag
75
77
76
78
77
- @final
78
- class Suit(TaggedUnion):
79
- HEARTS = Tag[None]
80
- SPADES = Tag[None]()
81
- CLUBS = Tag[None]()
82
- DIAMONDS = Tag[None]()
79
+ @tagged_union
80
+ class Suit:
81
+ tag: Literal["spades", "hearts", "clubs", "diamonds"] = tag()
82
+
83
+ spades: Literal[True] = case()
84
+ hearts: Literal[True] = case()
85
+ clubs: Literal[True] = case()
86
+ diamonds: Literal[True] = case()
83
87
84
88
@staticmethod
85
- def hearts () -> Suit:
86
- return Suit(Suit.HEARTS() )
89
+ def Spades () -> Suit:
90
+ return Suit(spades=True )
87
91
88
92
@staticmethod
89
- def spades () -> Suit:
90
- return Suit(Suit.SPADES )
93
+ def Hearts () -> Suit:
94
+ return Suit(hearts=True )
91
95
92
96
@staticmethod
93
- def clubs () -> Suit:
94
- return Suit(Suit.CLUBS )
97
+ def Clubs () -> Suit:
98
+ return Suit(clubs=True )
95
99
96
100
@staticmethod
97
- def diamonds () -> Suit:
98
- return Suit(Suit.DIAMONDS )
101
+ def Diamonds () -> Suit:
102
+ return Suit(diamonds=True )
99
103
104
+ @tagged_union
105
+ class Face:
106
+ tag: Literal["jack", "queen", "king", "ace"] = tag()
100
107
101
- @final
102
- class Face(TaggedUnion):
103
- JACK = Tag[None]()
104
- QUEEN = Tag[None]()
105
- KIND = Tag[None]()
106
- ACE = Tag[None]()
108
+ jack: Literal[True] = case()
109
+ queen: Literal[True] = case()
110
+ king: Literal[True] = case()
111
+ ace: Literal[True] = case()
107
112
108
113
@staticmethod
109
- def jack () -> Face:
110
- return Face(Face.JACK )
114
+ def Jack () -> Face:
115
+ return Face(jack=True )
111
116
112
117
@staticmethod
113
- def queen () -> Face:
114
- return Face(Face.QUEEN )
118
+ def Queen () -> Face:
119
+ return Face(queen=True )
115
120
116
121
@staticmethod
117
- def king () -> Face:
118
- return Face(Face.KIND )
122
+ def King () -> Face:
123
+ return Face(king=True )
119
124
120
125
@staticmethod
121
- def ace() -> Face:
122
- return Face(Face.ACE)
126
+ def Ace() -> Face:
127
+ return Face(ace=True)
128
+
123
129
130
+ @tagged_union
131
+ class Card:
132
+ tag: Literal["value", "face", "joker"] = tag()
124
133
125
- @final
126
- class Card(TaggedUnion):
127
- FACE_CARD = Tag[Tuple[Suit, Face]]()
128
- VALUE_CARD = Tag[Tuple[Suit, int]]()
129
- JOKER = Tag[None]()
134
+ face: tuple[Suit, Face] = case()
135
+ value: tuple[Suit, int] = case()
136
+ joker: Literal[True] = case()
130
137
131
138
@staticmethod
132
- def face_card (suit: Suit, face: Face) -> Card:
133
- return Card(Card.FACE_CARD, suit= suit, face=face )
139
+ def Face (suit: Suit, face: Face) -> Card:
140
+ return Card(face=( suit, face) )
134
141
135
142
@staticmethod
136
- def value_card(suit: Suit, value: int) -> Card:
137
- return Card(Card.VALUE_CARD, suit=suit, value=value)
143
+ def Value(suit: Suit, value: int) -> Card:
144
+ if value < 1 or value > 10:
145
+ raise ValueError("Value must be between 1 and 10")
146
+ return Card(value=(suit, value))
138
147
139
148
@staticmethod
140
149
def Joker() -> Card:
141
- return Card(Card.JOKER )
150
+ return Card(joker=True )
142
151
143
152
144
- jack_of_hearts = Card.face_card( Suit.hearts (), Face.jack ())
145
- three_of_clubs = Card.value_card( Suit.clubs (), 3)
153
+ jack_of_hearts = Card.Face(suit= Suit.Hearts (), face= Face.Jack ())
154
+ three_of_clubs = Card.Value(suit= Suit.Clubs (), value= 3)
146
155
joker = Card.Joker()
147
156
```
148
157
149
158
We can now use our types with pattern matching to create our domain logic:
150
159
151
160
``` {code-cell} python
152
161
def calculate_value(card: Card) -> int:
153
- with match(card) as case:
154
- if case(Card.JOKER):
155
- return 0
156
- if case(Card.FACE_CARD(suit=Suit.SPADES, face=Face.QUEEN)):
162
+ match card:
163
+ case Card(tag="face", face=(Suit(spades=True), Face(queen=True))):
157
164
return 40
158
- if case( Card.FACE_CARD( face= Face.ACE )):
165
+ case Card(tag=" face", face=(_suit, Face(ace=True) )):
159
166
return 15
160
- if case(Card.FACE_CARD()):
161
- return 10
162
- if case(Card.VALUE_CARD(value=10)):
167
+ case Card(tag="face", face=(_suit, _face)):
163
168
return 10
164
- if case._:
165
- return 5
166
-
167
- assert False
169
+ case Card(tag="value", value=(_suit, value)):
170
+ return value
171
+ case Card(tag="joker", joker=True):
172
+ return 0
173
+ case _:
174
+ raise AssertionError("Should not match")
168
175
169
176
170
177
rummy_score = calculate_value(jack_of_hearts)
@@ -175,4 +182,34 @@ print("Score: ", rummy_score)
175
182
176
183
rummy_score = calculate_value(joker)
177
184
print("Score: ", rummy_score)
178
- ```
185
+ ```
186
+
187
+ ## Single case tagged unions
188
+
189
+ You can also use tagged unions to create single case tagged unions. This is useful
190
+ when you want to create a type that is different from the underlying type. For example
191
+ you may want to create a type that is a string but is a different type to a normal
192
+ string:
193
+
194
+ ``` {code-cell} python
195
+ @tagged_union(frozen=True, repr=False)
196
+ class SecurePassword:
197
+ password: str = case()
198
+
199
+ # Override __str__ and __repr__ to make sure we don't leak the password in logs
200
+ def __str__(self) -> str:
201
+ return "********"
202
+
203
+ def __repr__(self) -> str:
204
+ return "SecurePassword(password='********')"
205
+
206
+ password = SecurePassword(password="secret")
207
+ match password:
208
+ case SecurePassword(password=p):
209
+ assert p == "secret"
210
+
211
+ ```
212
+
213
+ This will make sure that the password is not leaked in logs or when printed to the
214
+ console, and that we don't assign a password to a normal string anywhere in our code.
215
+
0 commit comments