-
-
Save divs1210/d218d4b747b08751b2a232260321cdeb to your computer and use it in GitHub Desktop.
import types | |
# Helpers | |
# ======= | |
def _obj(): | |
'''Dummy object''' | |
return lambda: None | |
_FILLER = _obj() | |
# API | |
# === | |
def Y(g): | |
'''Y combinator - makes recursive lambdas | |
ex: Y(lambda fact: | |
lambda n: | |
1 if n < 2 else n * fact(n - 1))(5) | |
gives: 120 | |
''' | |
exp = lambda f: g(lambda arg: f(f)(arg)) | |
return (exp)(exp) | |
def COND(cond_body_pairs, _else=lambda: None): | |
'''Functional if-elif-...-else expression | |
ex: COND((1==0, lambda: 'a', | |
2==0, lambda: 'b', | |
3==0, lambda: 'c'), | |
_else= lambda: 'd') | |
gives: 'd' | |
Note: All conditions are evaluated immediately! | |
For conditions that should be evaluated only | |
when required, use IF. | |
''' | |
if len(cond_body_pairs) == 0: | |
return _else() | |
cond, body = cond_body_pairs[:2] | |
if cond: | |
return body() | |
else: | |
return COND(cond_body_pairs[2:], _else) | |
def IF(cond, then, _else=lambda: None): | |
'''Functional if-then-else expression | |
ex: IF(1==0, lambda: 'a', | |
_else= lambda: 'b') | |
gives: 'b' | |
''' | |
return COND((cond, then), _else) | |
def LET(bindings, body, env=None): | |
'''Introduce local bindings. | |
ex: LET(('a', 1, | |
'b', 2), | |
lambda o: [o.a, o.b]) | |
gives: [1, 2] | |
Bindings down the chain can depend on | |
the ones above them through a lambda. | |
ex: LET(('a', 1, | |
'b', lambda o: o.a + 1), | |
lambda o: o.b) | |
gives: 2 | |
''' | |
if len(bindings) == 0: | |
return body(env) | |
env = env or _obj() | |
k, v = bindings[:2] | |
if isinstance(v, types.FunctionType): | |
v = v(env) | |
setattr(env, k, v) | |
return LET(bindings[2:], body, env) | |
def FOR(bindings, body, env=None): | |
'''Clojure style List comprehension. | |
ex: FOR(('a', range(2), | |
'b', range(2)), | |
lambda o: (o.a, o.b)) | |
gives: [(0, 0), (0, 1), (1, 0), (1, 1)] | |
Bindings down the chain can depend on | |
the ones above them through a lambda | |
like in LET. | |
Special bindings take lambdas as values | |
and can be used any number of times: | |
* ':LET' - Temporary bindings | |
* ':IF' - don't produce a value if this | |
returns a falsey value | |
* ':WHILE' - break out of the innermost | |
loop if this returns a falsey value | |
''' | |
if len(bindings) == 0: | |
tmp = body(env) | |
return [] if tmp is _FILLER else [tmp] | |
env = env or _obj() | |
k, v = bindings[:2] | |
if k == ':IF': | |
cond = v(env) | |
return FOR(bindings[2:], | |
lambda e: body(e) if cond else _FILLER, | |
env) | |
elif k == ':LET': | |
return LET(v, | |
lambda e: FOR(bindings[2:], body, e), | |
env) | |
elif k == ':WHILE': | |
if v(env): | |
return FOR(bindings[2:], body, env) | |
else: | |
return [] | |
elif isinstance(v, types.FunctionType): | |
v = v(env) | |
res = [] | |
for x in v: | |
setattr(env, k, x) | |
res += FOR(bindings[2:], body, env) | |
delattr(env, k) | |
return res | |
# Tests | |
# ===== | |
## LET form | |
assert LET(('a', 2, | |
'b', lambda o: o.a * 3), | |
lambda o: o.b - 1) == 5 | |
## Y combinator (recursive lambda) and IF form | |
assert Y(lambda fact: | |
lambda n: | |
IF(n < 2, lambda: 1, | |
_else= lambda: n * fact(n - 1)))(5) == 120 | |
## FOR comprehension | |
assert FOR(('a', range(3)), | |
lambda o: o.a + 1) == [1, 2, 3] | |
## Chained FOR comprehension | |
assert FOR(('a', range(3), | |
':IF', lambda o: o.a > 0, | |
'b', lambda o: range(3 - o.a), | |
':LET', ('res', lambda o: [o.a, o.b]), | |
':WHILE', lambda o: o.a < 2), | |
lambda o: o.res) == [ | |
# filtered a == 0 | |
[1, 0], [1, 1], | |
# stopped at a == 2 | |
] |
Brilliant! I had been replacing my need for let x = ... ,,,
with ((lambda x ,,,) ...)
, producing monsters like this (a fast memoized Fibonacci when fed to a 2-arg Y combinator):
ff_e = (lambda f:
(lambda a:
(lambda n:
(a, 1) if n < 2 else
((lambda n_1:
(a, a[n_1]) if n_1 in a else
((lambda fim1:
((lambda m1:
((lambda r1:
((lambda a1:
((lambda n_2:
(a1, r1 + a1[n_2]) if n_2 in a1 else
((lambda fim2:
((lambda m2:
((lambda r2:
((lambda a2:
(a2, r1 + r2))
(m2[0] | {n_2: r2})))
(m2[1])))
(fim2(n_2))))
(f(a1))))
(n - 2)))
(m1[0] | {n_1: r1})))
(m1[1])))
(fim1(n_1))))
(f(a))))
(n - 1)))))
ouch, that's gnarly! 😅
glad to be of help!
It's from this fully-typed development of Y https://github.com/rebcabin/rebcabin.github.io/blob/main/PythonYCombinators.ipynb . I'm working my way through "Lambda, the Ultimate Imperative" as part of a new Python compiler https://lpython.org
@rebcabin Nice!
I can't seem to find much to read about LCompilers or ASR online, though.
Could you point me to some literature?
@rebcabin Would love to know more about the ASR stuff!
I had gone through the LCompilers website and github, but didn't find much info.
Went through the LFortran page this time, and saw some more details.
We will never extend a language, but we may subset it for speed.
This seems to not be true for LFortran, with the extended global scope and type inference.
Could I be of help in some way? Would love to contribute to LPython!
I have seen two different ways of speeding up Python using PE - PyPy tracing JIT and Graal's self-modifying AST.
The ASR stuff looks like a variant of PE from afar, correct me if I'm wrong!
Yes, I will get back to you here with some info on contributing to LPython!
BTW, I am running with one of your ideas to build a Schemulation of Python's imperatives so that we are sure we know what we're doing when we design ASR.
@divs1210 we would love any contributions. You are welcome to send a PR, you can join us at the Zulip chat (https://lfortran.zulipchat.com/, we use it for LPython as well for now), and I am also happy to meet with you to get you up and running.
Check out similar exercises: