jjinx 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.
@@ -0,0 +1,334 @@
1
+ """Methods implementing J adverbs."""
2
+
3
+ import dataclasses
4
+ import functools
5
+ from typing import Callable
6
+
7
+ import numpy as np
8
+ from jinx.errors import DomainError, JinxNotImplementedError, LengthError, ValenceError
9
+ from jinx.execution.numpy.application import _apply_dyad, _apply_monad
10
+ from jinx.execution.numpy.helpers import (
11
+ get_fill_value,
12
+ is_box,
13
+ is_ufunc,
14
+ maybe_pad_with_fill_value,
15
+ maybe_parenthesise_verb_spelling,
16
+ )
17
+ from jinx.vocabulary import Dyad, Monad, Verb
18
+
19
+ INFINITY = float("inf")
20
+
21
+
22
+ def slash_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
23
+ if verb.dyad is None or verb.dyad.function is None:
24
+ # Note: this differs from J which still allows the adverb to be applied
25
+ # to a verb, but may raise an error when the new verb is applied to a noun
26
+ # and the verb has no dyadic valence.
27
+ raise ValenceError(f"Verb {verb.spelling} has no dyadic valence.")
28
+
29
+ if is_ufunc(verb.dyad.function) and verb.dyad.is_commutative:
30
+ f: np.ufunc = verb.dyad.function # type: ignore[assignment]
31
+ monad = f.reduce
32
+ dyad = f.outer
33
+
34
+ else:
35
+ # Slow path: dyad is not a ufunc.
36
+ # The function is either callable, in which cases it is applied directly,
37
+ # or a Verb object that needs to be applied indirectly with _apply_dyad().
38
+ if isinstance(verb.dyad.function, Verb):
39
+ func = functools.partial(_apply_dyad, verb) # type: ignore[assignment]
40
+ else:
41
+ func = verb.dyad.function # type: ignore[assignment]
42
+
43
+ def _dyad_arg_swap(x: np.ndarray, y: np.ndarray) -> np.ndarray:
44
+ return func(y, x)
45
+
46
+ def _reduce(y: np.ndarray) -> np.ndarray:
47
+ y = np.atleast_1d(y)
48
+ y = np.flip(y, axis=0)
49
+ return functools.reduce(_dyad_arg_swap, y)
50
+
51
+ def _outer(x: np.ndarray, y: np.ndarray) -> np.ndarray:
52
+ # We have already checked that verb.dyad is not None, so this is safe.
53
+ verb_slash = _modify_rank(verb, np.array([verb.dyad.left_rank, INFINITY])) # type: ignore[union-attr]
54
+ return _apply_dyad(verb_slash, x, y)
55
+
56
+ monad = _reduce
57
+ dyad = _outer
58
+
59
+ spelling = maybe_parenthesise_verb_spelling(verb.spelling)
60
+ spelling = f"{verb.spelling}/"
61
+
62
+ return Verb[np.ndarray](
63
+ name=spelling,
64
+ spelling=spelling,
65
+ monad=Monad(name=spelling, rank=INFINITY, function=monad),
66
+ dyad=Dyad(
67
+ name=spelling, left_rank=INFINITY, right_rank=INFINITY, function=dyad
68
+ ),
69
+ )
70
+
71
+
72
+ def bslash_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
73
+ # Common cases that have a straightforward optimisation.
74
+ SPECIAL_MONAD = {
75
+ "+/": np.add.accumulate,
76
+ "*/": np.multiply.accumulate,
77
+ "<./": np.minimum.accumulate,
78
+ ">./": np.maximum.accumulate,
79
+ }
80
+
81
+ if verb.spelling in SPECIAL_MONAD:
82
+ monad_ = SPECIAL_MONAD[verb.spelling]
83
+
84
+ else:
85
+
86
+ def monad_(y: np.ndarray) -> np.ndarray: # type: ignore[misc]
87
+ y = np.atleast_1d(y)
88
+ result = []
89
+ for i in range(1, len(y) + 1):
90
+ result.append(_apply_monad(verb, y[:i]))
91
+ result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
92
+ return np.asarray(result)
93
+
94
+ def dyad_(x: np.ndarray, y: np.ndarray) -> np.ndarray:
95
+ if not np.issubdtype(x.dtype, np.integer):
96
+ raise DomainError(f"x has nonintegral value ({x})")
97
+ y = np.atleast_1d(y)
98
+ if x == 0:
99
+ return np.zeros(len(y) + 1, dtype=np.int64)
100
+ if x == 1 or x == -1:
101
+ windows = y
102
+ elif x > 0:
103
+ # Overlapping windows
104
+ windows = np.array([y[i : i + x] for i in range(len(y) - x + 1)])
105
+ else:
106
+ # Non-overlapping windows
107
+ windows = np.array([y[i : i - x] for i in range(0, len(y), -x)])
108
+
109
+ result = []
110
+ for window in windows:
111
+ result.append(_apply_monad(verb, window))
112
+ result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
113
+ return np.asarray(result)
114
+
115
+ spelling = maybe_parenthesise_verb_spelling(verb.spelling)
116
+ spelling = f"{spelling}\\"
117
+
118
+ return Verb(
119
+ name=spelling,
120
+ spelling=spelling,
121
+ monad=Monad(name=spelling, rank=INFINITY, function=monad_),
122
+ dyad=Dyad(name=spelling, left_rank=0, right_rank=INFINITY, function=dyad_),
123
+ )
124
+
125
+
126
+ def bslashdot_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
127
+ SPECIAL_MONAD = {
128
+ "+/": lambda x: np.add.accumulate(x[::-1])[::-1],
129
+ "*/": lambda x: np.multiply.accumulate(x[::-1])[::-1],
130
+ "<./": lambda x: np.minimum.accumulate(x[::-1])[::-1],
131
+ ">./": lambda x: np.maximum.accumulate(x[::-1])[::-1],
132
+ }
133
+
134
+ if verb.spelling in SPECIAL_MONAD:
135
+ monad_ = SPECIAL_MONAD[verb.spelling]
136
+ else:
137
+
138
+ def monad_(y: np.ndarray) -> np.ndarray:
139
+ y = np.atleast_1d(y)
140
+ result = []
141
+ for i in range(len(y)):
142
+ result.append(_apply_monad(verb, y[i:]))
143
+ result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
144
+ return np.asarray(result)
145
+
146
+ def dyad_(x: np.ndarray, y: np.ndarray) -> np.ndarray:
147
+ if not np.issubdtype(x.dtype, np.integer):
148
+ raise DomainError(f"x has nonintegral value ({x})")
149
+ y = np.atleast_1d(y)
150
+ if x == 0:
151
+ return y
152
+ elif x > 0:
153
+ # Overlapping windows
154
+ windows = [
155
+ np.concatenate([y[:i], y[i + x :]], axis=0)
156
+ for i in range(len(y) - x + 1)
157
+ ]
158
+ else:
159
+ # Non-overlapping windows
160
+ windows = [
161
+ np.concatenate([y[:i], y[i - x :]], axis=0)
162
+ for i in range(0, len(y), -x)
163
+ ]
164
+
165
+ result = []
166
+ for window in windows:
167
+ result.append(_apply_monad(verb, window))
168
+ result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
169
+ return np.asarray(result)
170
+
171
+ spelling = maybe_parenthesise_verb_spelling(verb.spelling)
172
+ spelling = f"{verb.spelling}\\."
173
+
174
+ return Verb(
175
+ name=spelling,
176
+ spelling=spelling,
177
+ monad=Monad(name=spelling, rank=INFINITY, function=monad_),
178
+ dyad=Dyad(name=spelling, left_rank=0, right_rank=INFINITY, function=dyad_),
179
+ )
180
+
181
+
182
+ def tilde_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
183
+ if verb.dyad is None or verb.dyad.function is None:
184
+ # Note: this differs from J which still allows the adverb to be applied
185
+ # to a verb, but may raise an error when the new verb is applied to a noun
186
+ # and the verb has no dyadic valence.
187
+ raise ValenceError(f"Verb {verb.spelling} has no dyadic valence.")
188
+
189
+ def monad(y: np.ndarray) -> np.ndarray:
190
+ # replicate argument and apply verb dyadically
191
+ return _apply_dyad(verb, y, y)
192
+
193
+ def dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
194
+ # swap the arguments and apply verb dyadically
195
+ return _apply_dyad(verb, y, x)
196
+
197
+ spelling = maybe_parenthesise_verb_spelling(verb.spelling)
198
+ spelling = f"{verb.spelling}~"
199
+
200
+ return Verb[np.ndarray](
201
+ name=spelling,
202
+ spelling=spelling,
203
+ monad=Monad(name=spelling, rank=INFINITY, function=monad),
204
+ dyad=Dyad(
205
+ name=spelling,
206
+ left_rank=verb.dyad.right_rank,
207
+ right_rank=verb.dyad.left_rank,
208
+ function=dyad,
209
+ ),
210
+ )
211
+
212
+
213
+ def _modify_rank(verb: Verb, rank: np.ndarray | int | float) -> Verb:
214
+ rank = np.atleast_1d(rank)
215
+ if np.issubdtype(rank.dtype, np.floating):
216
+ if not np.isinf(rank).any():
217
+ raise DomainError(f"Rank must be an integer or infinity, got {rank.dtype}")
218
+
219
+ elif not np.issubdtype(rank.dtype, np.integer):
220
+ raise DomainError(f"Rank must be an integer or infinity, got {rank.dtype}")
221
+
222
+ if rank.size > 3 or rank.ndim > 1:
223
+ raise DomainError(
224
+ f"Rank must be a scalar or 1D array of length <= 3, got {rank.ndim}D array with shape {rank.shape}"
225
+ )
226
+
227
+ rank_list = [int(r) if not np.isinf(r) else INFINITY for r in rank.tolist()]
228
+ verb_spelling = spelling = maybe_parenthesise_verb_spelling(verb.spelling)
229
+
230
+ if len(rank_list) == 1:
231
+ monad_rank = left_rank = right_rank = rank_list[0]
232
+ spelling = f'{verb_spelling}"{rank_list[0]}'
233
+
234
+ elif len(rank_list) == 2:
235
+ left_rank, right_rank = rank_list
236
+ monad_rank = right_rank
237
+ spelling = f'{verb_spelling}"{left_rank} {right_rank}'
238
+
239
+ else:
240
+ monad_rank, left_rank, right_rank = rank_list
241
+ spelling = f'{verb_spelling}"{monad_rank} {left_rank} {right_rank}'
242
+
243
+ if verb.monad:
244
+ monad = dataclasses.replace(verb.monad, rank=monad_rank, function=verb)
245
+ else:
246
+ monad = None
247
+
248
+ if verb.dyad:
249
+ dyad = dataclasses.replace(
250
+ verb.dyad,
251
+ left_rank=left_rank,
252
+ right_rank=right_rank,
253
+ function=verb,
254
+ )
255
+ else:
256
+ dyad = None
257
+
258
+ return dataclasses.replace(
259
+ verb,
260
+ spelling=spelling,
261
+ name=spelling,
262
+ monad=monad,
263
+ dyad=dyad,
264
+ )
265
+
266
+
267
+ def slashdot_adverb(verb: Verb) -> Verb:
268
+ def monad(y: np.ndarray) -> np.ndarray:
269
+ y = np.atleast_1d(y)
270
+
271
+ if y.ndim == 1:
272
+ result = [_apply_monad(verb, item) for item in y]
273
+ elif y.ndim <= 3:
274
+ result = []
275
+ for offset in range(1 - y.shape[0], y.shape[1]):
276
+ item = np.diagonal(y[::-1], offset).T[::-1]
277
+ result.append(_apply_monad(verb, item))
278
+ else:
279
+ JinxNotImplementedError(
280
+ f"Monad {verb.spelling} dooes not yet support array rank > 3."
281
+ )
282
+
283
+ result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
284
+ return np.asarray(result)
285
+
286
+ def dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
287
+ x = np.atleast_1d(x)
288
+ y = np.atleast_1d(y)
289
+
290
+ if len(x) != len(y):
291
+ raise LengthError(
292
+ f"x and y must have the same length, got {len(x)} and {len(y)}"
293
+ )
294
+
295
+ item_indices: dict[bytes, list[int]] = {}
296
+
297
+ if is_box(x):
298
+ for i, x_item in enumerate(x):
299
+ item_indices.setdefault(x_item[0].tobytes(), []).append(i)
300
+
301
+ else:
302
+ for i, x_item in enumerate(x):
303
+ item_indices.setdefault(x_item.tobytes(), []).append(i)
304
+
305
+ result = []
306
+ for idx in item_indices.values():
307
+ result.append(_apply_monad(verb, y[idx]))
308
+
309
+ result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
310
+ return np.asarray(result)
311
+
312
+ spelling = maybe_parenthesise_verb_spelling(verb.spelling)
313
+ spelling = f"{verb.spelling}/."
314
+
315
+ return Verb(
316
+ name=spelling,
317
+ spelling=spelling,
318
+ monad=Monad(name=spelling, rank=INFINITY, function=monad),
319
+ dyad=Dyad(
320
+ name=spelling,
321
+ left_rank=INFINITY,
322
+ right_rank=INFINITY,
323
+ function=dyad,
324
+ ),
325
+ )
326
+
327
+
328
+ ADVERB_MAP: dict[str, Callable[[Verb[np.ndarray]], Verb[np.ndarray]]] = {
329
+ "SLASH": slash_adverb,
330
+ "SLASHDOT": slashdot_adverb,
331
+ "BSLASH": bslash_adverb,
332
+ "BSLASHDOT": bslashdot_adverb,
333
+ "TILDE": tilde_adverb,
334
+ }
@@ -0,0 +1,343 @@
1
+ """Methods for applying verb implementations to nouns and verbs.
2
+
3
+ Main references:
4
+ * https://code.jsoftware.com/wiki/Vocabulary/Agreement
5
+ * https://www.jsoftware.com/help/jforc/loopless_code_i_verbs_have_r.htm
6
+
7
+ """
8
+
9
+ import functools
10
+ from dataclasses import dataclass
11
+
12
+ import numpy as np
13
+ from jinx.errors import JinxNotImplementedError, LengthError, ValenceError
14
+ from jinx.execution.numpy.conversion import ndarray_or_scalar_to_noun
15
+ from jinx.execution.numpy.helpers import (
16
+ is_ufunc,
17
+ is_ufunc_based,
18
+ maybe_pad_with_fill_value,
19
+ )
20
+ from jinx.vocabulary import Adverb, Conjunction, Dyad, Monad, Noun, RankT, Verb
21
+
22
+
23
+ def get_rank(verb_rank: RankT, noun_rank: int) -> int:
24
+ """Get the rank at which to apply the verb to the noun.
25
+
26
+ If the verb rank is negative, it means that the verb rank is subtracted
27
+ from the noun rank, to a minimum of 0.
28
+ """
29
+ if verb_rank < 0:
30
+ return max(0, noun_rank + verb_rank) # type: ignore[return-value]
31
+ return min(verb_rank, noun_rank) # type: ignore[return-value]
32
+
33
+
34
+ def fill_and_assemble(
35
+ cells: list[np.ndarray], frame_shape: tuple[int, ...]
36
+ ) -> np.ndarray:
37
+ if not cells:
38
+ return np.asarray(cells).reshape(frame_shape)
39
+
40
+ cells = maybe_pad_with_fill_value(cells)
41
+ return np.asarray(cells).reshape(frame_shape + cells[0].shape)
42
+
43
+
44
+ @dataclass
45
+ class ArrayCells:
46
+ cell_shape: tuple[int, ...]
47
+ frame_shape: tuple[int, ...]
48
+ cells: np.ndarray
49
+
50
+
51
+ def split_into_cells(arr: np.ndarray, rank: int) -> ArrayCells:
52
+ """
53
+ Look at the array shape and rank to determine frame and cell shape.
54
+
55
+ The trailing `rank` axes define the cell shape and the preceding
56
+ axes define the frame shape. E.g. for rank=2:
57
+
58
+ arr.shape = (n0, n1, n2, n3, n4)
59
+ ---------- ------
60
+ ^ frame ^ cell
61
+
62
+ If rank=0, the frame shape is the same as the shape and the monad
63
+ applies to each atom of the array.
64
+ """
65
+ if arr.size == 0:
66
+ return ArrayCells(cell_shape=(), frame_shape=arr.shape, cells=arr)
67
+
68
+ if rank == 0:
69
+ return ArrayCells(cell_shape=(), frame_shape=arr.shape, cells=arr.ravel())
70
+
71
+ return ArrayCells(
72
+ cell_shape=arr.shape[-rank:],
73
+ frame_shape=arr.shape[:-rank],
74
+ cells=arr.reshape(-1, *arr.shape[-rank:]),
75
+ )
76
+
77
+
78
+ def apply_monad(verb: Verb[np.ndarray], noun: Noun[np.ndarray]) -> Noun[np.ndarray]:
79
+ result = _apply_monad(verb, noun.implementation)
80
+ return ndarray_or_scalar_to_noun(result)
81
+
82
+
83
+ def _apply_monad(verb: Verb[np.ndarray], arr: np.ndarray) -> np.ndarray:
84
+ if verb.monad is None or verb.monad.function is None:
85
+ raise ValenceError(f"Verb {verb.spelling} has no monadic valence.")
86
+ if verb.monad.function is NotImplemented:
87
+ raise JinxNotImplementedError(
88
+ f"Verb {verb.spelling} monad function is not yet implemented in Jinx."
89
+ )
90
+
91
+ if isinstance(verb.monad.function, Verb):
92
+ function = functools.partial(_apply_monad, verb.monad.function)
93
+ else:
94
+ function = verb.monad.function # type: ignore[assignment]
95
+
96
+ if arr.size == 0:
97
+ return function(arr)
98
+
99
+ rank = get_rank(verb.monad.rank, arr.ndim)
100
+
101
+ # If the verb rank is 0 it applies to each atom of the array.
102
+ # NumPy's unary ufuncs are typically designed to work this way
103
+ # Apply the function directly here as an optimisation.
104
+ if rank == 0 and (is_ufunc(function) or is_ufunc_based(function)):
105
+ return function(arr)
106
+
107
+ array_cells = split_into_cells(arr, rank)
108
+ cells = [function(cell) for cell in array_cells.cells]
109
+ return fill_and_assemble(cells, array_cells.frame_shape)
110
+
111
+
112
+ def apply_dyad(
113
+ verb: Verb[np.ndarray], noun_1: Noun[np.ndarray], noun_2: Noun[np.ndarray]
114
+ ) -> Noun[np.ndarray]:
115
+ result = _apply_dyad(verb, noun_1.implementation, noun_2.implementation)
116
+ return ndarray_or_scalar_to_noun(result)
117
+
118
+
119
+ def _apply_dyad(
120
+ verb: Verb[np.ndarray], left_arr: np.ndarray, right_arr: np.ndarray
121
+ ) -> np.ndarray:
122
+ if verb.dyad is None or verb.dyad.function is None:
123
+ raise ValenceError(f"Verb {verb.spelling} has no dyadic valence.")
124
+ if verb.dyad.function is NotImplemented:
125
+ raise JinxNotImplementedError(
126
+ f"Verb {verb.spelling} dyad function is not yet implemented."
127
+ )
128
+
129
+ if isinstance(verb.dyad.function, Verb):
130
+ function = functools.partial(_apply_dyad, verb.dyad.function)
131
+ else:
132
+ function = verb.dyad.function # type: ignore[assignment]
133
+
134
+ if left_arr.size == 0 or right_arr.size == 0:
135
+ return function(left_arr, right_arr)
136
+
137
+ left_rank = get_rank(verb.dyad.left_rank, left_arr.ndim)
138
+ right_rank = get_rank(verb.dyad.right_rank, right_arr.ndim)
139
+
140
+ # If the left and right ranks are both 0 and one of the arrays is a scalar,
141
+ # apply the dyad directly as an optimisation.
142
+ if (
143
+ left_rank == right_rank == 0
144
+ and (is_ufunc(function) or is_ufunc_based(function))
145
+ and (left_arr.ndim == 0 or right_arr.ndim == 0)
146
+ ):
147
+ return function(left_arr, right_arr)
148
+
149
+ left = split_into_cells(left_arr, left_rank)
150
+ right = split_into_cells(right_arr, right_rank)
151
+
152
+ # If the left and right frame shapes are the same, we can apply the dyad immediately.
153
+ if left.frame_shape == right.frame_shape:
154
+ cells = [
155
+ function(left_cell, right_cell)
156
+ for left_cell, right_cell in zip(left.cells, right.cells, strict=True)
157
+ ]
158
+ return fill_and_assemble(cells, left.frame_shape)
159
+
160
+ # Otherwise we need to find the common frame shape. One of the frame shapes must
161
+ # be a prefix of the other, otherwise it's not possible to apply the dyad.
162
+ common_frame_shape = find_common_frame_shape(left.frame_shape, right.frame_shape)
163
+ if common_frame_shape is None:
164
+ raise LengthError(
165
+ f"Cannot apply dyad {verb.spelling} to arrays of shape {left.frame_shape} and {right.frame_shape}"
166
+ )
167
+
168
+ rcf = len(common_frame_shape)
169
+
170
+ left_rcf_cell_shape = left_arr.shape[rcf:]
171
+ right_rcf_cell_shape = right_arr.shape[rcf:]
172
+
173
+ left_arr_reshaped = left_arr.reshape(-1, *left_rcf_cell_shape)
174
+ right_arr_reshaped = right_arr.reshape(-1, *right_rcf_cell_shape)
175
+
176
+ cells = []
177
+ for left_cell, right_cell in zip(
178
+ left_arr_reshaped, right_arr_reshaped, strict=True
179
+ ):
180
+ subcells = []
181
+ if common_frame_shape == left.frame_shape:
182
+ # right_cell is longer and contains multiple operand cells
183
+ if right_rank == 0:
184
+ right_subcells = right_cell.ravel()
185
+ else:
186
+ right_subcells = right_cell.reshape(-1, *right.cell_shape)
187
+
188
+ for right_subcell in right_subcells:
189
+ subcells.append(function(left_cell, right_subcell))
190
+ else:
191
+ # left_cell is longer and contains multiple operand cells
192
+ if left_rank == 0:
193
+ left_subcells = left_cell.ravel()
194
+ else:
195
+ left_subcells = left_cell.reshape(-1, *left.cell_shape)
196
+
197
+ for left_subcell in left_subcells:
198
+ subcells.append(function(left_subcell, right_cell))
199
+
200
+ subcells = maybe_pad_with_fill_value(subcells)
201
+ subarray = np.asarray(subcells)
202
+ if subarray.shape:
203
+ cells.extend(subcells)
204
+ else:
205
+ cells.append(subarray)
206
+
207
+ cells = maybe_pad_with_fill_value(cells)
208
+ array = np.asarray(cells)
209
+
210
+ # Gather the cells into the final frame shape (the longer of the left
211
+ # and right frame shapes, plus the result cell shape).
212
+ collecting_frame = max(left.frame_shape, right.frame_shape, key=len)
213
+ return array.reshape(collecting_frame + cells[0].shape)
214
+
215
+
216
+ def find_common_frame_shape(
217
+ left_frame_shape: tuple[int, ...], right_frame_shape: tuple[int, ...]
218
+ ) -> tuple[int, ...] | None:
219
+ if len(left_frame_shape) <= len(right_frame_shape):
220
+ shorter = left_frame_shape
221
+ longer = right_frame_shape
222
+ else:
223
+ shorter = right_frame_shape
224
+ longer = left_frame_shape
225
+
226
+ if all(a == b for a, b in zip(shorter, longer)):
227
+ return shorter
228
+
229
+ return None
230
+
231
+
232
+ def apply_conjunction(
233
+ verb_or_noun_1: Verb | Noun, conjunction: Conjunction, verb_or_noun_2: Verb | Noun
234
+ ) -> Verb | Noun:
235
+ return conjunction.function(verb_or_noun_1, verb_or_noun_2)
236
+
237
+
238
+ def apply_adverb(verb_or_noun: Verb | Noun, adverb: Adverb) -> Verb:
239
+ return adverb.function(verb_or_noun)
240
+
241
+
242
+ INFINITY = float("inf")
243
+
244
+
245
+ def build_hook(f: Verb[np.ndarray], g: Verb[np.ndarray]) -> Verb[np.ndarray]:
246
+ """Build a hook given verbs f and g.
247
+
248
+ (f g) y -> y f (g y)
249
+ x (f g) y -> x f (g y)
250
+
251
+ The new verb has infinite rank.
252
+ """
253
+
254
+ def _monad(y: np.ndarray) -> np.ndarray:
255
+ a = _apply_monad(g, y)
256
+ return _apply_dyad(f, y, a)
257
+
258
+ def _dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
259
+ a = _apply_monad(g, y)
260
+ return _apply_dyad(f, x, a)
261
+
262
+ f_spelling = f"({f.spelling})" if " " in f.spelling else f.spelling
263
+ g_spelling = f"({g.spelling})" if " " in g.spelling else g.spelling
264
+ spelling = f"{f_spelling} {g_spelling}"
265
+
266
+ return Verb[np.ndarray](
267
+ spelling=spelling,
268
+ name=spelling,
269
+ monad=Monad(
270
+ name=spelling,
271
+ rank=INFINITY,
272
+ function=_monad,
273
+ ),
274
+ dyad=Dyad(
275
+ name=spelling,
276
+ left_rank=INFINITY,
277
+ right_rank=INFINITY,
278
+ function=_dyad,
279
+ ),
280
+ )
281
+
282
+
283
+ def build_fork(
284
+ f: Verb[np.ndarray] | Noun[np.ndarray], g: Verb[np.ndarray], h: Verb[np.ndarray]
285
+ ) -> Verb[np.ndarray]:
286
+ """Build a fork given verbs f, g, h.
287
+
288
+ (f g h) y -> (f y) g (h y)
289
+ x (f g h) y -> (x f y) g (x h y)
290
+
291
+ The new verb has infinite rank.
292
+
293
+ Note that f can be a noun, in which case there is one fewer function calls.
294
+ """
295
+
296
+ def _monad(y: np.ndarray) -> np.ndarray:
297
+ if isinstance(f, Verb) and f.spelling == "[:":
298
+ hy = _apply_monad(h, y)
299
+ return _apply_monad(g, hy)
300
+
301
+ if isinstance(f, Verb):
302
+ a = _apply_monad(f, y)
303
+ else:
304
+ a = f.implementation
305
+ b = _apply_monad(h, y)
306
+ return _apply_dyad(g, a, b)
307
+
308
+ def _dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
309
+ if isinstance(f, Verb) and f.spelling == "[:":
310
+ hy = _apply_dyad(h, x, y)
311
+ return _apply_monad(g, hy)
312
+
313
+ if isinstance(f, Verb):
314
+ a = _apply_dyad(f, x, y)
315
+ else:
316
+ a = f.implementation
317
+ b = _apply_dyad(h, x, y)
318
+ return _apply_dyad(g, a, b)
319
+
320
+ if isinstance(f, Verb):
321
+ f_spelling = f"({f.spelling})" if " " in f.spelling else f.spelling
322
+ else:
323
+ f_spelling = str(f.implementation)
324
+
325
+ g_spelling = f"({g.spelling})" if " " in g.spelling else g.spelling
326
+ h_spelling = f"({h.spelling})" if " " in h.spelling else h.spelling
327
+ spelling = f"{f_spelling} {g_spelling} {h_spelling}"
328
+
329
+ return Verb[np.ndarray](
330
+ spelling=spelling,
331
+ name=spelling,
332
+ monad=Monad(
333
+ name=spelling,
334
+ rank=INFINITY,
335
+ function=_monad,
336
+ ),
337
+ dyad=Dyad(
338
+ name=spelling,
339
+ left_rank=INFINITY,
340
+ right_rank=INFINITY,
341
+ function=_dyad,
342
+ ),
343
+ )