grscheller.fp.err_handling

Maybe and Either Monads.

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

Map MB with function f and flatten.

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

Convert a MB to an XOR.

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

Convert an XOR to a MB.