-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtest_pytkell.py
More file actions
285 lines (241 loc) · 11.6 KB
/
test_pytkell.py
File metadata and controls
285 lines (241 loc) · 11.6 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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# -*- coding: utf-8 -*-
"""Test the Pytkell dialect."""
# from mcpyrate.debug import dialects, StepExpansion
from ...dialects import dialects, Pytkell # noqa: F401
from ...syntax import macros, test, the, test_raises # noqa: F401
from ...test.fixtures import session, testset
from ...syntax import macros, continuations, call_cc, tco # noqa: F401, F811
from ...syntax import macros, monadic_do # noqa: F401, F811
from ...monads import Maybe, Writer, List
from ...funutil import Values
from ...misc import timer
from math import sqrt
from types import FunctionType
from operator import add, mul
def runtests():
print(f"Hello from {__lang__}!") # noqa: F821, the dialect template defines it.
# function definitions (both def and lambda) and calls are auto-curried
with testset("implicit autocurry"):
def add3(a, b, c):
return a + b + c
a = add3(1)
test[isinstance(the[a], FunctionType)]
a = a(2)
test[isinstance(the[a], FunctionType)]
a = a(3)
test[isinstance(the[a], int)]
# actually partial evaluation so any of these works
test[add3(1)(2)(3) == 6]
test[add3(1, 2)(3) == 6]
test[add3(1)(2, 3) == 6]
test[add3(1, 2, 3) == 6]
# arguments of a function call are auto-lazified (converted to promises, lazy[])
with testset("implicit lazify"):
def addfirst2(a, b, c):
# a and b are read, so their promises are forced
# c is not used, so not evaluated either
return a + b
test[addfirst2(1)(2)(1 / 0) == 3]
# let-bindings are auto-lazified
with test["y is unused, so it should not be evaluated"]:
x = let[[x << 42, # noqa: F821
y << 1 / 0] in x] # noqa: F821
return x == 42 # access `x`, to force the promise
# assignments are not (because they can imperatively update existing names)
with test_raises[ZeroDivisionError]:
a = 1 / 0
# so if you want that, use lazy[] manually (it's a builtin in Pytkell)
with test:
a = lazy[1 / 0] # this blows up only when the value is read (name 'a' in Load context) # noqa: F821
# manually lazify items in a data structure literal, recursively (see unpythonic.syntax.lazyrec):
with test:
a = lazyrec[(1, 2, 3 / 0)] # noqa: F821
return a[:-1] == (1, 2) # reading a slice forces only that slice
# laziness passes through
def g(a, b):
return a # b not used
def f(a, b):
return g(a, b) # b is passed along, but its value is not used
test[f(42, 1 / 0) == 42]
def f(a, b):
return (a, b)
test[f(1, 2) == (1, 2)]
test[(flip(f))(1, 2) == (2, 1)] # NOTE flip reverses all (doesn't just flip the first two) # noqa: F821
# flip reverses only those arguments that are passed *positionally*
test[(flip(f))(1, b=2) == (1, 2)] # b -> kwargs # noqa: F821
# http://www.cse.chalmers.se/~rjmh/Papers/whyfp.html
with testset("iterables"):
my_sum = foldl(add, 0) # noqa: F821
my_prod = foldl(mul, 1) # noqa: F821
my_map = lambda f: foldr(compose(cons, f), nil) # compose is unpythonic.fun.composerc # noqa: F821
test[my_sum(range(1, 5)) == 10]
test[my_prod(range(1, 5)) == 24]
test[tuple(my_map((lambda x: 2 * x), (1, 2, 3))) == (2, 4, 6)]
test[tuple(scanl(add, 0, (1, 2, 3))) == (0, 1, 3, 6)] # noqa: F821
test[tuple(scanr(add, 0, (1, 2, 3))) == (0, 3, 5, 6)] # NOTE output ordering different from Haskell # noqa: F821
with testset("let constructs"):
# let-in
x = let[[a << 21] in 2 * a] # noqa: F821
test[x == 42]
x = let[[a << 21, # noqa: F821
b << 17] in # noqa: F821
2 * a + b] # noqa: F821
test[x == 59]
# let-where
x = let[2 * a, where[a << 21]] # noqa: F821
test[x == 42]
x = let[2 * a + b, # noqa: F821
where[a << 21, # noqa: F821
b << 17]] # noqa: F821
test[x == 59]
# nondeterministic evaluation (essentially do-notation in the List monad)
#
# pythagorean triples
with testset("nondeterministic evaluation"):
# TODO: This is very slow in Pytkell; investigate whether the cause is `lazify`, `autocurry`, or both.
#
# Running the same code in a macro-enabled IPython (i.e. without Pytkell), there is no noticeable delay
# after you press enter, before it gives the result. If you want to try it, you'll need to:
#
# %load_ext mcpyrate.repl.iconsole
# from unpythonic.syntax import macros, forall, test
# from unpythonic import insist
#
pt = forall[z << range(1, 21), # hypotenuse # noqa: F821
x << range(1, z + 1), # shorter leg # noqa: F821
y << range(x, z + 1), # longer leg # noqa: F821
insist(x * x + y * y == z * z), # see also deny() # noqa: F821
(x, y, z)] # noqa: F821
test[tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
(8, 15, 17), (9, 12, 15), (12, 16, 20))]
with testset("functional update"):
# functional update for sequences
tup1 = (1, 2, 3, 4, 5)
tup2 = fup(tup1)[2:] << (10, 20, 30) # fup(sequence)[idx_or_slice] << sequence_of_values # noqa: F821
test[tup2 == (1, 2, 10, 20, 30)]
test[tup1 == (1, 2, 3, 4, 5)]
# immutable dict, with functional update
d1 = frozendict(foo='bar', bar='tavern') # noqa: F821
d2 = frozendict(d1, bar='pub') # noqa: F821
test[tuple(sorted(d1.items())) == (('bar', 'tavern'), ('foo', 'bar'))]
test[tuple(sorted(d2.items())) == (('bar', 'pub'), ('foo', 'bar'))]
# s = mathematical Sequence (const, arithmetic, geometric, power)
with testset("mathematical sequences with s()"):
test[last(take(10000, s(1, ...))) == 1] # noqa: F821
test[last(take(5, s(0, 1, ...))) == 4] # noqa: F821
test[last(take(5, s(1, 2, 4, ...))) == (1 * 2 * 2 * 2 * 2)] # 16 # noqa: F821
test[last(take(5, s(2, 4, 16, ...))) == (((((2)**2)**2)**2)**2)] # 65536 # noqa: F821
# s() takes care to avoid roundoff
test[last(take(1001, s(0, 0.001, ...))) == 1] # noqa: F821
# iterables returned by s() support infix math
# (to add infix math support to some other iterable, imathify(iterable))
c = s(1, 3, ...) + s(2, 4, ...) # noqa: F821
test[tuple(take(5, c)) == (3, 7, 11, 15, 19)] # noqa: F821
test[tuple(take(5, c)) == (23, 27, 31, 35, 39)] # consumed! # noqa: F821
# imemoize = memoize Iterable (makes a gfunc, drops math support)
# gmathify returns a new gfunc that adds infix math support
# to generators the original gfunc makes.
#
# see also gmemoize, fimemoize in unpythonic
#
with testset("mathematical sequences utilities"):
mi = lambda x: gmathify(imemoize(x)) # noqa: F821
a = mi(s(1, 3, ...)) # noqa: F821
b = mi(s(2, 4, ...)) # noqa: F821
c = lambda: a() + b()
test[tuple(take(5, c())) == (3, 7, 11, 15, 19)] # noqa: F821
test[tuple(take(5, c())) == (3, 7, 11, 15, 19)] # now it's a new instance; no recomputation # noqa: F821
factorials = mi(scanl(mul, 1, s(1, 2, ...))) # 0!, 1!, 2!, ... # noqa: F821
test[last(take(6, factorials())) == 120] # noqa: F821
test[first(drop(5, factorials())) == 120] # noqa: F821
squares = s(1, 2, ...)**2 # noqa: F821
test[last(take(10, squares)) == 100] # noqa: F821
harmonic = 1 / s(1, 2, ...) # noqa: F821
test[last(take(10, harmonic)) == 1 / 10] # noqa: F821
# unpythonic's continuations are supported
with testset("integration with continuations"):
with continuations:
k = None # kontinuation
def setk(*args, cc):
nonlocal k
k = cc # current continuation, i.e. where to go after setk() finishes
return Values(*args) # multiple-return-values
def doit():
lst = ['the call returned']
*more, = call_cc[setk('A')]
return lst + list(more)
test[doit() == ['the call returned', 'A']]
# We can now send stuff into k, as long as it conforms to the
# signature of the assignment targets of the "call_cc".
test[k('again') == ['the call returned', 'again']]
test[k('thrice', '!') == ['the call returned', 'thrice', '!']]
# as is unpythonic's tco
with testset("integration with tco"):
with tco:
def fact(n):
def f(k, acc):
if k == 1:
return acc
return f(k - 1, k * acc)
return f(n, acc=1)
test[fact(4) == 24]
# **CAUTION**: Pytkell is slow, because so much happens at run time. On an i7-4710MQ:
#
# - The performance test below, `fact(5000)`, completes in about 500ms.
#
# **Without** Pytkell, using a macro-enabled IPython session:
#
# - `fact(5000)` with the same definition (the `with tco` block above) completes in about 15ms.
# - `prod(range(1, 5001))` completes in about 7ms. (This is `unpythonic.prod`, which uses
# `unpythonic`'s custom fold implementation.)
# - The simplest thing that works:
# n = 1
# for k in range(1, 5001):
# n *= k
# completes in about 5ms.
print("Performance...")
with timer() as tictoc:
fact(5000) # no crash
print(" Time taken for factorial of 5000: {:g}s".format(tictoc.dt))
# No kell is complete without its monads.
with testset("monadic do-notation"):
# `nil` is available from the Pytkell dialect template (module-level import).
# Maybe — sqrt chain. In Pytkell's auto-lazy world, the chain
# still evaluates eagerly at the bind points (since the receiver
# of >> needs to be an actual monad to dispatch).
def maybe_sqrt(x):
if x < 0:
return Maybe(nil) # noqa: F821 -- `nil` is in the Pytkell dialect
return Maybe(sqrt(x))
with monadic_do[Maybe] as root4:
[a := maybe_sqrt(16),
b := maybe_sqrt(a),
Maybe(b)]
test[root4 == Maybe(2.0)]
with monadic_do[Maybe] as bad:
[a := maybe_sqrt(-1),
b := maybe_sqrt(a),
Maybe(b)]
test[bad == Maybe(nil)] # noqa: F821 -- `nil` is in the Pytkell dialect
# List — classical Pythagorean triples.
def r(lo, hi):
return List.from_iterable(range(lo, hi))
with monadic_do[List] as pt:
[z := r(1, 21),
x := r(1, z + 1),
y := r(x, z + 1),
List.guard(x * x + y * y == z * z),
List((x, y, z))]
test[tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
(8, 15, 17), (9, 12, 15), (12, 16, 20))]
# Writer — logged computation.
with monadic_do[Writer] as w:
[a := Writer(10, "start; "),
b := Writer(a + 1, "+1; "),
Writer(b * 2, "doubled; ")]
value, log = w.data
test[value == 22]
test[log == "start; +1; doubled; "]
if __name__ == '__main__':
with session(__file__):
runtests()