In the spirit of toolz, we provide missing features for Python, mainly from the list processing tradition, but with some Haskellisms mixed in. We extend the language with a set of syntactic macros. We emphasize clear, pythonic syntax, and making features work together.
The features are built out of, in increasing order of magic:
- Pure Python (e.g. batteries for
itertools), - Macros driving a pure-Python core (e.g.
do,let), - Pure macros (e.g.
continuations,lazify,dbg).
This depends on the purpose of each feature, as well as ease-of-use considerations. See our design notes for more information.
None required.
MacroPy optional, to enable the syntactic macro layer.
Pure-Python feature set
Syntactic macro feature set
Design notes: for more insight into the design choices of unpythonic.
Small, limited-space overview of the overall flavor. There's a lot more that doesn't fit here, especially in the pure-Python feature set. See the full documentation and unit tests for more.
Click each example to expand.
Scan, fold and unfold like a boss.
[docs]
from operator import add
from unpythonic import scanl, foldl, unfold, take
assert tuple(scanl(add, 0, range(1, 5))) == (0, 1, 3, 6, 10)
def op(e1, e2, acc):
return acc + e1 * e2
assert foldl(op, 0, (1, 2), (3, 4)) == 11 # we accept multiple input sequences, like Racket
def nextfibo(a, b): # *oldstates
return (a, b, a + b) # value, *newstates
assert tuple(take(10, unfold(nextfibo, 1, 1))) == (1, 1, 2, 3, 5, 8, 13, 21, 34, 55)Allow a lambda to call itself.
[docs]
from unpythonic import withself
fact = withself(lambda self, n: n * self(n - 1) if n > 1 else 1)
assert fact(5) == 120Break infinite recursion cycles.
[docs]
from typing import NoReturn
from unpythonic import fix
@fix()
def a(k):
return b((k + 1) % 3)
@fix()
def b(k):
return a((k + 1) % 3)
assert a(0) is NoReturnBuild number sequences by example. Slice general iterables.
[docs for s] [docs for islice]
from unpythonic import s, islice
seq = s(1, 2, 4, ...)
assert tuple(islice(seq)[:10]) == (1, 2, 4, 8, 16, 32, 64, 128, 256, 512)Memoize functions and generators.
[docs for memoize] [docs for gmemoize]
from itertools import count, takewhile
from unpythonic import memoize, gmemoize, islice
# "memoize lambda": classic evaluate-at-most-once thunk
thunk = memoize(lambda: print("hi from thunk"))
thunk()
thunk()
@gmemoize # <-- important part
def primes(): # FP sieve of Eratosthenes
yield 2
for n in count(start=3, step=2):
if not any(n % p == 0 for p in takewhile(lambda x: x*x <= n, primes())):
yield n
assert tuple(islice(primes())[:10]) == (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)Make functional updates.
[docs]
from itertools import repeat
from unpythonic import fup
t = (1, 2, 3, 4, 5)
s = fup(t)[0::2] << tuple(repeat(10, 3))
assert s == (10, 2, 10, 4, 10)
assert t == (1, 2, 3, 4, 5)View list slices writably, re-slicably.
[docs]
from unpythonic import view
lst = list(range(10))
v = view(lst)[::2] # [0, 2, 4, 6, 8]
v[2:4] = (10, 20)
assert lst == [0, 1, 2, 3, 10, 5, 20, 7, 8, 9]
lst[2] = 42
assert v == [0, 42, 10, 20, 8]Focus on data flow in function composition.
[docs]
from unpythonic import piped, getvalue
double = lambda x: 2 * x
inc = lambda x: x + 1
x = piped(42) | double | inc | getvalue
assert x == 85Introduce expression-local variables.
[docs]
from unpythonic.syntax import macros, let
x = let[((a, 1), (b, 2)) in a + b]Introduce definition-local variables.
[docs]
from unpythonic.syntax import macros, dlet
@dlet((x, 0)) # let-over-lambda for Python
def count():
return x << x + 1 # `name << value` rebinds in the let env
assert count() == 1
assert count() == 2Code imperatively in an expression.
[docs]
from unpythonic.syntax import macros, do, local, delete
x = do[local[a << 21],
local[b << 2 * a],
print(b),
delete[b], # do[] local variables can be deleted, too
4 * a]
assert x == 84Apply tail call optimization (TCO) automatically.
[docs]
from unpythonic.syntax import macros, tco
with tco:
# expressions are automatically analyzed to detect tail position, too.
evenp = lambda x: (x == 0) or oddp(x - 1)
oddp = lambda x: (x != 0) and evenp(x - 1)
assert evenp(10000) is TrueCurry automatically, à la Haskell.
[docs]
from unpythonic.syntax import macros, curry
from unpythonic import foldr, composerc as compose, cons, nil, ll
with curry:
def add3(a, b, c):
return a + b + c
assert add3(1)(2)(3) == 6
mymap = lambda f: foldr(compose(cons, f), nil)
double = lambda x: 2 * x
assert mymap(double, (1, 2, 3)) == ll(2, 4, 6)Make lazy functions, a.k.a. call-by-need.
[docs]
from unpythonic.syntax import macros, lazify
with lazify:
def my_if(p, a, b):
if p:
return a # b never evaluated in this code path
else:
return b # a never evaluated in this code path
assert my_if(True, 23, 1/0) == 23
assert my_if(False, 1/0, 42) == 42Capture and use continuations (call/cc).
[docs]
from unpythonic import macros, continuations, call_cc
with continuations: # automatically enables also TCO
# McCarthy's amb() operator
stack = []
def amb(lst, cc):
if not lst:
return fail()
first, *rest = tuple(lst)
if rest:
remaining_part_of_computation = cc
stack.append(lambda: amb(rest, cc=remaining_part_of_computation))
return first
def fail():
if stack:
f = stack.pop()
return f()
# Pythagorean triples using amb()
def pt():
z = call_cc[amb(range(1, 21))] # capture continuation, auto-populate cc arg
y = call_cc[amb(range(1, z+1)))]
x = call_cc[amb(range(1, y+1))]
if x*x + y*y != z*z:
return fail()
return x, y, z
t = pt()
while t:
print(t)
t = fail() # note pt() has already returned when we call this.PyPI
pip3 install unpythonic --user
or
sudo pip3 install unpythonic
GitHub
Clone (or pull) from GitHub. Then,
python3 setup.py install --user
or
sudo python3 setup.py install
Uninstall
Uninstallation must be invoked in a folder which has no subfolder called unpythonic, so that pip recognizes it as a package name (instead of a filename). Then,
pip3 uninstall unpythonic
or
sudo pip3 uninstall unpythonic
All original code is released under the 2-clause BSD license.
For sources and licenses of fragments originally seen on the internet, see AUTHORS.
Thanks to TUT for letting me teach RAK-19006 in spring term 2018; early versions of parts of this library were originally developed as teaching examples for that course. Thanks to @AgenttiX for feedback.
The trampoline implementation of unpythonic.tco takes its remarkably clean and simple approach from recur.tco in fn.py. Our main improvements are a cleaner syntax for the client code, and the addition of the FP looping constructs.
Another important source of inspiration was tco by Thomas Baruchel, for thinking about the possibilities of TCO in Python.
Python clearly wants to be an impure-FP language. A decorator with arguments is a curried closure - how much more FP can you get?
-
Awesome Functional Python, especially a list of useful libraries. Some picks:
-
fn.py: Missing functional features of fp in Python (actively maintained fork). Includes e.g. tail call elimination by trampolining, and a very compact way to recursively define infinite streams.
-
more-itertools: More routines for operating on iterables, beyond itertools.
-
boltons: Like builtins, but boltons. Includes yet more itertools, and much more.
-
pyrsistent: Persistent/Immutable/Functional data structures for Python
-
pampy: Pattern matching for Python (pure Python, no AST transforms!)
-
-
List of languages that compile to Python including Hy, a Lisp (in the Lisp-2 family) that can use Python libraries.
Old, but interesting: