morphir-python 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. morphir/__init__.py +14 -0
  2. morphir/ir/__init__.py +110 -0
  3. morphir/ir/access_controlled.py +15 -0
  4. morphir/ir/decorations.py +19 -0
  5. morphir/ir/diagnostics.py +105 -0
  6. morphir/ir/distribution.py +21 -0
  7. morphir/ir/document.py +37 -0
  8. morphir/ir/documented.py +11 -0
  9. morphir/ir/format_version.py +483 -0
  10. morphir/ir/fqname.py +9 -0
  11. morphir/ir/json.py +11 -0
  12. morphir/ir/literal.py +25 -0
  13. morphir/ir/meta.py +11 -0
  14. morphir/ir/migrate.py +1267 -0
  15. morphir/ir/module.py +17 -0
  16. morphir/ir/name.py +27 -0
  17. morphir/ir/package.py +15 -0
  18. morphir/ir/path.py +25 -0
  19. morphir/ir/qname.py +9 -0
  20. morphir/ir/ref.py +17 -0
  21. morphir/ir/type.py +37 -0
  22. morphir/ir/type_constraints.py +33 -0
  23. morphir/ir/type_def.py +19 -0
  24. morphir/ir/type_spec.py +25 -0
  25. morphir/ir/v3/__init__.py +64 -0
  26. morphir/ir/v3/_legacy_value.py +201 -0
  27. morphir/ir/v3/access_controlled.py +31 -0
  28. morphir/ir/v3/codec.py +1275 -0
  29. morphir/ir/v3/codec_v1.py +491 -0
  30. morphir/ir/v3/codec_v2.py +187 -0
  31. morphir/ir/v3/distribution.py +42 -0
  32. morphir/ir/v3/documented.py +13 -0
  33. morphir/ir/v3/fqname.py +30 -0
  34. morphir/ir/v3/literal.py +46 -0
  35. morphir/ir/v3/module.py +32 -0
  36. morphir/ir/v3/name.py +42 -0
  37. morphir/ir/v3/package.py +23 -0
  38. morphir/ir/v3/path.py +29 -0
  39. morphir/ir/v3/qname.py +16 -0
  40. morphir/ir/v3/type.py +126 -0
  41. morphir/ir/v3/value.py +235 -0
  42. morphir/ir/v4/__init__.py +160 -0
  43. morphir/ir/v4/access_controlled.py +17 -0
  44. morphir/ir/v4/annotation.py +75 -0
  45. morphir/ir/v4/codec.py +2161 -0
  46. morphir/ir/v4/decorations.py +88 -0
  47. morphir/ir/v4/distribution.py +56 -0
  48. morphir/ir/v4/document.py +69 -0
  49. morphir/ir/v4/documented.py +10 -0
  50. morphir/ir/v4/fqname.py +69 -0
  51. morphir/ir/v4/incompleteness.py +78 -0
  52. morphir/ir/v4/json.py +103 -0
  53. morphir/ir/v4/literal.py +51 -0
  54. morphir/ir/v4/meta.py +27 -0
  55. morphir/ir/v4/module.py +30 -0
  56. morphir/ir/v4/name.py +69 -0
  57. morphir/ir/v4/package.py +19 -0
  58. morphir/ir/v4/path.py +50 -0
  59. morphir/ir/v4/qname.py +34 -0
  60. morphir/ir/v4/ref.py +35 -0
  61. morphir/ir/v4/type.py +140 -0
  62. morphir/ir/v4/type_constraints.py +91 -0
  63. morphir/ir/v4/type_def.py +38 -0
  64. morphir/ir/v4/type_spec.py +64 -0
  65. morphir/ir/v4/value.py +480 -0
  66. morphir/ir/value.py +81 -0
  67. morphir/py.typed +0 -0
  68. morphir/sdk/__init__.py +110 -0
  69. morphir/sdk/_compare.py +187 -0
  70. morphir/sdk/aggregate.py +421 -0
  71. morphir/sdk/basics.py +504 -0
  72. morphir/sdk/char.py +110 -0
  73. morphir/sdk/decimal.py +311 -0
  74. morphir/sdk/dict.py +336 -0
  75. morphir/sdk/instant.py +118 -0
  76. morphir/sdk/int.py +86 -0
  77. morphir/sdk/key.py +522 -0
  78. morphir/sdk/list.py +356 -0
  79. morphir/sdk/local_date.py +436 -0
  80. morphir/sdk/local_time.py +267 -0
  81. morphir/sdk/maybe.py +135 -0
  82. morphir/sdk/number.py +237 -0
  83. morphir/sdk/regex.py +287 -0
  84. morphir/sdk/result.py +162 -0
  85. morphir/sdk/result_list.py +169 -0
  86. morphir/sdk/rule.py +103 -0
  87. morphir/sdk/set.py +214 -0
  88. morphir/sdk/stateful_app.py +49 -0
  89. morphir/sdk/string.py +427 -0
  90. morphir/sdk/tuple.py +43 -0
  91. morphir/sdk/uuid.py +209 -0
  92. morphir_python-0.0.1.dist-info/METADATA +269 -0
  93. morphir_python-0.0.1.dist-info/RECORD +99 -0
  94. morphir_python-0.0.1.dist-info/WHEEL +4 -0
  95. morphir_python-0.0.1.dist-info/entry_points.txt +2 -0
  96. morphir_python-0.0.1.dist-info/licenses/LICENSE +201 -0
  97. morphir_tools/__init__.py +13 -0
  98. morphir_tools/cli.py +223 -0
  99. morphir_tools/py.typed +0 -0
