Skip to content

Commit 1c4b76a

Browse files
committed
add listhell and pytkell dialects
1 parent e310be3 commit 1c4b76a

File tree

5 files changed

+379
-0
lines changed

5 files changed

+379
-0
lines changed

unpythonic/dialects/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313

1414
# re-exports
1515
from .lispython import Lispython # noqa: F401
16+
from .listhell import Listhell # noqa: F401
17+
from .pytkell import Pytkell # noqa: F401

unpythonic/dialects/listhell.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
"""LisThEll: it's not Lisp, it's not Python, it's not Haskell.
3+
4+
Powered by `mcpyrate` and `unpythonic`.
5+
"""
6+
7+
__all__ = ["Listhell"]
8+
9+
__version__ = '2.0.0'
10+
11+
from mcpyrate.quotes import macros, q # noqa: F401
12+
13+
from mcpyrate.dialects import Dialect
14+
from mcpyrate.splicing import splice_dialect
15+
16+
class Listhell(Dialect):
17+
def transform_ast(self, tree): # tree is an ast.Module
18+
with q as template:
19+
__lang__ = "Listhell" # noqa: F841, just provide it to user code.
20+
from unpythonic.syntax import macros, prefix, autocurry # noqa: F401, F811
21+
# auxiliary syntax elements for the macros
22+
from unpythonic.syntax import q, u, kw # noqa: F401
23+
from unpythonic import apply # noqa: F401
24+
from unpythonic import composerc as compose # compose from Right, Currying # noqa: F401
25+
with prefix, autocurry:
26+
__paste_here__ # noqa: F821, just a splicing marker.
27+
tree.body = splice_dialect(tree.body, template, "__paste_here__")
28+
return tree

