0
# Function Composition
1
2
Higher-order functions for composing, currying, and transforming functions. These utilities enable elegant functional programming patterns and pipeline creation with support for memoization, partial application, and function introspection.
3
4
## Capabilities
5
6
### Basic Function Operations
7
8
Core utilities for working with functions and applying them in various ways.
9
10
```python { .api }
11
def identity(x):
12
"""
13
Identity function - returns input unchanged.
14
15
Parameters:
16
- x: any value
17
18
Returns:
19
The input value x unchanged
20
"""
21
22
def apply(*func_and_args, **kwargs):
23
"""
24
Apply function to arguments and return result.
25
26
Parameters:
27
- func_and_args: function followed by its arguments
28
- **kwargs: keyword arguments for function
29
30
Returns:
31
Result of calling func(*args, **kwargs)
32
"""
33
34
def do(func, x):
35
"""
36
Run function on x for side effects, return x unchanged.
37
38
Parameters:
39
- func: function to call for side effects
40
- x: value to pass to function and return
41
42
Returns:
43
The input value x (func result is discarded)
44
"""
45
46
def flip(func, a, b):
47
"""
48
Call function with arguments flipped.
49
50
Parameters:
51
- func: binary function to call
52
- a: first argument (becomes second)
53
- b: second argument (becomes first)
54
55
Returns:
56
Result of func(b, a)
57
"""
58
```
59
60
### Function Composition
61
62
Functions for combining multiple functions into pipelines and compositions.
63
64
```python { .api }
65
def compose(*funcs):
66
"""
67
Compose functions to operate in series (right to left).
68
69
Parameters:
70
- *funcs: functions to compose
71
72
Returns:
73
Function that applies all input functions in reverse order
74
"""
75
76
def compose_left(*funcs):
77
"""
78
Compose functions left to right.
79
80
Parameters:
81
- *funcs: functions to compose
82
83
Returns:
84
Function that applies all input functions in given order
85
"""
86
87
def pipe(data, *funcs):
88
"""
89
Pipe value through sequence of functions (left to right).
90
91
Parameters:
92
- data: initial value
93
- *funcs: functions to apply in sequence
94
95
Returns:
96
Result of applying all functions to data in sequence
97
"""
98
99
def thread_first(val, *forms):
100
"""
101
Thread value through sequence of functions (value as first argument).
102
103
Parameters:
104
- val: value to thread through functions
105
- *forms: functions or (function, *args) tuples
106
107
Returns:
108
Result of threading val through all forms as first argument
109
"""
110
111
def thread_last(val, *forms):
112
"""
113
Thread value through sequence of functions (value as last argument).
114
115
Parameters:
116
- val: value to thread through functions
117
- *forms: functions or (function, *args) tuples
118
119
Returns:
120
Result of threading val through all forms as last argument
121
"""
122
```
123
124
### Function Modification
125
126
Functions that modify or enhance the behavior of other functions.
127
128
```python { .api }
129
def complement(func):
130
"""
131
Convert predicate function to its logical complement.
132
133
Parameters:
134
- func: predicate function returning True/False
135
136
Returns:
137
Function that returns opposite boolean result
138
"""
139
140
def juxt(*funcs):
141
"""
142
Create function that calls several functions with same arguments.
143
144
Parameters:
145
- *funcs: functions to call with same arguments
146
147
Returns:
148
Function that returns tuple of results from all input functions
149
"""
150
151
def memoize(func, cache=None, key=None):
152
"""
153
Cache function results for faster repeated evaluation.
154
155
Parameters:
156
- func: function to memoize
157
- cache: dictionary to use for caching (optional)
158
- key: function to compute cache key (optional)
159
160
Returns:
161
Memoized version of input function
162
"""
163
164
def excepts(exc, func, handler=return_none):
165
"""
166
Create function with functional try/except block.
167
168
Parameters:
169
- exc: exception type or tuple of types to catch
170
- func: function to wrap with exception handling
171
- handler: function to call on exception (default returns None)
172
173
Returns:
174
Function that catches specified exceptions and calls handler
175
"""
176
177
def return_none(exc):
178
"""
179
Returns None regardless of input.
180
181
Used as default exception handler for excepts function.
182
183
Parameters:
184
- exc: exception (ignored)
185
186
Returns:
187
None
188
"""
189
```
190
191
### Currying & Partial Application
192
193
Support for currying and partial application of functions.
194
195
```python { .api }
196
class curry:
197
"""
198
Curry a callable for partial application.
199
200
A curried function can be called with fewer arguments than required,
201
returning a new function that expects the remaining arguments.
202
"""
203
204
def __init__(self, *args, **kwargs):
205
"""
206
Create curried function.
207
208
Parameters:
209
- func: function to curry
210
- *args: optional initial arguments
211
- **kwargs: optional initial keyword arguments
212
"""
213
214
def bind(self, *args, **kwargs):
215
"""
216
Create new curry with additional bound arguments.
217
218
Parameters:
219
- *args: additional positional arguments to bind
220
- **kwargs: additional keyword arguments to bind
221
222
Returns:
223
New curry instance with additional bound arguments
224
"""
225
226
def call(self, *args, **kwargs):
227
"""
228
Call function without currying (bypass curry behavior).
229
230
Parameters:
231
- *args: all positional arguments
232
- **kwargs: all keyword arguments
233
234
Returns:
235
Direct result of calling underlying function
236
"""
237
```
238
239
### Function Introspection
240
241
Utilities for inspecting function signatures and argument requirements.
242
243
```python { .api }
244
def num_required_args(func, sigspec=None):
245
"""
246
Number of required positional arguments for function.
247
248
Parameters:
249
- func: function to inspect
250
- sigspec: cached signature specification (optional)
251
252
Returns:
253
Integer number of required positional arguments
254
"""
255
256
def has_varargs(func, sigspec=None):
257
"""
258
Check if function accepts variable positional arguments (*args).
259
260
Parameters:
261
- func: function to inspect
262
- sigspec: cached signature specification (optional)
263
264
Returns:
265
True if function accepts *args
266
"""
267
268
def has_keywords(func, sigspec=None):
269
"""
270
Check if function accepts keyword arguments (**kwargs).
271
272
Parameters:
273
- func: function to inspect
274
- sigspec: cached signature specification (optional)
275
276
Returns:
277
True if function accepts **kwargs
278
"""
279
280
def is_valid_args(func, args, kwargs, sigspec=None):
281
"""
282
Check if func(*args, **kwargs) is valid call.
283
284
Parameters:
285
- func: function to check
286
- args: positional arguments tuple
287
- kwargs: keyword arguments dict
288
- sigspec: cached signature specification (optional)
289
290
Returns:
291
True if arguments are valid for function
292
"""
293
294
def is_partial_args(func, args, kwargs, sigspec=None):
295
"""
296
Check if partial(func, *args, **kwargs) could be valid.
297
298
Parameters:
299
- func: function to check
300
- args: positional arguments tuple
301
- kwargs: keyword arguments dict
302
- sigspec: cached signature specification (optional)
303
304
Returns:
305
True if partial application could be valid
306
"""
307
308
def is_arity(n, func, sigspec=None):
309
"""
310
Check if function takes exactly n positional arguments.
311
312
Parameters:
313
- n: expected number of arguments
314
- func: function to check
315
- sigspec: cached signature specification (optional)
316
317
Returns:
318
True if function requires exactly n positional arguments
319
"""
320
```
321
322
## Classes
323
324
```python { .api }
325
class Compose:
326
"""Function composition class for multiple function pipeline."""
327
328
def __init__(self, funcs):
329
"""
330
Create composition of functions.
331
332
Parameters:
333
- funcs: sequence of functions to compose
334
"""
335
336
def __call__(self, *args, **kwargs):
337
"""
338
Call composed functions on arguments.
339
340
Parameters:
341
- *args: positional arguments for first function
342
- **kwargs: keyword arguments for first function
343
344
Returns:
345
Result of applying all composed functions
346
"""
347
348
class InstanceProperty:
349
"""Property that returns different value when accessed on class vs instance."""
350
351
def __init__(self, fget=None, fset=None, fdel=None, doc=None, classval=None):
352
"""
353
Create instance property.
354
355
Parameters:
356
- fget: getter function
357
- fset: setter function (optional)
358
- fdel: deleter function (optional)
359
- doc: docstring (optional)
360
- classval: value returned when accessed on class (optional)
361
"""
362
```
363
364
## Usage Examples
365
366
### Function Pipeline Creation
367
368
```python
369
from toolz import pipe, compose, curry
370
371
# Data processing pipeline
372
def clean_text(text):
373
return text.strip().lower()
374
375
def remove_punctuation(text):
376
return ''.join(c for c in text if c.isalnum() or c.isspace())
377
378
def extract_words(text):
379
return text.split()
380
381
# Using pipe (left to right)
382
text = " Hello, World! "
383
words = pipe(
384
text,
385
clean_text,
386
remove_punctuation,
387
extract_words
388
)
389
# ['hello', 'world']
390
391
# Using compose (right to left)
392
text_processor = compose(
393
extract_words,
394
remove_punctuation,
395
clean_text
396
)
397
words = text_processor(" Hello, World! ")
398
# ['hello', 'world']
399
```
400
401
### Currying and Partial Application
402
403
```python
404
from toolz import curry
405
from operator import add, mul
406
407
# Create curried functions
408
@curry
409
def multiply(x, y):
410
return x * y
411
412
@curry
413
def power(base, exponent):
414
return base ** exponent
415
416
# Partial application
417
double = multiply(2)
418
square = power(exponent=2)
419
cube = power(exponent=3)
420
421
# Use in data processing
422
numbers = [1, 2, 3, 4, 5]
423
doubled = list(map(double, numbers)) # [2, 4, 6, 8, 10]
424
squared = list(map(square, numbers)) # [1, 4, 9, 16, 25]
425
cubed = list(map(cube, numbers)) # [1, 8, 27, 64, 125]
426
```
427
428
### Threading Operations
429
430
```python
431
from toolz import thread_first, thread_last
432
433
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
434
435
# Thread first (data as first argument)
436
result = thread_first(
437
data,
438
(filter, lambda x: x % 2 == 0), # filter(lambda x: x % 2 == 0, data)
439
(map, lambda x: x * 2), # map(lambda x: x * 2, filtered)
440
list # list(mapped)
441
)
442
# [4, 8, 12, 16, 20]
443
444
# Thread last (data as last argument)
445
result = thread_last(
446
data,
447
(lambda seq: filter(lambda x: x % 2 == 0, seq)),
448
(lambda seq: map(lambda x: x * 2, seq)),
449
list
450
)
451
# [4, 8, 12, 16, 20]
452
```
453
454
### Function Composition with Memoization
455
456
```python
457
from toolz import memoize, compose
458
import time
459
460
@memoize
461
def expensive_calculation(n):
462
time.sleep(0.1) # Simulate expensive operation
463
return n * n
464
465
@memoize
466
def another_expensive_calc(n):
467
time.sleep(0.1)
468
return n + 10
469
470
# Compose memoized functions
471
pipeline = compose(another_expensive_calc, expensive_calculation)
472
473
# First call is slow
474
start = time.time()
475
result1 = pipeline(5) # expensive_calculation(5) -> another_expensive_calc(25)
476
time1 = time.time() - start
477
478
# Second call is fast (memoized)
479
start = time.time()
480
result2 = pipeline(5) # Both results cached
481
time2 = time.time() - start
482
483
print(f"First call: {time1:.2f}s, Second call: {time2:.4f}s")
484
# First call: 0.20s, Second call: 0.0001s
485
```
486
487
### Function Utilities
488
489
```python
490
from toolz import juxt, complement, do
491
492
# juxt - call multiple functions with same arguments
493
def add_one(x): return x + 1
494
def multiply_two(x): return x * 2
495
def square(x): return x * x
496
497
multi_transform = juxt(add_one, multiply_two, square)
498
result = multi_transform(5) # (6, 10, 25)
499
500
# complement - logical opposite
501
def is_even(x): return x % 2 == 0
502
is_odd = complement(is_even)
503
504
evens = list(filter(is_even, [1, 2, 3, 4, 5])) # [2, 4]
505
odds = list(filter(is_odd, [1, 2, 3, 4, 5])) # [1, 3, 5]
506
507
# do - side effects while passing through
508
def log_value(x):
509
print(f"Processing: {x}")
510
511
process_with_logging = lambda x: do(log_value, x * 2)
512
result = process_with_logging(5) # Prints "Processing: 10", returns 10
513
```