-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcurry.py
More file actions
259 lines (200 loc) · 7.59 KB
/
curry.py
File metadata and controls
259 lines (200 loc) · 7.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def curry_vector(a):
# 1
def line(b=0):
# b = 0
def compute(x):
# 1 + 0 * xi for xi in x
return [a + b * xi for xi in x]
# return function w a + b as local vars
return compute
# return a callable that returns a callable
# that returns the result of the computation
return line
# set values for a for inner computation
base_func = curry_vector(
10
) # is a callable that returns a callable that returns the results of the computation
# base_func is line in the sense that calling it with no args will return compute (the function)
# with the value of b set in the computation
inner_callable = base_func() # b = 0, returns compute'
inner_callable([2, 3, 4]) # returns 10 + 0 * x for x in *args
def draw_vector(a, b, points):
return [a + b * xi for xi in points]
# also recursive partial application
from functools import partial
from inspect import signature
def mult(x, y, z):
return x * y * z
mult_10 = partial(mult, 10)
mult_10_20 = partial(mult_10, 20)
print(mult_10_20(30))
def curry(func):
def inner(arg):
# checking if the function has one argument,
# then return function as it is
if len(signature(func).parameters) == 1:
return func(arg)
return curry(partial(func, arg))
return inner
# using cuury function on the previous example function
@curry
def mult(x, y, z):
return x * y * z
print(mult(10)(20)(30))
from typing import Callable, Generic, TypeVar, Union
ReturnType = TypeVar("ReturnType")
class Partial(Generic[ReturnType]):
"""Represents a partial function application. `fn` is the function being
wrapped, and the args and kwargs are the saved ones from previous calls.
"""
def __init__(
self, num_args: int, fn: Callable[..., ReturnType], *args, **kwargs
) -> None:
self.num_args = num_args
self.fn = fn
self.args = args
self.kwargs = kwargs
def __call__(
self, *more_args, **more_kwargs
) -> Union[Partial[ReturnType], ReturnType]:
all_args = self.args + more_args # tuple addition
all_kwargs = dict(**self.kwargs, **more_kwargs) # non-mutative dictionary union
num_args = len(all_args) + len(all_kwargs)
if num_args >= self.num_args:
return self.fn(*all_args, **all_kwargs)
else:
return Partial(self.num_args, self.fn, *all_args, **all_kwargs)
def __repr__(self):
return f"Partial({self.fn}, args={self.args}, kwargs={self.kwargs})"
def curry(num_args: int) -> Callable[[Callable[..., ReturnType]], Partial[ReturnType]]:
"""Curries the decorated function. Instead of having to provide all arguments
at once, they can be provided one or a few at a time. Once at least `num_args`
arguments are provided, the wrapped function will be called. The doctests below
best illustrate its use.
The decorator returns a `Partial` object, which represents a partial application
of `fn`. This object is callable. It stores the arguments provided thus far, and
after receiving at least `num_args` arguments, it calls `fn`, otherwise it returns
another `Partial` object representing the partial progress made.
>>> @curry(num_args=3)
... def add(a, b, c):
... return a + b + c
>>> add5 = add(5)
>>> add7 = add5(2)
You can still call the function without currying.
>>> add(1, 2, 3)
6
This is not "real" currying, but it allows passing more complex state,
so I'd say this is in the spirit of currying and should be legal.
>>> add5(4, 3)
12
>>> add(1)(2, 3)
6
>>> add(1, 2)(3)
6
Strict currying:
>>> add5(4)(3)
12
>>> add7(2)
9
>>> add(1)(2)(3)
6
It is okay to have some default arguments. Notice that the wrapped function
`make_email` takes up to three arguments, but gets called when at least two
are provided.
>>> @curry(num_args=2)
... def make_email(username, domain, separator="@"):
... return username + separator + domain
>>> make_gmail = make_email(domain="gmail.com")
>>> make_gmail("haskell")
'haskell@gmail.com'
>>> make_gmail(username="curry")
'curry@gmail.com'
>>> make_gmail("curry", separator=">>=")
'curry>>=gmail.com'
>>> make_email("haskell", "curry.com")
'haskell@curry.com'
>>> make_email("haskell")("curry.com", ">>=")
'haskell>>=curry.com'
Note that I did consider using `inspect.signature` instead of using `num_args`,
but I chose the latter for making my code less verbose, since my objective here
is to show how one can curry functions elegantly in Python, and not to write
a bulletproof currying function that can handle all of Python's wonders. And
even then I think this is a very useful and flexible function.
Parameters
----------
num_args number of arguments to wait for before evaluating wrapped function
Returns
-------
a decorator that curries a function
"""
def decorator(fn: Callable[..., ReturnType]):
return Partial(num_args, fn)
return decorator
def curry_functional(num_args: int):
"""Curries the decorated function. Instead of having to provide all arguments
at once, they can be provided one or a few at a time. Once at least `num_args`
arguments are provided, the wrapped function will be called. The doctests below
best illustrate its use.
This is a purely functional implementation, not relying on any user-defined
classes. This fundamentally does the same thing as the implementation with
`Partial`, and I deliberately named the private functions `init` and `call`
to highlight their similarities to `Partial.__init__` and `Partial.__call__`.
>>> @curry_functional(num_args=3)
... def add(a, b, c):
... return a + b + c
>>> add5 = add(5)
>>> add7 = add5(2)
You can still call the function without currying.
>>> add(1, 2, 3)
6
This is not "real" currying, but it allows passing more complex state,
so I'd say this is in the spirit of currying and should be legal.
>>> add5(4, 3)
12
>>> add(1)(2, 3)
6
>>> add(1, 2)(3)
6
Strict currying:
>>> add5(4)(3)
12
>>> add7(2)
9
>>> add(1)(2)(3)
6
It is okay to have some default arguments. Notice that the wrapped function
`make_email` takes up to three arguments, but gets called when at least two
are provided.
>>> @curry_functional(num_args=2)
... def make_email(username, domain, separator="@"):
... return username + separator + domain
>>> make_gmail = make_email(domain="gmail.com")
>>> make_gmail("haskell")
'haskell@gmail.com'
>>> make_gmail(username="curry")
'curry@gmail.com'
>>> make_gmail("curry", separator=">>=")
'curry>>=gmail.com'
>>> make_email("haskell", "curry.com")
'haskell@curry.com'
>>> make_email("haskell")("curry.com", ">>=")
'haskell>>=curry.com'
Parameters
----------
num_args number of arguments to wait for before evaluating wrapped function
Returns
-------
a decorator that curries a function
"""
def decorator(fn: Callable[..., ReturnType]):
def init(*args, **kwargs):
def call(*more_args, **more_kwargs):
all_args = args + more_args
all_kwargs = dict(**kwargs, **more_kwargs)
if len(all_args) + len(all_kwargs) >= num_args:
return fn(*all_args, **all_kwargs)
else:
return init(*all_args, **all_kwargs)
return call
return init()
return decorator