morphir/sdk/decimal.py ADDED
@@ -0,0 +1,311 @@
1
+ """Morphir.SDK.Decimal: arbitrary-precision decimal numbers.
2
+
3
+ A `Decimal` is the standard library `decimal.Decimal`. All arithmetic goes
4
+ through a context that is local to this module, with 50 significant digits and
5
+ half-up rounding. The module never reads or changes the global decimal context,
6
+ so other users of the `decimal` module see no effect, and a change to the global
7
+ context has no effect on the results here.
8
+
9
+ `Decimal` values compare by numeric value: `1.50 == 1.5`, and both have the same
10
+ hash.
11
+
12
+ Some names in this module shadow Python built-ins (`abs`, `round`). Import the
13
+ module with an alias, not the names:
14
+
15
+ >>> from morphir.sdk import decimal as Decimal
16
+ >>> Decimal.to_string(Decimal.add(Decimal.from_float(0.1), Decimal.bps(5)))
17
+ '0.1005'
18
+
19
+ Departures from the Elm runtime:
20
+
21
+ * `from_float` raises `ValueError` for `NaN` and the infinities, which a
22
+ `Decimal` in this module cannot hold.
23
+ * `shift_decimal_left` moves the exponent and uses no float. The Elm runtime
24
+ multiplies by the float `10 ^ -n`.
25
+ """
26
+
27
+ import decimal as _decimal
28
+ import math
29
+ import re
30
+
31
+ from morphir.sdk import maybe
32
+ from morphir.sdk._compare import Order
33
+ from morphir.sdk.maybe import Just, Maybe, Nothing
34
+
35
+ __all__ = [
36
+ "Decimal",
37
+ "abs",
38
+ "add",
39
+ "bps",
40
+ "compare",
41
+ "div",
42
+ "div_with_default",
43
+ "eq",
44
+ "from_float",
45
+ "from_int",
46
+ "from_string",
47
+ "gt",
48
+ "gte",
49
+ "hundred",
50
+ "hundredth",
51
+ "lt",
52
+ "lte",
53
+ "million",
54
+ "millionth",
55
+ "minus_one",
56
+ "mul",
57
+ "negate",
58
+ "neq",
59
+ "one",
60
+ "round",
61
+ "shift_decimal_left",
62
+ "shift_decimal_right",
63
+ "sub",
64
+ "tenth",
65
+ "thousand",
66
+ "thousandth",
67
+ "to_float",
68
+ "to_string",
69
+ "truncate",
70
+ "zero",
71
+ ]
72
+
73
+ Decimal = _decimal.Decimal
74
+ """The standard library decimal type."""
75
+
76
+ PRECISION = 50
77
+ """The number of significant digits that arithmetic results keep."""
78
+
79
+ # The exponent limits are as wide as the library allows, so that overflow is not
80
+ # a practical concern.
81
+ _CONTEXT = _decimal.Context(
82
+ prec=PRECISION,
83
+ rounding=_decimal.ROUND_HALF_UP,
84
+ Emin=_decimal.MIN_EMIN,
85
+ Emax=_decimal.MAX_EMAX,
86
+ )
87
+
88
+ # Elm's grammar: [<sign>]<digits>[.<digits>][e[<sign>]<digits>]. The standard
89
+ # library also accepts "Infinity", "NaN", underscores, white space and digits
90
+ # that are not ASCII; Elm parses none of these.
91
+ _ELM_DECIMAL = re.compile(r"[+-]?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?")
92
+
93
+
94
+ # Constants
95
+
96
+ zero: Decimal = Decimal(0)
97
+ """The number 0."""
98
+
99
+ one: Decimal = Decimal(1)
100
+ """The number 1."""
101
+
102
+ minus_one: Decimal = Decimal(-1)
103
+ """The number -1."""
104
+
105
+
106
+ # Convert from
107
+
108
+
109
+ def from_int(n: int) -> Decimal:
110
+ """Convert an integer to a `Decimal`. The conversion is exact."""
111
+ return Decimal(n)
112
+
113
+
114
+ def from_float(f: float) -> Decimal:
115
+ """Convert a float to a `Decimal`.
116
+
117
+ The result has the shortest digits that identify the float, so
118
+ `from_float(0.1)` is exactly 0.1. Negative zero becomes zero.
119
+
120
+ Raises:
121
+ ValueError: If the float is `NaN` or an infinity.
122
+ """
123
+ if not math.isfinite(f):
124
+ raise ValueError(f"from_float: {f!r} is not a finite number")
125
+ return zero if f == 0 else Decimal(repr(f))
126
+
127
+
128
+ def from_string(s: str) -> Maybe[Decimal]:
129
+ """Parse a `Decimal`.
130
+
131
+ The accepted form is an optional sign, digits, an optional fraction and an
132
+ optional exponent: `[+-]?digits(.digits)?([eE][+-]?digits)?`. Anything else
133
+ yields `Nothing`. The conversion is exact; it keeps all the digits.
134
+ """
135
+ if not _ELM_DECIMAL.fullmatch(s):
136
+ return Nothing()
137
+ try:
138
+ parsed = Decimal(s)
139
+ except _decimal.InvalidOperation:
140
+ # The exponent is outside the range that the library can hold.
141
+ return Nothing()
142
+ return Just(parsed) if parsed.is_finite() else Nothing()
143
+
144
+
145
+ # Convert from a known exponent
146
+
147
+
148
+ def hundred(n: int) -> Decimal:
149
+ """Return `n` hundreds."""
150
+ return Decimal(n).scaleb(2, _CONTEXT)
151
+
152
+
153
+ def thousand(n: int) -> Decimal:
154
+ """Return `n` thousands."""
155
+ return Decimal(n).scaleb(3, _CONTEXT)
156
+
157
+
158
+ def million(n: int) -> Decimal:
159
+ """Return `n` millions."""
160
+ return Decimal(n).scaleb(6, _CONTEXT)
161
+
162
+
163
+ def tenth(n: int) -> Decimal:
164
+ """Return `n` tenths, with no float arithmetic."""
165
+ return Decimal(n).scaleb(-1, _CONTEXT)
166
+
167
+
168
+ def hundredth(n: int) -> Decimal:
169
+ """Return `n` hundredths, with no float arithmetic."""
170
+ return Decimal(n).scaleb(-2, _CONTEXT)
171
+
172
+
173
+ def thousandth(n: int) -> Decimal:
174
+ """Return `n` thousandths, with no float arithmetic."""
175
+ return Decimal(n).scaleb(-3, _CONTEXT)
176
+
177
+
178
+ def bps(n: int) -> Decimal:
179
+ """Return `n` basis points (ten-thousandths), with no float arithmetic."""
180
+ return Decimal(n).scaleb(-4, _CONTEXT)
181
+
182
+
183
+ def millionth(n: int) -> Decimal:
184
+ """Return `n` millionths, with no float arithmetic."""
185
+ return Decimal(n).scaleb(-6, _CONTEXT)
186
+
187
+
188
+ # Convert to
189
+
190
+
191
+ def to_string(d: Decimal) -> str:
192
+ """Convert a `Decimal` to a string.
193
+
194
+ The result is always in positional notation, never in exponent notation,
195
+ and has no trailing zeros in the fraction.
196
+ """
197
+ text = format(d, "f")
198
+ if "." in text:
199
+ text = text.rstrip("0").rstrip(".")
200
+ return "0" if text in ("-0", "") else text
201
+
202
+
203
+ def to_float(d: Decimal) -> float:
204
+ """Convert a `Decimal` to the nearest float. This can lose precision."""
205
+ return float(d)
206
+
207
+
208
+ # Arithmetic
209
+
210
+
211
+ def add(a: Decimal, b: Decimal) -> Decimal:
212
+ """Add two decimals."""
213
+ return _CONTEXT.add(a, b)
214
+
215
+
216
+ def sub(a: Decimal, b: Decimal) -> Decimal:
217
+ """Subtract the second decimal from the first."""
218
+ return _CONTEXT.subtract(a, b)
219
+
220
+
221
+ def mul(a: Decimal, b: Decimal) -> Decimal:
222
+ """Multiply two decimals."""
223
+ return _CONTEXT.multiply(a, b)
224
+
225
+
226
+ def div(a: Decimal, b: Decimal) -> Maybe[Decimal]:
227
+ """Divide two decimals, or yield `Nothing` when the divisor is zero.
228
+
229
+ A quotient that is not exact has 50 significant digits.
230
+ """
231
+ return Nothing() if b.is_zero() else Just(_CONTEXT.divide(a, b))
232
+
233
+
234
+ def div_with_default(default: Decimal, a: Decimal, b: Decimal) -> Decimal:
235
+ """Divide two decimals, or yield the default when the divisor is zero."""
236
+ return maybe.with_default(default, div(a, b))
237
+
238
+
239
+ def shift_decimal_left(n: int, d: Decimal) -> Decimal:
240
+ """Move the decimal point `n` digits to the left, which divides by 10^n."""
241
+ return d.scaleb(-n, _CONTEXT)
242
+
243
+
244
+ def shift_decimal_right(n: int, d: Decimal) -> Decimal:
245
+ """Move the decimal point `n` digits to the right, which multiplies by 10^n."""
246
+ return d.scaleb(n, _CONTEXT)
247
+
248
+
249
+ def negate(d: Decimal) -> Decimal:
250
+ """Change the sign of a decimal. Zero stays zero."""
251
+ return zero if d.is_zero() else d.copy_negate()
252
+
253
+
254
+ def abs(d: Decimal) -> Decimal:
255
+ """Return the absolute value of a decimal."""
256
+ return d.copy_abs()
257
+
258
+
259
+ # Rounding
260
+
261
+
262
+ def truncate(d: Decimal) -> Decimal:
263
+ """Round toward zero to an integer."""
264
+ return d.to_integral_value(rounding=_decimal.ROUND_DOWN, context=_CONTEXT)
265
+
266
+
267
+ def round(d: Decimal) -> Decimal:
268
+ """Round to the nearest integer. A half goes away from zero."""
269
+ return d.to_integral_value(rounding=_decimal.ROUND_HALF_UP, context=_CONTEXT)
270
+
271
+
272
+ # Compare
273
+
274
+
275
+ def compare(a: Decimal, b: Decimal) -> Order:
276
+ """Compare two decimals by numeric value."""
277
+ if a < b:
278
+ return Order.LT
279
+ if a > b:
280
+ return Order.GT
281
+ return Order.EQ
282
+
283
+
284
+ def eq(a: Decimal, b: Decimal) -> bool:
285
+ """Check `a == b` by numeric value, so 1.50 equals 1.5."""
286
+ return a == b
287
+
288
+
289
+ def neq(a: Decimal, b: Decimal) -> bool:
290
+ """Check `a /= b` by numeric value."""
291
+ return a != b
292
+
293
+
294
+ def gt(a: Decimal, b: Decimal) -> bool:
295
+ """Check `a > b`."""
296
+ return a > b
297
+
298
+
299
+ def gte(a: Decimal, b: Decimal) -> bool:
300
+ """Check `a >= b`."""
301
+ return a >= b
302
+
303
+
304
+ def lt(a: Decimal, b: Decimal) -> bool:
305
+ """Check `a < b`."""
306
+ return a < b
307
+
308
+
309
+ def lte(a: Decimal, b: Decimal) -> bool:
310
+ """Check `a <= b`."""
311
+ return a <= b
morphir/sdk/dict.py ADDED
@@ -0,0 +1,336 @@
1
+ """Morphir.SDK.Dict: a mapping from comparable keys to values.
2
+
3
+ This module follows elm/core 1.0.5. A `Dict[K, V]` is a frozen dataclass that
4
+ holds its entries as a tuple of `(key, value)` pairs, sorted in ascending order
5
+ by `basics.compare` on the key, with no key twice. Thus:
6
+
7
+ * `to_list`, `keys` and `values` yield the entries in key order, as in Elm.
8
+ * Two dicts with the same entries are equal (`==`) and have the same hash, no
9
+ matter in which order the entries were inserted.
10
+ * Keys can be any comparable value: numbers, strings, and tuples or lists of
11
+ comparables. A key that is not comparable raises `TypeError`.
12
+
13
+ Build a `Dict` with `empty`, `singleton`, `from_list` and `insert`. Do not pass
14
+ entries to the `Dict` constructor, because it does not sort them.
15
+
16
+ All functions take their arguments in Elm order, with the dict last, and yield
17
+ a new `Dict`; nothing is mutated. Lookup is a binary search, O(log n). `insert`
18
+ and `remove` copy the entries, O(n).
19
+
20
+ Some names in this module shadow Python built-ins (`filter`, `map`). Import the
21
+ module with an alias, not the names:
22
+
23
+ >>> from morphir.sdk import dict as Dict
24
+ >>> Dict.get("a", Dict.from_list((("b", 2), ("a", 1))))
25
+ Just(value=1)
26
+ """
27
+
28
+ from dataclasses import dataclass
29
+ from typing import TYPE_CHECKING
30
+
31
+ from morphir.sdk import _compare
32
+ from morphir.sdk._compare import Comparable, Order
33
+ from morphir.sdk.maybe import Just, Maybe, Nothing
34
+
35
+ if TYPE_CHECKING:
36
+ from collections.abc import Callable, Iterator, Sequence
37
+
38
+ __all__ = [
39
+ "Dict",
40
+ "diff",
41
+ "empty",
42
+ "filter",
43
+ "foldl",
44
+ "foldr",
45
+ "from_list",
46
+ "get",
47
+ "insert",
48
+ "intersect",
49
+ "is_empty",
50
+ "keys",
51
+ "map",
52
+ "member",
53
+ "merge",
54
+ "partition",
55
+ "remove",
56
+ "singleton",
57
+ "size",
58
+ "to_list",
59
+ "union",
60
+ "update",
61
+ "values",
62
+ ]
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class Dict[K: Comparable, V]:
67
+ """An immutable dictionary with its entries sorted by key.
68
+
69
+ Use `from_list`, `singleton` or `insert` to build one. The constructor
70
+ accepts only entries that are already sorted by key with no key twice, and
71
+ stores every list inside a key as a tuple.
72
+
73
+ Raises:
74
+ ValueError: If the entries are not sorted by key, or a key repeats.
75
+ TypeError: If two keys are not comparable with each other.
76
+ """
77
+
78
+ entries: tuple[tuple[K, V], ...] = ()
79
+
80
+ def __post_init__(self) -> None:
81
+ entries = tuple((_compare.freeze(key), value) for key, value in self.entries)
82
+ _compare.check_strictly_ascending([key for key, _ in entries], "Dict")
83
+ object.__setattr__(self, "entries", entries)
84
+
85
+
86
+ def _key_of[K: Comparable, V](entry: tuple[K, V]) -> K:
87
+ return entry[0]
88
+
89
+
90
+ def _search[K: Comparable, V](key: K, dict_: Dict[K, V]) -> tuple[bool, int]:
91
+ return _compare.search(key, dict_.entries, _key_of)
92
+
93
+
94
+ # Build
95
+
96
+
97
+ def empty[K: Comparable, V]() -> Dict[K, V]: # pyright: ignore[reportInvalidTypeVarUse]
98
+ """Create an empty dictionary."""
99
+ return Dict()
100
+
101
+
102
+ def singleton[K: Comparable, V](key: K, value: V) -> Dict[K, V]:
103
+ """Create a dictionary with one entry."""
104
+ return Dict(((key, value),))
105
+
106
+
107
+ def insert[K: Comparable, V](key: K, value: V, dict_: Dict[K, V]) -> Dict[K, V]:
108
+ """Insert an entry. A value that is already there for the key is replaced.
109
+
110
+ Raises:
111
+ TypeError: If the key is not comparable with the keys in the dictionary.
112
+ """
113
+ found, index = _search(key, dict_)
114
+ entries = dict_.entries
115
+ after = entries[index + 1 :] if found else entries[index:]
116
+ return Dict((*entries[:index], (key, value), *after))
117
+
118
+
119
+ def update[K: Comparable, V](
120
+ key: K, alter: Callable[[Maybe[V]], Maybe[V]], dict_: Dict[K, V]
121
+ ) -> Dict[K, V]:
122
+ """Change the value for a key.
123
+
124
+ The function gets `Just` the current value, or `Nothing` when the key is not
125
+ there. When it yields `Nothing`, the key is removed.
126
+ """
127
+ match alter(get(key, dict_)):
128
+ case Just(value):
129
+ return insert(key, value, dict_)
130
+ case Nothing():
131
+ return remove(key, dict_)
132
+
133
+
134
+ def remove[K: Comparable, V](key: K, dict_: Dict[K, V]) -> Dict[K, V]:
135
+ """Remove the entry for a key. A key that is not there changes nothing."""
136
+ found, index = _search(key, dict_)
137
+ if not found:
138
+ return dict_
139
+ return Dict((*dict_.entries[:index], *dict_.entries[index + 1 :]))
140
+
141
+
142
+ # Query
143
+
144
+
145
+ def is_empty[K: Comparable, V](dict_: Dict[K, V]) -> bool:
146
+ """Check that a dictionary is empty."""
147
+ return not dict_.entries
148
+
149
+
150
+ def member[K: Comparable, V](key: K, dict_: Dict[K, V]) -> bool:
151
+ """Check that a key is in a dictionary."""
152
+ return _search(key, dict_)[0]
153
+
154
+
155
+ def get[K: Comparable, V](key: K, dict_: Dict[K, V]) -> Maybe[V]:
156
+ """Return the value for a key, or `Nothing` when the key is not there."""
157
+ found, index = _search(key, dict_)
158
+ return Just(dict_.entries[index][1]) if found else Nothing()
159
+
160
+
161
+ def size[K: Comparable, V](dict_: Dict[K, V]) -> int:
162
+ """Return the number of entries in a dictionary."""
163
+ return len(dict_.entries)
164
+
165
+
166
+ # Lists
167
+
168
+
169
+ def keys[K: Comparable, V](dict_: Dict[K, V]) -> tuple[K, ...]:
170
+ """Return the keys, from lowest to highest."""
171
+ return tuple(key for key, _ in dict_.entries)
172
+
173
+
174
+ def values[K: Comparable, V](dict_: Dict[K, V]) -> tuple[V, ...]:
175
+ """Return the values, in the order of their keys."""
176
+ return tuple(value for _, value in dict_.entries)
177
+
178
+
179
+ def to_list[K: Comparable, V](dict_: Dict[K, V]) -> tuple[tuple[K, V], ...]:
180
+ """Return the `(key, value)` pairs, sorted by key."""
181
+ return dict_.entries
182
+
183
+
184
+ def from_list[K: Comparable, V](pairs: Sequence[tuple[K, V]]) -> Dict[K, V]:
185
+ """Create a dictionary from `(key, value)` pairs.
186
+
187
+ When a key occurs more than once, the last pair wins, as in Elm.
188
+
189
+ Raises:
190
+ TypeError: If the keys are not comparable with each other.
191
+ """
192
+ key_of = _compare.sort_key(_compare.compare)
193
+ entries: list[tuple[K, V]] = []
194
+ # The sort is stable, so the last pair for a key is the last one in its run.
195
+ for pair in sorted(pairs, key=lambda pair: key_of(pair[0])):
196
+ if entries and _compare.compare(entries[-1][0], pair[0]) is Order.EQ:
197
+ entries[-1] = pair
198
+ else:
199
+ entries.append(pair)
200
+ return Dict(tuple(entries))
201
+
202
+
203
+ # Transform
204
+
205
+
206
+ def map[K: Comparable, A, B](f: Callable[[K, A], B], dict_: Dict[K, A]) -> Dict[K, B]:
207
+ """Apply a function to every value. The function gets the key first."""
208
+ return Dict(tuple((key, f(key, value)) for key, value in dict_.entries))
209
+
210
+
211
+ def foldl[K: Comparable, V, R](
212
+ f: Callable[[K, V, R], R], initial: R, dict_: Dict[K, V]
213
+ ) -> R:
214
+ """Reduce the entries from the lowest key to the highest."""
215
+ accumulator = initial
216
+ for key, value in dict_.entries:
217
+ accumulator = f(key, value, accumulator)
218
+ return accumulator
219
+
220
+
221
+ def foldr[K: Comparable, V, R](
222
+ f: Callable[[K, V, R], R], initial: R, dict_: Dict[K, V]
223
+ ) -> R:
224
+ """Reduce the entries from the highest key to the lowest."""
225
+ accumulator = initial
226
+ for key, value in reversed(dict_.entries):
227
+ accumulator = f(key, value, accumulator)
228
+ return accumulator
229
+
230
+
231
+ def filter[K: Comparable, V](
232
+ predicate: Callable[[K, V], bool], dict_: Dict[K, V]
233
+ ) -> Dict[K, V]:
234
+ """Keep the entries that pass the test."""
235
+ return Dict(tuple(entry for entry in dict_.entries if predicate(*entry)))
236
+
237
+
238
+ def partition[K: Comparable, V](
239
+ predicate: Callable[[K, V], bool], dict_: Dict[K, V]
240
+ ) -> tuple[Dict[K, V], Dict[K, V]]:
241
+ """Split a dictionary into the entries that pass the test and those that fail."""
242
+ passed: list[tuple[K, V]] = []
243
+ failed: list[tuple[K, V]] = []
244
+ for entry in dict_.entries:
245
+ (passed if predicate(*entry) else failed).append(entry)
246
+ return (Dict(tuple(passed)), Dict(tuple(failed)))
247
+
248
+
249
+ # Combine
250
+
251
+
252
+ def _walk[K: Comparable, A, B](
253
+ left: Dict[K, A], right: Dict[K, B]
254
+ ) -> Iterator[tuple[K, Maybe[A], Maybe[B]]]:
255
+ """Walk two dictionaries in one pass, in ascending key order.
256
+
257
+ Each step yields the key with `Just` the value from each side that has it.
258
+ """
259
+ lefts = left.entries
260
+ rights = right.entries
261
+ i = 0
262
+ j = 0
263
+ while i < len(lefts) and j < len(rights):
264
+ order = _compare.compare(lefts[i][0], rights[j][0])
265
+ if order is Order.LT:
266
+ yield (lefts[i][0], Just(lefts[i][1]), Nothing())
267
+ i += 1
268
+ elif order is Order.GT:
269
+ yield (rights[j][0], Nothing(), Just(rights[j][1]))
270
+ j += 1
271
+ else:
272
+ yield (lefts[i][0], Just(lefts[i][1]), Just(rights[j][1]))
273
+ i += 1
274
+ j += 1
275
+ for key, a in lefts[i:]:
276
+ yield (key, Just(a), Nothing())
277
+ for key, b in rights[j:]:
278
+ yield (key, Nothing(), Just(b))
279
+
280
+
281
+ def union[K: Comparable, V](left: Dict[K, V], right: Dict[K, V]) -> Dict[K, V]:
282
+ """Combine two dictionaries. For a key in both, the first dictionary wins."""
283
+ entries: list[tuple[K, V]] = []
284
+ for key, a, b in _walk(left, right):
285
+ if isinstance(a, Just):
286
+ entries.append((key, a.value))
287
+ elif isinstance(b, Just):
288
+ entries.append((key, b.value))
289
+ return Dict(tuple(entries))
290
+
291
+
292
+ def intersect[K: Comparable, V, W](left: Dict[K, V], right: Dict[K, W]) -> Dict[K, V]:
293
+ """Keep the entries of the first dictionary whose keys are in the second."""
294
+ return Dict(
295
+ tuple(
296
+ (key, a.value)
297
+ for key, a, b in _walk(left, right)
298
+ if isinstance(a, Just) and isinstance(b, Just)
299
+ )
300
+ )
301
+
302
+
303
+ def diff[K: Comparable, V, W](left: Dict[K, V], right: Dict[K, W]) -> Dict[K, V]:
304
+ """Keep the entries of the first dictionary whose keys are not in the second."""
305
+ return Dict(
306
+ tuple(
307
+ (key, a.value)
308
+ for key, a, b in _walk(left, right)
309
+ if isinstance(a, Just) and isinstance(b, Nothing)
310
+ )
311
+ )
312
+
313
+
314
+ def merge[K: Comparable, A, B, R](
315
+ left_step: Callable[[K, A, R], R],
316
+ both_step: Callable[[K, A, B, R], R],
317
+ right_step: Callable[[K, B, R], R],
318
+ left: Dict[K, A],
319
+ right: Dict[K, B],
320
+ initial: R,
321
+ ) -> R:
322
+ """Combine two dictionaries in the most general way.
323
+
324
+ The keys are visited from lowest to highest. `left_step` gets the keys that
325
+ are only in the left dictionary, `both_step` the keys in both, and
326
+ `right_step` the keys that are only in the right dictionary.
327
+ """
328
+ accumulator = initial
329
+ for key, a, b in _walk(left, right):
330
+ if isinstance(a, Just) and isinstance(b, Just):
331
+ accumulator = both_step(key, a.value, b.value, accumulator)
332
+ elif isinstance(a, Just):
333
+ accumulator = left_step(key, a.value, accumulator)
334
+ elif isinstance(b, Just):
335
+ accumulator = right_step(key, b.value, accumulator)
336
+ return accumulator