Containers#
In Python a container is something that contains something. Containers may be sequences, sets or mappings. Thus a collection is an abstraction of “something” that:
May contain something
Sequences are iterable
Collections have a size
We usually talk about generic container types such as List[T]
, Set[T]
,
Tuple[T,...]
. But we can also imagine taking the abstraction to a higher-order making
the left side generic as well, e.g Something[T]
. What do types of Something
have in
common?
A something within a something
A container is really just some kind of box that you can pull values out of. Can values be pushed out of a container?
Mapping#
A mapping object maps immutable values to arbitrary objects. There is both Mapping
and
MutableMapping
. The most known mutable mapping is the dict
type.
Sequence#
A sequence is an iterable container such as List
, Tuple
, str
, …
Immutable data types#
Immutable data types are important in functional programming. Immutable means that it’s
not possible to make any changes after the type have been created. Most data structures
in Python are mutable such as List
and Dict
, but Python also have a few immutable
data types:
Strings
Tuples
Iterable
The advantages of immutable data types are:
Thread-safe. Multiple threads cannot modify or corrupt the state.
Safe to share and reuse
Easy to reason about. Reduces the cognitive load
Easier to debug
Expression extends Python with a couple of more immutable data types:
Block#
A Block is an immutable List type. The implementation is based on the already immutable tuple type but gives it a list feeling and lots of functions and methods to work with it.
from expression.collections import Block
xs = Block.of_seq(range(10))
print(xs)
ys = xs.cons(10)
print(ys)
zs = xs.tail()
print(zs)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Map#
The Expression Map module is an immutable Dict type. The implementation is based on map type from F# and uses a balanced binary tree implementation.
from expression.collections import Map
items = dict(a=10, b=20).items()
xs = Map.of_seq(items)
print(xs)
ys = xs.filter(lambda k, v: v>10)
print(ys)
map [("a", 10); ("b", 20)]
map [("b", 20)]
Functions are Containers#
It might not be obvious at first, but functions can also be containers. This is because values might be stored in function closures. That means that a value might be visible in the scope of the function.
A closure is a poor man’s object. An object is a poor man’s closure.
In functional programming we often use function arguments to store values instead of objects
def hat(item):
def pull():
return item
return pull
small_hat = lambda item: lambda pull: item
pull = hat("rabbit")
pull()
'rabbit'
List out of lambda (LOL)#
We can even create a fully functional list implementation using only functions:
empty_list = None
def prepend(el, lst):
return lambda selector: selector(el, lst)
def head(lst):
return lst(lambda h, t: h)
def tail(lst):
return lst(lambda h, t: t)
def is_empty(lst):
return (lst == empty_list)
a = prepend("a", prepend("b", empty_list))
assert("a" == head(a))
assert("b" == head(tail(a)))
assert(tail(tail(a))==empty_list)
assert(not is_empty(a))
assert(is_empty(empty_list))
print("all tests are green!")
LOL (more compact)#
A list can be created using only lambda functions:
empty_list = None
prepend = lambda el, lst: lambda selector: selector(el, lst)
head = lambda lst: lst(lambda h, t: h)
tail = lambda lst: lst(lambda h, t: t)
is_empty = lambda lst: lst is empty_list
a = prepend("a", prepend("b", empty_list))
assert("a" == head(a))
assert("b" == head(tail(a)))
assert(tail(tail(a))==empty_list)
assert(not is_empty(a))
assert(is_empty(empty_list))
print("all tests are green!")
Pull vs Push#
List, iterables, mappings, strings etc are what we call “pull” collections. This is
because we are actively pulling the values out of the collection by calling the next()
function on the Iterator.
iterable = [1, 2, 3]
iterator = iter(iterable) # get iterator
value = next(iterator)
print(value)
value = next(iterator)
print(value)
value = next(iterator)
print(value)
# value = next(iterator)
Push Collections#
A push collection is something that pushes values out of the collection. This can be seen as temporal (push) containers vs spatial (pull) collections. This collection is called an Observable and is the dual (or the opposite) of an Iterable.
An Iterable
have getter for getting an Iterator
(iter)
An Obserable
have a setter for setting an Observer
(subscribe)
An Iterator
have a getter for getting the next value (next)
An Observer
have a setter for setting the next value (on_next, or send)
Summarized:
Iterable is a getter-getter function
Observable is a setter-setter function
Let’s try to implement an Observable using only functions:
import sys
def observer(value):
print(f"got value: {value}")
def infinite():
def subscribe(obv):
for x in range(1000):
obv(x)
return subscribe
def take(count):
def obs(source):
def subscribe(obv):
n = count
def observer(value):
nonlocal n
if n > 0:
obv(value)
n -= 1
source(observer)
return subscribe
return obs
take(10)(infinite())(observer)
def pipe(arg, *fns):
for fn in fns:
arg = fn(arg)
return arg
observable = pipe(
infinite(), # infinite sequence of values
take(10) # take the first 10
)
observable(observer)
RxPY is an implementation of Observable
and
aioreactive project is an implementation of
AsyncObservable
.