unpythonic/dialects/pytkell.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# -*- coding: utf-8 -*-
2+
"""Pytkell: Python with automatic currying and lazy functions.
3+
4+
Powered by `mcpyrate` and `unpythonic`.
5+
"""
6+
7+
__all__ = ["Pytkell"]
8+
9+
__version__ = '2.0.0'
10+
11+
from mcpyrate.quotes import macros, q # noqa: F401
12+
13+
from mcpyrate.dialects import Dialect
14+
from mcpyrate.splicing import splice_dialect
15+
16+
class Pytkell(Dialect):
17+
def transform_ast(self, tree): # tree is an ast.Module
18+
with q as template:
19+
__lang__ = "Pytkell" # noqa: F841, just provide it to user code.
20+
from unpythonic.syntax import (macros, lazy, lazyrec, lazify, autocurry, # noqa: F401, F811
21+
let, letseq, letrec,
22+
dlet, dletseq, dletrec,
23+
blet, bletseq, bletrec,
24+
local, delete, do, do0,
25+
cond, forall)
26+
# auxiliary syntax elements for the macros
27+
from unpythonic.syntax import where, insist, deny # noqa: F401
28+
# functions that have a haskelly feel to them
29+
from unpythonic import (foldl, foldr, scanl, scanr, # noqa: F401
30+
s, imathify, gmathify, frozendict,
31+
memoize, fupdate, fup,
32+
gmemoize, imemoize, fimemoize,
33+
islice, take, drop, split_at, first, second, nth, last,
34+
flip, rotate)
35+
from unpythonic import composerc as compose # compose from Right, Currying (Haskell's . operator) # noqa: F401
36+
# this is a bit lispy, but we're not going out of our way to provide
37+
# a haskelly surface syntax for these.
38+
from unpythonic import cons, car, cdr, ll, llist, nil # noqa: F401
39+
with lazify, autocurry:
40+
__paste_here__ # noqa: F821, just a splicing marker.
41+
tree.body = splice_dialect(tree.body, template, "__paste_here__")
42+
return tree
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# -*- coding: utf-8 -*-
2+
"""Test the LisThEll dialect."""
3+
4+
# from mcpyrate.debug import dialects, StepExpansion
5+
from ...dialects import dialects, Listhell # noqa: F401
6+
7+
from ...syntax import macros, let, local, delete, do # noqa: F401
8+
from ...syntax import where # for let-where # noqa: F401
9+
from unpythonic import foldr, cons, nil, ll
10+
11+
# TODO: use the test framework
12+
13+
def runtests():
14+
# Function calls can be made in prefix notation, like in Lisps.
15+
# The first element of a literal tuple is the function to call,
16+
# the rest are its arguments.
17+
(print, f"Hello from {__lang__}!") # noqa: F821, the dialect template defines it.
18+
19+
x = 42 # can write any regular Python, too
20+
21+
# quote operator q locally turns off the function-call transformation:
22+
t1 = (q, 1, 2, (3, 4), 5) # q takes effect recursively # noqa: F821, the dialect template defines `q`.
23+
t2 = (q, 17, 23, x) # unlike in Lisps, x refers to its value even in a quote # noqa: F821
24+
(print, t1, t2)
25+
26+
# unquote operator u locally turns the transformation back on:
27+
t3 = (q, (u, print, 42), (print, 42), "foo", "bar") # noqa: F821
28+
assert t3 == (q, None, (print, 42), "foo", "bar") # noqa: F821
29+
30+
# quotes nest; call transformation made when quote level == 0
31+
t4 = (q, (print, 42), (q, (u, u, print, 42)), "foo", "bar") # noqa: F821
32+
assert t4 == (q, (print, 42), (None,), "foo", "bar") # noqa: F821
33+
34+
# Be careful:
35+
#
36+
# In LisThEll, this means "call the 0-arg function `x`".
37+
# But if `x` is not callable, `currycall` will return
38+
# the value as-is (needed for interaction with `call_ec`
39+
# and some other replace-def-with-value decorators).
40+
assert (x,) == 42
41+
42+
# This means "the tuple where the first element is `x`"
43+
(q, x) # noqa: F821
44+
45+
# give named args with kw(...) [it's syntax, not really a function!]:
46+
def f(*, a, b):
47+
return (q, a, b) # noqa: F821
48+
# in one kw(...), or...
49+
assert (f, kw(a="hi there", b="foo")) == (q, "hi there", "foo") # noqa: F821
50+
# in several kw(...), doesn't matter
51+
assert (f, kw(a="hi there"), kw(b="foo")) == (q, "hi there", "foo") # noqa: F821
52+
# in case of duplicate name across kws, rightmost wins
53+
assert (f, kw(a="hi there"), kw(b="foo"), kw(b="bar")) == (q, "hi there", "bar") # noqa: F821
54+
55+
# give *args with unpythonic.fun.apply, like in Lisps:
56+
lst = [1, 2, 3]
57+
def g(*args, **kwargs):
58+
return args + tuple(sorted(kwargs.items()))
59+
assert (apply, g, lst) == (q, 1, 2, 3) # noqa: F821
60+
# lst goes last; may have other args first
61+
assert (apply, g, "hi", "ho", lst) == (q, "hi", "ho", 1, 2, 3) # noqa: F821
62+
# named args in apply are also fine
63+
assert (apply, g, "hi", "ho", lst, kw(myarg=4)) == (q, "hi", "ho", 1, 2, 3, ('myarg', 4)) # noqa: F821
64+
65+
# Function call transformation only applies to tuples in load context
66+
# (i.e. NOT on the LHS of an assignment)
67+
a, b = (q, 100, 200) # noqa: F821
68+
assert a == 100 and b == 200
69+
a, b = (q, b, a) # pythonic swap in prefix syntax; must quote RHS # noqa: F821
70+
assert a == 200 and b == 100
71+
72+
# the prefix syntax leaves alone the let binding syntax ((name0, value0), ...)
73+
a = let[(x, 42)][x << x + 1]
74+
assert a == 43
75+
76+
# but the RHSs of the bindings are transformed normally:
77+
def double(x):
78+
return 2 * x
79+
a = let[(x, (double, 21))][x << x + 1]
80+
assert a == 43
81+
82+
# similarly, the prefix syntax leaves the "body tuple" of a do alone
83+
# (syntax, not semantically a tuple), but recurses into it:
84+
a = do[1, 2, 3]
85+
assert a == 3
86+
a = do[1, 2, (double, 3)]
87+
assert a == 6
88+
89+
# the extra bracket syntax (implicit do) has no danger of confusion, as it's a list, not tuple
90+
a = let[(x, 3)][[
91+
1,
92+
2,
93+
(double, x)]]
94+
assert a == 6
95+
96+
my_map = lambda f: (foldr, (compose, cons, f), nil) # noqa: F821
97+
assert (my_map, double, (q, 1, 2, 3)) == (ll, 2, 4, 6) # noqa: F821
98+
99+
(print, "All tests PASSED")
100+
101+
if __name__ == '__main__':
102+
(runtests,)
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# -*- coding: utf-8 -*-
2+
"""Test the Pytkell dialect."""
3+
4+
# from mcpyrate.debug import dialects, StepExpansion
5+
from ...dialects import dialects, Pytkell # noqa: F401
6+
7+
from ...syntax import macros, continuations, call_cc, tco # noqa: F401
8+
from ...misc import timer
9+
10+
from types import FunctionType
11+
from operator import add, mul
12+
13+
# TODO: use the test framework
14+
15+
def runtests():
16+
print(f"Hello from {__lang__}!") # noqa: F821, the dialect template defines it.
17+
18+
# function definitions (both def and lambda) and calls are auto-curried
19+
def add3(a, b, c):
20+
return a + b + c
21+
22+
a = add3(1)
23+
assert isinstance(a, FunctionType)
24+
a = a(2)
25+
assert isinstance(a, FunctionType)
26+
a = a(3)
27+
assert isinstance(a, int)
28+
29+
# actually partial evaluation so any of these works
30+
assert add3(1)(2)(3) == 6
31+
assert add3(1, 2)(3) == 6
32+
assert add3(1)(2, 3) == 6
33+
assert add3(1, 2, 3) == 6
34+
35+
# arguments of a function call are auto-lazified (converted to promises, MacroPy lazy[])
36+
def addfirst2(a, b, c):
37+
# a and b are read, so their promises are forced
38+
# c is not used, so not evaluated either
39+
return a + b
40+
assert addfirst2(1)(2)(1 / 0) == 3
41+
42+
# let-bindings are auto-lazified
43+
x = let[((x, 42), # noqa: F821
44+
(y, 1 / 0)) in x] # noqa: F821
45+
assert x == 42
46+
47+
# assignments are not (because they can imperatively update existing names)
48+
try:
49+
a = 1 / 0
50+
except ZeroDivisionError:
51+
pass
52+
else:
53+
assert False, "expected a zero division error"
54+
55+
# so if you want that, use lazy[] manually (it's a builtin in Pytkell)
56+
a = lazy[1 / 0] # this blows up only when the value is read (name 'a' in Load context) # noqa: F821
57+
58+
# manually lazify items in a data structure literal, recursively (see unpythonic.syntax.lazyrec):
59+
a = lazyrec[(1, 2, 3 / 0)] # noqa: F821
60+
assert a[:-1] == (1, 2) # reading a slice forces only that slice
61+
62+
# laziness passes through
63+
def g(a, b):
64+
return a # b not used
65+
def f(a, b):
66+
return g(a, b) # b is passed along, but its value is not used
67+
assert f(42, 1 / 0) == 42
68+
69+
def f(a, b):
70+
return (a, b)
71+
assert f(1, 2) == (1, 2)
72+
assert (flip(f))(1, 2) == (2, 1) # NOTE flip reverses all (doesn't just flip the first two) # noqa: F821
73+
74+
# # TODO: this doesn't work, because curry sees f's arities as (2, 2) (kwarg handling!)
75+
# assert (flip(f))(1, b=2) == (1, 2) # b -> kwargs
76+
77+
# http://www.cse.chalmers.se/~rjmh/Papers/whyfp.html
78+
my_sum = foldl(add, 0) # noqa: F821
79+
my_prod = foldl(mul, 1) # noqa: F821
80+
my_map = lambda f: foldr(compose(cons, f), nil) # compose is unpythonic.fun.composerc # noqa: F821
81+
82+
assert my_sum(range(1, 5)) == 10
83+
assert my_prod(range(1, 5)) == 24
84+
assert tuple(my_map((lambda x: 2 * x), (1, 2, 3))) == (2, 4, 6)
85+
86+
assert tuple(scanl(add, 0, (1, 2, 3))) == (0, 1, 3, 6) # noqa: F821
87+
assert tuple(scanr(add, 0, (1, 2, 3))) == (0, 3, 5, 6) # NOTE output ordering different from Haskell # noqa: F821
88+
89+
# let-in
90+
x = let[(a, 21) in 2 * a] # noqa: F821
91+
assert x == 42
92+
93+
x = let[((a, 21), # noqa: F821
94+
(b, 17)) in # noqa: F821
95+
2 * a + b] # noqa: F821
96+
assert x == 59
97+
98+
# let-where
99+
x = let[2 * a, where(a, 21)] # noqa: F821
100+
assert x == 42
101+
102+
x = let[2 * a + b, # noqa: F821
103+
where((a, 21), # noqa: F821
104+
(b, 17))] # noqa: F821
105+
assert x == 59
106+
107+
# nondeterministic evaluation (essentially do-notation in the List monad)
108+
#
109+
# pythagorean triples
110+
pt = forall[z << range(1, 21), # hypotenuse # noqa: F821
111+
x << range(1, z + 1), # shorter leg # noqa: F821
112+
y << range(x, z + 1), # longer leg # noqa: F821
113+
insist(x * x + y * y == z * z), # see also deny() # noqa: F821
114+
(x, y, z)] # noqa: F821
115+
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
116+
(8, 15, 17), (9, 12, 15), (12, 16, 20))
117+
118+
# functional update for sequences
119+
#
120+
tup1 = (1, 2, 3, 4, 5)
121+
tup2 = fup(tup1)[2:] << (10, 20, 30) # fup(sequence)[idx_or_slice] << sequence_of_values # noqa: F821
122+
assert tup2 == (1, 2, 10, 20, 30)
123+
assert tup1 == (1, 2, 3, 4, 5)
124+
125+
# immutable dict, with functional update
126+
#
127+
d1 = frozendict(foo='bar', bar='tavern') # noqa: F821
128+
d2 = frozendict(d1, bar='pub') # noqa: F821
129+
assert tuple(sorted(d1.items())) == (('bar', 'tavern'), ('foo', 'bar'))
130+
assert tuple(sorted(d2.items())) == (('bar', 'pub'), ('foo', 'bar'))
131+
132+
# s = mathematical Sequence (const, arithmetic, geometric, power)
133+
#
134+
assert last(take(10000, s(1, ...))) == 1 # noqa: F821
135+
assert last(take(5, s(0, 1, ...))) == 4 # noqa: F821
136+
assert last(take(5, s(1, 2, 4, ...))) == (1 * 2 * 2 * 2 * 2) # 16 # noqa: F821
137+
assert last(take(5, s(2, 4, 16, ...))) == (((((2)**2)**2)**2)**2) # 65536 # noqa: F821
138+
139+
# s() takes care to avoid roundoff
140+
assert last(take(1001, s(0, 0.001, ...))) == 1 # noqa: F821
141+
142+
# iterables returned by s() support infix math
143+
# (to add infix math support to some other iterable, m(iterable))
144+
c = s(1, 3, ...) + s(2, 4, ...) # noqa: F821
145+
assert tuple(take(5, c)) == (3, 7, 11, 15, 19) # noqa: F821
146+
assert tuple(take(5, c)) == (23, 27, 31, 35, 39) # consumed! # noqa: F821
147+
148+
# imemoize = memoize Iterable (makes a gfunc, drops math support)
149+
# gmathify returns a new gfunc that adds infix math support
150+
# to generators the original gfunc makes.
151+
#
152+
# see also gmemoize, fimemoize in unpythonic
153+
#
154+
mi = lambda x: gmathify(imemoize(x)) # noqa: F821
155+
a = mi(s(1, 3, ...)) # noqa: F821
156+
b = mi(s(2, 4, ...)) # noqa: F821
157+
c = lambda: a() + b()
158+
assert tuple(take(5, c())) == (3, 7, 11, 15, 19) # noqa: F821
159+
assert tuple(take(5, c())) == (3, 7, 11, 15, 19) # now it's a new instance; no recomputation # noqa: F821
160+
161+
factorials = mi(scanl(mul, 1, s(1, 2, ...))) # 0!, 1!, 2!, ... # noqa: F821
162+
assert last(take(6, factorials())) == 120 # noqa: F821
163+
assert first(drop(5, factorials())) == 120 # noqa: F821
164+
165+
squares = s(1, 2, ...)**2 # noqa: F821
166+
assert last(take(10, squares)) == 100 # noqa: F821
167+
168+
harmonic = 1 / s(1, 2, ...) # noqa: F821
169+
assert last(take(10, harmonic)) == 1 / 10 # noqa: F821
170+
171+
# unpythonic's continuations are supported
172+
with continuations:
173+
k = None # kontinuation
174+
def setk(*args, cc):
175+
nonlocal k
176+
k = cc # current continuation, i.e. where to go after setk() finishes
177+
return args # tuple means multiple-return-values
178+
def doit():
179+
lst = ['the call returned']
180+
*more, = call_cc[setk('A')]
181+
return lst + list(more)
182+
assert doit() == ['the call returned', 'A']
183+
# We can now send stuff into k, as long as it conforms to the
184+
# signature of the assignment targets of the "call_cc".
185+
assert k('again') == ['the call returned', 'again']
186+
assert k('thrice', '!') == ['the call returned', 'thrice', '!']
187+
188+
# as is unpythonic's tco
189+
with tco:
190+
def fact(n):
191+
def f(k, acc):
192+
if k == 1:
193+
return acc
194+
return f(k - 1, k * acc)
195+
return f(n, 1) # TODO: doesn't work as f(n, acc=1) due to curry's kwarg handling
196+
assert fact(4) == 24
197+
print("Performance...")
198+
with timer() as tictoc:
199+
fact(5000) # no crash, but Pytkell is a bit slow
200+
print(" Time taken for factorial of 5000: {:g}s".format(tictoc.dt))
201+
202+
print("All tests PASSED")
203+
204+
if __name__ == '__main__':
205+
runtests()

0 commit comments

Comments
 (0)