grscheller.fp.woException

Maybe and Either Monads.

Functional data types to use in lieu of exceptions.

Monadic types:
  • MB: Maybe monad
  • XOR: Left biased Either monad
  1# Copyright 2023-2024 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""### Maybe and Either Monads.
 16
 17Functional data types to use in lieu of exceptions.
 18
 19##### Monadic types:
 20
 21* **MB:** Maybe monad
 22* **XOR:** Left biased Either monad
 23
 24"""
 25from __future__ import annotations
 26
 27__all__ = [ 'MB', 'XOR', 'mb_to_xor', 'xor_to_mb' ]
 28
 29from typing import Callable, cast, Final, Generic, Iterator, Never, TypeVar
 30from .nothingness import _NoValue, noValue
 31
 32D = TypeVar('D')
 33U = TypeVar('U')
 34L = TypeVar('L')
 35R = TypeVar('R')
 36
 37class MB(Generic[D]):
 38    """#### Maybe Monad
 39
 40    Class wrapping a potentially missing value.
 41
 42    * where `MB(value)` contains a possible value of type `~D`
 43    * `MB( )` semantically represent a non-existent or missing value of type ~D
 44    * immutable, a `MB` does not change after being created
 45      * immutable semantics, map & flatMap return new instances
 46      * warning: contained values need not be immutable
 47      * warning: not hashable if a mutable value is contained
 48    * implementation detail:
 49      * `MB( )` contains `sentinel` as a sentinel value
 50        * as a result, a MB cannot semantically contain the `sentinel` value
 51    * raises `ValueError` if empty and get method not given a default value
 52
 53    """
 54    __slots__ = '_value',
 55
 56    def __init__(self, value: D|_NoValue=noValue) -> None:
 57        self._value = value
 58
 59    def __bool__(self) -> bool:
 60        return not self._value is noValue
 61
 62    def __iter__(self) -> Iterator[D]:
 63        if self:
 64            yield cast(D, self._value)
 65
 66    def __repr__(self) -> str:
 67        if self:
 68            return 'MB(' + repr(self._value) + ')'
 69        else:
 70            return 'MB()'
 71
 72    def __len__(self) -> int:
 73        return (1 if self else 0)
 74
 75    def __eq__(self, other: object) -> bool:
 76        if not isinstance(other, type(self)):
 77            return False
 78
 79        if self._value is other._value:
 80            return True
 81        return self._value == other._value
 82
 83    def get(self, alt: D|_NoValue=noValue) -> D|Never:
 84        """Return the contained value if it exists, otherwise an alternate value.
 85
 86        * alternate value must me of type ~D
 87        * raises `ValueError` if an alternate value is not provided but needed
 88
 89        """
 90        if self._value is not noValue:
 91            return cast(D, self._value)
 92        else:
 93            if alt is not noValue:
 94                return cast(D, alt)
 95            else:
 96                msg = 'An alternate return type not provided.'
 97                raise ValueError(msg)
 98
 99    def map(self, f: Callable[[D], U]) -> MB[U]:
100        """Map function f over the 0 or 1 elements of this data structure."""
101        return (MB(f(cast(D, self._value))) if self else MB())
102
103    def flatmap(self, f: Callable[[D], MB[U]]) -> MB[U]:
104        """Map MB with function f and flatten."""
105        return (f(cast(D, self._value)) if self else MB())
106
107class XOR(Generic[L, R]):
108    """#### Either Monad
109
110    Class semantically containing either a "left" or a "right" value,
111    but not both.
112
113    * implements a left biased Either Monad
114      * `XOR(left: L, right: R)` produces a "left" value
115        * with a default potential "right" value
116      * `XOR(left)` produces a "left" value
117      * `XOR(right=right)` produces a "right" value
118    * in a Boolean context, returns True if a "left", False if a "right"
119    * two `XOR` objects compare as equal when
120      * both are left values or both are right values which
121        * contain the same value or
122        * whose values compare as equal
123    * immutable, an `XOR` does not change after being created
124      * immutable semantics, map & flatMap return new instances
125      * warning: contained values need not be immutable
126      * warning: not hashable if value or potential right value mutable
127    * `get` and `getRight` methods can raises `ValueError` when
128      * a "right" value is needed but a potential "right" value was not given
129
130    """
131    __slots__ = '_left', '_right'
132
133    def __init__(self, left: L|_NoValue=noValue, right: R|_NoValue=noValue) -> None:
134        self._left, self._right = left, right
135
136    def __bool__(self) -> bool:
137        return self._left is not noValue
138
139    def __iter__(self) -> Iterator[L]:
140        if self._left is not noValue:
141            yield cast(L, self._left)
142
143    def __repr__(self) -> str:
144        return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
145
146    def __str__(self) -> str:
147        if self:
148            return '< ' + str(self._left) + ' | >'
149        else:
150            return '< | ' + str(self._right) + ' >'
151
152    def __len__(self) -> int:
153        # Semantically, an XOR always contains just one value.
154        return 1
155
156    def __eq__(self, other: object) -> bool:
157        if not isinstance(other, type(self)):
158            return False
159
160        if self and other:
161            if self._left is other._left:
162                return True
163            return self._left == other._left
164        elif not self and not other:
165            if self._right is other._right:
166                return True
167            return self._right == other._right
168        else:
169            return False
170
171    def get(self, alt: L|_NoValue=noValue) -> L|Never:
172        """Get value if a Left.
173
174        * if the XOR is a left, return its value
175        * otherwise, return alt: L if it is provided
176        * alternate value must me of type ~L
177        * raises `ValueError` if an alternate value is not provided but needed
178
179        """
180        if self._left is noValue:
181            if alt is noValue:
182                msg = 'An alt return value was needed by get, but none was provided.'
183                raise ValueError(msg)
184            else:
185                return cast(L, alt)
186        else:
187            return cast(L, self._left)
188
189    def getRight(self, alt: R|_NoValue=noValue) -> R|Never:
190        """Get value of `XOR` if a Right, potential right value if a left.
191
192        * if XOR is a right, return its value
193          * otherwise return a provided alternate value of type ~R
194        * if XOR is a left, return the potential right value
195          * raises `ValueError` if a potential right value was not provided
196
197        """
198        if self:
199            if alt is noValue:
200                if self._right is noValue:
201                    msg = 'A potential right was needed by get, but none was provided.'
202                    raise ValueError(msg)
203                else:
204                    return cast(R, self._right)
205            else:
206                return cast(R, alt)
207        else:
208            return cast(R, self._right)
209
210    def makeRight(self, right: R|_NoValue=noValue) -> XOR[L, R]:
211        """Make right
212
213        Return a new instance transformed into a right `XOR`. Change the right
214        value to `right` if given.
215        """
216        if right is noValue:
217            right = self.getRight()
218        return cast(XOR[L, R], XOR(right=right))
219
220    def swapRight(self, right: R) -> XOR[L, R]:
221        """Swap in a new right value, returns a new instance with a new right
222        (or potential right) value.
223        """
224        if self._left is noValue:
225            return cast(XOR[L, R], XOR(right=right))
226        else:
227            return XOR(self.get(), right)
228
229    def map(self, f: Callable[[L], U]) -> XOR[U, R]:
230        """Map over if a left value.
231
232        * if `XOR` is a "left" then map `f` over its value
233          * if `f` successful return a left XOR[S, R]
234          * if `f` unsuccessful return right `XOR`
235            * swallows any exceptions `f` may throw
236        * if `XOR` is a "right"
237          * return new `XOR(right=self._right): XOR[S, R]`
238          * use method mapRight to adjust the returned value
239
240        """
241        if self._left is noValue:
242            return cast(XOR[U, R], XOR(right=self._right))
243
244        try:
245            applied = f(cast(L, self._left))
246        except Exception:
247            return XOR(right=self._right)
248        else:
249            return XOR(applied, self._right)
250
251    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
252        """Map over a right or potential right value."""
253        return XOR(self._left, g(cast(R, self._right)))
254
255    def flatMap(self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
256        """Flatmap - Monadically bind
257
258        * map over then flatten left values
259        * propagate right values
260
261        """
262        if self._left is noValue:
263            return XOR(noValue, self._right)
264        else:
265            return f(cast(L, self._left))
266
267# Conversion functions
268
269def mb_to_xor(m: MB[D], right: R) -> XOR[D, R]:
270    """Convert a MB to an XOR."""
271    if m:
272        return XOR(m.get(), right)
273    else:
274        return XOR(noValue, right)
275
276def xor_to_mb(e: XOR[D,U]) -> MB[D]:
277    """Convert an XOR to a MB."""
278    if e:
279        return MB(e.get())
280    else:
281        return MB()
class MB(typing.Generic[~D]):
 38class MB(Generic[D]):
 39    """#### Maybe Monad
 40
 41    Class wrapping a potentially missing value.
 42
 43    * where `MB(value)` contains a possible value of type `~D`
 44    * `MB( )` semantically represent a non-existent or missing value of type ~D
 45    * immutable, a `MB` does not change after being created
 46      * immutable semantics, map & flatMap return new instances
 47      * warning: contained values need not be immutable
 48      * warning: not hashable if a mutable value is contained
 49    * implementation detail:
 50      * `MB( )` contains `sentinel` as a sentinel value
 51        * as a result, a MB cannot semantically contain the `sentinel` value
 52    * raises `ValueError` if empty and get method not given a default value
 53
 54    """
 55    __slots__ = '_value',
 56
 57    def __init__(self, value: D|_NoValue=noValue) -> None:
 58        self._value = value
 59
 60    def __bool__(self) -> bool:
 61        return not self._value is noValue
 62
 63    def __iter__(self) -> Iterator[D]:
 64        if self:
 65            yield cast(D, self._value)
 66
 67    def __repr__(self) -> str:
 68        if self:
 69            return 'MB(' + repr(self._value) + ')'
 70        else:
 71            return 'MB()'
 72
 73    def __len__(self) -> int:
 74        return (1 if self else 0)
 75
 76    def __eq__(self, other: object) -> bool:
 77        if not isinstance(other, type(self)):
 78            return False
 79
 80        if self._value is other._value:
 81            return True
 82        return self._value == other._value
 83
 84    def get(self, alt: D|_NoValue=noValue) -> D|Never:
 85        """Return the contained value if it exists, otherwise an alternate value.
 86
 87        * alternate value must me of type ~D
 88        * raises `ValueError` if an alternate value is not provided but needed
 89
 90        """
 91        if self._value is not noValue:
 92            return cast(D, self._value)
 93        else:
 94            if alt is not noValue:
 95                return cast(D, alt)
 96            else:
 97                msg = 'An alternate return type not provided.'
 98                raise ValueError(msg)
 99
100    def map(self, f: Callable[[D], U]) -> MB[U]:
101        """Map function f over the 0 or 1 elements of this data structure."""
102        return (MB(f(cast(D, self._value))) if self else MB())
103
104    def flatmap(self, f: Callable[[D], MB[U]]) -> MB[U]:
105        """Map MB with function f and flatten."""
106        return (f(cast(D, self._value)) if self else MB())

Maybe Monad

Class wrapping a potentially missing value.

  • where MB(value) contains a possible value of type ~D
  • MB( ) semantically represent a non-existent or missing value of type ~D
  • immutable, a MB does not change after being created
    • immutable semantics, map & flatMap return new instances
    • warning: contained values need not be immutable
    • warning: not hashable if a mutable value is contained
  • implementation detail:
    • MB( ) contains sentinel as a sentinel value
      • as a result, a MB cannot semantically contain the sentinel value
  • raises ValueError if empty and get method not given a default value
MB(value: Union[~D, grscheller.fp.nothingness._NoValue] = noValue)
57    def __init__(self, value: D|_NoValue=noValue) -> None:
58        self._value = value
def get( self, alt: Union[~D, grscheller.fp.nothingness._NoValue] = noValue) -> Union[~D, Never]:
84    def get(self, alt: D|_NoValue=noValue) -> D|Never:
85        """Return the contained value if it exists, otherwise an alternate value.
86
87        * alternate value must me of type ~D
88        * raises `ValueError` if an alternate value is not provided but needed
89
90        """
91        if self._value is not noValue:
92            return cast(D, self._value)
93        else:
94            if alt is not noValue:
95                return cast(D, alt)
96            else:
97                msg = 'An alternate return type not provided.'
98                raise ValueError(msg)

Return the contained value if it exists, otherwise an alternate value.

  • alternate value must me of type ~D
  • raises ValueError if an alternate value is not provided but needed
def map(self, f: Callable[[~D], ~U]) -> MB[~U]:
100    def map(self, f: Callable[[D], U]) -> MB[U]:
101        """Map function f over the 0 or 1 elements of this data structure."""
102        return (MB(f(cast(D, self._value))) if self else MB())

Map function f over the 0 or 1 elements of this data structure.

def flatmap( self, f: Callable[[~D], MB[~U]]) -> MB[~U]:
104    def flatmap(self, f: Callable[[D], MB[U]]) -> MB[U]:
105        """Map MB with function f and flatten."""
106        return (f(cast(D, self._value)) if self else MB())

Map MB with function f and flatten.

class XOR(typing.Generic[~L, ~R]):
108class XOR(Generic[L, R]):
109    """#### Either Monad
110
111    Class semantically containing either a "left" or a "right" value,
112    but not both.
113
114    * implements a left biased Either Monad
115      * `XOR(left: L, right: R)` produces a "left" value
116        * with a default potential "right" value
117      * `XOR(left)` produces a "left" value
118      * `XOR(right=right)` produces a "right" value
119    * in a Boolean context, returns True if a "left", False if a "right"
120    * two `XOR` objects compare as equal when
121      * both are left values or both are right values which
122        * contain the same value or
123        * whose values compare as equal
124    * immutable, an `XOR` does not change after being created
125      * immutable semantics, map & flatMap return new instances
126      * warning: contained values need not be immutable
127      * warning: not hashable if value or potential right value mutable
128    * `get` and `getRight` methods can raises `ValueError` when
129      * a "right" value is needed but a potential "right" value was not given
130
131    """
132    __slots__ = '_left', '_right'
133
134    def __init__(self, left: L|_NoValue=noValue, right: R|_NoValue=noValue) -> None:
135        self._left, self._right = left, right
136
137    def __bool__(self) -> bool:
138        return self._left is not noValue
139
140    def __iter__(self) -> Iterator[L]:
141        if self._left is not noValue:
142            yield cast(L, self._left)
143
144    def __repr__(self) -> str:
145        return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
146
147    def __str__(self) -> str:
148        if self:
149            return '< ' + str(self._left) + ' | >'
150        else:
151            return '< | ' + str(self._right) + ' >'
152
153    def __len__(self) -> int:
154        # Semantically, an XOR always contains just one value.
155        return 1
156
157    def __eq__(self, other: object) -> bool:
158        if not isinstance(other, type(self)):
159            return False
160
161        if self and other:
162            if self._left is other._left:
163                return True
164            return self._left == other._left
165        elif not self and not other:
166            if self._right is other._right:
167                return True
168            return self._right == other._right
169        else:
170            return False
171
172    def get(self, alt: L|_NoValue=noValue) -> L|Never:
173        """Get value if a Left.
174
175        * if the XOR is a left, return its value
176        * otherwise, return alt: L if it is provided
177        * alternate value must me of type ~L
178        * raises `ValueError` if an alternate value is not provided but needed
179
180        """
181        if self._left is noValue:
182            if alt is noValue:
183                msg = 'An alt return value was needed by get, but none was provided.'
184                raise ValueError(msg)
185            else:
186                return cast(L, alt)
187        else:
188            return cast(L, self._left)
189
190    def getRight(self, alt: R|_NoValue=noValue) -> R|Never:
191        """Get value of `XOR` if a Right, potential right value if a left.
192
193        * if XOR is a right, return its value
194          * otherwise return a provided alternate value of type ~R
195        * if XOR is a left, return the potential right value
196          * raises `ValueError` if a potential right value was not provided
197
198        """
199        if self:
200            if alt is noValue:
201                if self._right is noValue:
202                    msg = 'A potential right was needed by get, but none was provided.'
203                    raise ValueError(msg)
204                else:
205                    return cast(R, self._right)
206            else:
207                return cast(R, alt)
208        else:
209            return cast(R, self._right)
210
211    def makeRight(self, right: R|_NoValue=noValue) -> XOR[L, R]:
212        """Make right
213
214        Return a new instance transformed into a right `XOR`. Change the right
215        value to `right` if given.
216        """
217        if right is noValue:
218            right = self.getRight()
219        return cast(XOR[L, R], XOR(right=right))
220
221    def swapRight(self, right: R) -> XOR[L, R]:
222        """Swap in a new right value, returns a new instance with a new right
223        (or potential right) value.
224        """
225        if self._left is noValue:
226            return cast(XOR[L, R], XOR(right=right))
227        else:
228            return XOR(self.get(), right)
229
230    def map(self, f: Callable[[L], U]) -> XOR[U, R]:
231        """Map over if a left value.
232
233        * if `XOR` is a "left" then map `f` over its value
234          * if `f` successful return a left XOR[S, R]
235          * if `f` unsuccessful return right `XOR`
236            * swallows any exceptions `f` may throw
237        * if `XOR` is a "right"
238          * return new `XOR(right=self._right): XOR[S, R]`
239          * use method mapRight to adjust the returned value
240
241        """
242        if self._left is noValue:
243            return cast(XOR[U, R], XOR(right=self._right))
244
245        try:
246            applied = f(cast(L, self._left))
247        except Exception:
248            return XOR(right=self._right)
249        else:
250            return XOR(applied, self._right)
251
252    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
253        """Map over a right or potential right value."""
254        return XOR(self._left, g(cast(R, self._right)))
255
256    def flatMap(self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
257        """Flatmap - Monadically bind
258
259        * map over then flatten left values
260        * propagate right values
261
262        """
263        if self._left is noValue:
264            return XOR(noValue, self._right)
265        else:
266            return f(cast(L, self._left))

Either Monad

Class semantically containing either a "left" or a "right" value, but not both.

  • implements a left biased Either Monad
    • XOR(left: L, right: R) produces a "left" value
      • with a default potential "right" value
    • XOR(left) produces a "left" value
    • XOR(right=right) produces a "right" value
  • in a Boolean context, returns True if a "left", False if a "right"
  • two XOR objects compare as equal when
    • both are left values or both are right values which
      • contain the same value or
      • whose values compare as equal
  • immutable, an XOR does not change after being created
    • immutable semantics, map & flatMap return new instances
    • warning: contained values need not be immutable
    • warning: not hashable if value or potential right value mutable
  • get and getRight methods can raises ValueError when
    • a "right" value is needed but a potential "right" value was not given
XOR( left: Union[~L, grscheller.fp.nothingness._NoValue] = noValue, right: Union[~R, grscheller.fp.nothingness._NoValue] = noValue)
134    def __init__(self, left: L|_NoValue=noValue, right: R|_NoValue=noValue) -> None:
135        self._left, self._right = left, right
def get( self, alt: Union[~L, grscheller.fp.nothingness._NoValue] = noValue) -> Union[~L, Never]:
172    def get(self, alt: L|_NoValue=noValue) -> L|Never:
173        """Get value if a Left.
174
175        * if the XOR is a left, return its value
176        * otherwise, return alt: L if it is provided
177        * alternate value must me of type ~L
178        * raises `ValueError` if an alternate value is not provided but needed
179
180        """
181        if self._left is noValue:
182            if alt is noValue:
183                msg = 'An alt return value was needed by get, but none was provided.'
184                raise ValueError(msg)
185            else:
186                return cast(L, alt)
187        else:
188            return cast(L, self._left)

Get value if a Left.

  • if the XOR is a left, return its value
  • otherwise, return alt: L if it is provided
  • alternate value must me of type ~L
  • raises ValueError if an alternate value is not provided but needed
def getRight( self, alt: Union[~R, grscheller.fp.nothingness._NoValue] = noValue) -> Union[~R, Never]:
190    def getRight(self, alt: R|_NoValue=noValue) -> R|Never:
191        """Get value of `XOR` if a Right, potential right value if a left.
192
193        * if XOR is a right, return its value
194          * otherwise return a provided alternate value of type ~R
195        * if XOR is a left, return the potential right value
196          * raises `ValueError` if a potential right value was not provided
197
198        """
199        if self:
200            if alt is noValue:
201                if self._right is noValue:
202                    msg = 'A potential right was needed by get, but none was provided.'
203                    raise ValueError(msg)
204                else:
205                    return cast(R, self._right)
206            else:
207                return cast(R, alt)
208        else:
209            return cast(R, self._right)

Get value of XOR if a Right, potential right value if a left.

  • if XOR is a right, return its value
    • otherwise return a provided alternate value of type ~R
  • if XOR is a left, return the potential right value
    • raises ValueError if a potential right value was not provided
def makeRight( self, right: Union[~R, grscheller.fp.nothingness._NoValue] = noValue) -> XOR[~L, ~R]:
211    def makeRight(self, right: R|_NoValue=noValue) -> XOR[L, R]:
212        """Make right
213
214        Return a new instance transformed into a right `XOR`. Change the right
215        value to `right` if given.
216        """
217        if right is noValue:
218            right = self.getRight()
219        return cast(XOR[L, R], XOR(right=right))

Make right

Return a new instance transformed into a right XOR. Change the right value to right if given.

def swapRight(self, right: ~R) -> XOR[~L, ~R]:
221    def swapRight(self, right: R) -> XOR[L, R]:
222        """Swap in a new right value, returns a new instance with a new right
223        (or potential right) value.
224        """
225        if self._left is noValue:
226            return cast(XOR[L, R], XOR(right=right))
227        else:
228            return XOR(self.get(), right)

Swap in a new right value, returns a new instance with a new right (or potential right) value.

def map(self, f: Callable[[~L], ~U]) -> XOR[~U, ~R]:
230    def map(self, f: Callable[[L], U]) -> XOR[U, R]:
231        """Map over if a left value.
232
233        * if `XOR` is a "left" then map `f` over its value
234          * if `f` successful return a left XOR[S, R]
235          * if `f` unsuccessful return right `XOR`
236            * swallows any exceptions `f` may throw
237        * if `XOR` is a "right"
238          * return new `XOR(right=self._right): XOR[S, R]`
239          * use method mapRight to adjust the returned value
240
241        """
242        if self._left is noValue:
243            return cast(XOR[U, R], XOR(right=self._right))
244
245        try:
246            applied = f(cast(L, self._left))
247        except Exception:
248            return XOR(right=self._right)
249        else:
250            return XOR(applied, self._right)

Map over if a left value.

  • if XOR is a "left" then map f over its value
    • if f successful return a left XOR[S, R]
    • if f unsuccessful return right XOR
      • swallows any exceptions f may throw
  • if XOR is a "right"
    • return new XOR(right=self._right): XOR[S, R]
    • use method mapRight to adjust the returned value
def mapRight(self, g: Callable[[~R], ~R]) -> XOR[~L, ~R]:
252    def mapRight(self, g: Callable[[R], R]) -> XOR[L, R]:
253        """Map over a right or potential right value."""
254        return XOR(self._left, g(cast(R, self._right)))

Map over a right or potential right value.

def flatMap( self, f: Callable[[~L], XOR[~U, ~R]]) -> XOR[~U, ~R]:
256    def flatMap(self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
257        """Flatmap - Monadically bind
258
259        * map over then flatten left values
260        * propagate right values
261
262        """
263        if self._left is noValue:
264            return XOR(noValue, self._right)
265        else:
266            return f(cast(L, self._left))

Flatmap - Monadically bind

  • map over then flatten left values
  • propagate right values
def mb_to_xor( m: MB[~D], right: ~R) -> XOR[~D, ~R]:
270def mb_to_xor(m: MB[D], right: R) -> XOR[D, R]:
271    """Convert a MB to an XOR."""
272    if m:
273        return XOR(m.get(), right)
274    else:
275        return XOR(noValue, right)

Convert a MB to an XOR.

def xor_to_mb( e: XOR[~D, ~U]) -> MB[~D]:
277def xor_to_mb(e: XOR[D,U]) -> MB[D]:
278    """Convert an XOR to a MB."""
279    if e:
280        return MB(e.get())
281    else:
282        return MB()

Convert an XOR to a MB.