Skip to content

Instantly share code, notes, and snippets.

@techieji
Created August 17, 2020 22:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save techieji/539c62d1409af3a43ed2a2251cffc3b0 to your computer and use it in GitHub Desktop.
Save techieji/539c62d1409af3a43ed2a2251cffc3b0 to your computer and use it in GitHub Desktop.
elixpy.py
"A set of modules which emulate the standard library of Elixir in Python."
__version__ = '0.0.2'
import time
import math
import operator
from inspect import signature
import random
class func:
"A container for functions which follow the functional programming paradigm."
class msg:
"A way to raise errors without terminating the program. Not yet complete."
ok = True
error = False
def __init__(self, msg, value):
self.msg = msg
self.value = value
def __str__(self):
return (self.msg, self.value).__str__()
def __repr__(self):
return (self.msg, self.value).__repr__()
def nofail(function):
"Create a function which doesn't fail when it raises an error."
def f(*args, **kwargs):
try:
return func.msg(func.msg.ok, function(*args, **kwargs))
except Exception as e:
return func.msg(func.msg.error, e)
return f
class thunk:
"A class used for partial function application and deferring function calls"
def __init__(self, func: callable, *args, **kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
self.num = len(signature(func).parameters)
def __call__(self, *args, **kwargs):
num = len(self.args) + len(args) + len(self.kwargs) + len(kwargs)
if num == self.num:
return self.func(*self.args, *args, **self.kwargs, **kwargs)
elif num < self.num:
return func.thunk(self, *self.args, *args, **self.kwargs, **kwargs)
else:
raise TypeError(f"Exactly {self.num} arguments are needed ({num} given)")
class enum(list):
"A container for eager functions which work with enumerables."
def pipe(self, fn, *args, **kwargs):
return fn(self, *args, **kwargs)
def chunk_by(i, fn):
"""
Returns an enumerable which contains sublists. These sublists all
contain values which when passed into the function, produce the
same result. Order is preserved. An example:
>>> func.enum.chunk_by([0, 1, 2, -1, -2], lambda x: x > 0)
[[0], [1, 2], [-1, -2]]
"""
i = iter(i)
ans = [[next(i)]]
for x in i:
lastres = fn(ans[-1][0])
if fn(x) != lastres:
ans.append([])
ans[-1].append(x)
return ans
def dedup_by(i, fn):
"""
Deletes duplicates with a function. The function maps a value to
another value which can be used to determine if two values are
different. Does not mutate the original enumerable. An example:
>>> func.enum.dedup_by([1, 2, 3, 4], lambda x: x % 2)
[1, 2]
"""
fncalls = set()
ret = []
for x in i:
if fn(x) not in fncalls:
fncalls.add(fn(x))
ret.append(x)
return ret
def dedup(i):
"""
Deletes duplicates. Does not mutate the original enumerable.
An example:
>>> func.enum.dedup([1, 1, 2, 2, 3, 5, 3])
[1, 2, 3, 5]
"""
return func.enum.dedup_by(i, lambda x: x)
def rotate(l: list, shift: int = 1) -> iter:
"""
Rotates all the elements in an enumerable by the specified shift.
The shift defaults to 1. Some examples:
>>> func.enum.rotate([1, 2, 3, 4])
[4, 1, 2, 3]
>>> func.enum.rotate([1, 2, 3, 4], shift=2)
[3, 4, 1, 2]
>>> func.enum.rotate([1, 2, 3, 4], shift=-1)
[2, 3, 4, 1]
"""
return l[-shift:] + l[:-shift]
def find(l: list, index: int) -> object:
"""
Indexes into a enumerable. For a list, is equivalent to l[i]. This
function works with any enumerable. An example:
>>> func.enum.find([1, 3, 2, 4], 1)
3
"""
l = iter(l)
while index:
next(l)
index -= 1
return next(l)
def flip(l: list, i1: int, i2: int = None) -> list:
"""
Replaces two element of a list with each other. The second index
defaults to the first index plus one. Some examples:
>>> func.enum.flip([1, 2, 3, 4], 1, 2)
[1, 3, 2, 4]
>>> func.enum.flip([1, 2, 3, 4], 1)
[1, 3, 2, 4]
"""
if i2 == None:
i2 = i1 + 1
i1, i2 = sorted([i1, i2])
t = type(l)
return l[:i1] + t([l[i2]]) + l[i1 + 1:i2] + t([l[i1]]) + l[i2 + 1:]
def map(l: iter, fn: callable) -> iter:
"""
Maps a function over an enumerable. Is eager and does not accept
more than one enumerable. Note: the built-in map is a lazy version.
An example:
>>> func.enum.map([1, 2, 3, 4], lambda x: x + 2)
[3, 4, 5, 6]
"""
return type(l)([fn(x) for x in l])
def flatmap(fn: callable, *ls) -> iter:
"""
Maps a function over an enumerable and then flattens the result.
Accepts multiple enumerables as arguments. Some examples:
>>> fun = lambda x: (x, -x)
>>> func.enum.flatmap(fun, [1, 2, 3])
[1, -1, 2, -2, 3, -3]
>>> fun = lambda x, y: (x + y, x - y)
>>> func.enum.flatmap(fun, [1, 2, 3], [3, 2, 1])
[4, -2, 4, 0, 4, 2]
"""
ret = []
for x in zip(*ls):
try:
ret += list(fn(*x))
except TypeError:
ret += [fn(*x)]
return type(ls[0])(ret)
def filter(fn: callable, l: iter) -> iter:
"""
Eager variation of the built-in function `filter`. An example:
>>> func.enum.filter(lambda x: x > 0, [-2, -1, 0, 1, 2])
[1, 2]
"""
return type(l)([x for x in l if fn(x)])
def flatten(l) -> iter:
"""
Flattens a list one level. Some examples:
>>> func.enum.flatten([[1, 2], [3, 4, 5]])
[1, 2, 3, 4, 5]
"""
return func.enum.flatmap(lambda x: x, l)
def foldr(f: callable, acc: object, l: iter) -> object:
"""
Folds the given enumerable rightward with the given accumulator and
given function. An example:
>>> func.enum.foldr(lambda x, y: x - y, 0, range(1, 11))
-5
"""
if len(l) == 1:
return f(l[0], acc)
else:
return f(l[0], func.enum.foldr(f, acc, l[1:]))
def foldl(f: callable, acc: object, l: iter) -> object:
"""
Folds the given enumerable leftward with the given accumulator and
given function. An example:
>>> func.enum.foldl(lambda x, y: x - y, 0, range(1, 11))
-55
"""
for x in l[::-1]:
acc = f(acc, x)
return acc
def reject(fn: callable, l: iter) -> iter:
"""
Filters out the elements in the enumerable for which the function
returns `True`. An example:
>>> func.enum.reject(lambda x: x > 0, [-2, -1, 0, 1, 2])
[-2, -1, 0]
"""
return func.enum.filter(lambda x: not(fn(x)), l)
def scanr(f: callable, acc: object, l: iter) -> iter:
def fn(x, acc):
return acc + [f(acc, x)]
return func.enum.foldr(fn, acc, l)
def scanl(f: callable, acc: object, l: iter) -> iter:
def fn(x, acc):
return acc + [f(acc, x)]
return func.enum.foldl(fn, acc, l)
def zip(*ls):
"""
Eager version of the built-in function `zip`. An example:
>>> func.enum.zip([1, 2, 3], [3, 2, 1])
[(1, 3), (2, 2), (3, 1)]
"""
return type(ls[0])(zip(*ls))
def unzip(e: iter):
"""
Unzips a zipped enumerable. Returns a list of lists containing the
original lists. An example:
>>> z = zip([1, 2, 3], [3, 2, 1])
>>> func.enum.unzip(z)
[[1, 2, 3], [3, 2, 1]]
"""
e = list(e)
ret = [[] for x in range(len(e[0]))]
for x in e:
for i in range(0, len(x)):
ret[i].append(x[i])
return ret
def take(e: iter, num: int):
"""
Takes the first `num` elements from the given enumerable.
For lists, equivalent to l[0:n - 1]. An example:
>>> func.enum.take([1, 2, 3, 4, 5], 2)
[1, 2]
"""
e = iter(e)
i = 0
l = []
for x in e:
if i == num:
break
l.append(x)
i += 1
return l
class stream:
"""
A class for dealing with large, possibly infinite collections. Note
that this class has much more support for infinite collections than
finite ones. Can be initalized with a rule or with one of the functions
which return a stream as a result. Streams are lazy and thus defer
operations until needing to do them. This means that streams need to
iterate over all the elements only once. Streams are a good choice when
you need to do many operations on a large set of data.
"""
def __init__(self, rule: callable):
"""
Takes a callable to initalize. The callable takes one argument, the
index and returns the element at that index.
"""
self.rule = rule
self.i = -1
self.iter = self
def __next__(self):
self.i += 1
return self.rule(self.i)
def __iter__(self):
self.i = -1
return self
def pipe(self, fun, *args, **kwargs):
return fun(self, *args, **kwargs)
def find(s, i):
"""
Locates the element of the given stream at the specified index.
An example:
>>> s = func.stream(lambda x: x + 1)
>>> func.stream.find(s, 5)
6
"""
return s.rule(i)
def from_iter(it: iter):
"""
Converts an enumerable into a stream. Any values beyond the final
element of the enumerable will be given as None.
"""
def f(i):
x = list(it)
index = 0
try:
return x[i]
except IndexError:
return None
ret = func.stream(f)
ret.iter = it
return ret
def chunk_by(s: iter, fn: callable):
"""
Takes a stream and a callable and returns a stream which emits
chunks for which the result of the function called on them is the
same. See the doc string of func.enum.chunk_by.
"""
def f(i):
enumed = []
index = 1
while len(enumed) < i + 1:
enumed = func.enum.chunk_by(func.enum.take(s, index), fn)
index += 1
return enumed[-2]
return func.stream(f)
def concat(s1, s2):
"""
Joins two streams together. It's lazy, like all other functions in
this module, so if the first stream is infinite, nothing will
happen. Isn't implemented correctly.
"""
raise NotImplementedError
def f(i):
if func.stream.find(s1, i) == None:
return func.stream.find(s2, i)
else:
return func.stream.find(s1, i)
return func.stream(f)
def cycle(it):
"""
Cycles through an enumerable. For example, cycling over [1, 2, 3]
will produce the stream [1, 2, 3, 1, 2, 3, 1, ...]. An example:
>>> s = func.stream.cycle([1, 2, 3])
>>> func.enum.find(s, 5)
3
"""
it = func.stream.from_iter(it)
def f(i):
return func.stream.find(it, i % len(it.iter))
return func.stream(f)
def dedup_by(it, fn):
"""
Returns a stream which only emits elements if they are different
from the last element according to the transformation function
given. See the doc string for func.enum.dedup_by
"""
last = None
offset = 0
def f(i):
while fn(func.stream.find(it, i + offset)) == last:
offset += 1
ret = func.stream.find(it, i + offset)
last = fn(ret)
return ret
return func.stream(f)
def dedup(it):
"See the doc string for func.enum.dedup"
return func.stream.dedup_by(it, lambda x: x)
def map(s, fn):
"""
A lazy map. Note that this function provides exactly the same
functionality as the built-in map function.
"""
def f(i):
return fn(s.rule(i))
return func.stream(f)
def iterate(start, fn): # look at the memory constant impl
"""
Repeatedly applies the given function i times to the starting value
to get the i-th element. Produces a string. Note: this isn't
implemented correctly yet.
"""
raise NotImplementedError
def f(i):
func = lambda x: x
for x in range(i):
func = lambda x: fn(func(x))
return func(i)
return func.stream(f)
def protocol(*functions) -> callable:
"""
A decorator which delegates to other function by using the types of the
argument. When no function matches, calls the code in the decorated
function. Make sure that the function is annotated.
"""
def dec(function) -> callable:
def f(*args):
types = list(map(type, args))
for func in functions:
if list(func.__annotations__.values()) == types:
return func(*args)
return function(*args)
return f
return dec
def guard(condition: callable, default = None) -> callable:
"""
Decorator which calls the function only if the condition is satisfied.
Otherwise, it defaults to default, which defaults to None.
"""
def dec(f: callable) -> callable:
class F:
guardcon = condition
def __call__(self, *args, **kwargs):
if condition(*args, **kwargs):
return f(*args, **kwargs)
else:
return default
return F()
return dec
def compose(*functions) -> callable:
"""
Composes the functions given. Is not robust. Use at your own risk.
"""
def f(*args, **kwargs):
if len(functions) == 1:
return functions[0](*args, **kwargs)
else:
return functions[0](func.compose(*functions[1:])(*args, **kwargs))
return f
def argoverload(*functions) -> callable:
"""
Decorator which calls a different function based off of the numer of
arguments given. If nothing matches, calls the decorated function.
"""
def dec(function) -> callable:
def f(*args, **kwargs):
for x in functions:
if len(signature(x).parameters) == len(args) + len(kwargs):
return x(*args)
return function(*args)
return f
return dec
def memoize(size: int = None) -> callable:
"""
A simple memoizer. Saves already called function calls in order to
speed up future function calls. Saves up to `size` entries.
"""
def dec(function: callable) -> callable:
d = {}
def f(*args, **kwargs):
if size and len(d) > size - 1:
del d[random.choice(d)]
if args not in d.keys():
d[args] = function(*args, **kwargs)
return d[args]
return f
return dec
def rotateargs(function: callable, shift: int = 1):
"""
Rotates the arguments given. For example, if there exists a function
`f(a, b, c)` will be turned into `lambda a, b, c: f(c, a, b).
"""
def f(*args, **kwargs):
return function(*func.enum.rotate(args, shift), **kwargs)
return f
def flipargs(function: callable, i1: int = 0, i2: int = None) -> callable:
"""
Flips the specified arguments of the function.
"""
def f(*args, **kwargs):
return function(*func.enum.flip(args, i1, i2), **kwargs)
return f
def match(tree: list, values: list) -> dict:
"""
A pattern matching tool for Python. Can be used to destructure complex
data structure when you know the structure of the data. An example:
>>> func.match(['a', ('b', 'c')], [1, (2, 3)])
{'a': 1, 'b': 2, 'c': 3}
"""
d = {}
for t, v in zip(tree, values):
if hasattr(t, '__iter__') and type(t) == type(v):
d.update(func.match(t, v))
else:
d[t] = v
return d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment