๐Ÿ 

ZefOps basics

map, filter, reduce, apply โ€” the core four.

There are ~500 ZefOps in the box. You don't need to memorize them all. You need to learn the shapes โ€” because once you know the shape, the docs tell you the rest.

mental model #4 โ€” shape first

Every ZefOp has a shape:

input shape โ†’ op โ†’ output shape

For example: map(f) takes a list, returns a list. reduce(f) takes a list, returns one value. filter(p) takes a list, returns a shorter list.

Match shapes and the pipe just works.

map โ€” transform each element

[1, 2, 3] | map(add(1)) | collect      # [2, 3, 4]
['a', 'b'] | map(to_upper_case) | collect  # ['A', 'B']
[1, 4, 9] | map(str) | collect          # ['1', '4', '9']

Classic functional map. One thing in โ†’ one thing out, preserving the shape of the collection.

map with Z-expressions

Remember Z? It's a placeholder that makes predicates and small transforms look like math:

[1, 2, 3, 4] | map(Z * Z) | collect         # [1, 4, 9, 16]
[1, 2, 3] | map(Z + 10) | collect           # [11, 12, 13]
[{'x':1}, {'x':5}] | map(Z['x']) | collect   # [1, 5]

apply โ€” transform ONE value

apply is the singular sibling of map:

5 | apply(Z * Z) | collect               # 25
'hello' | apply(to_upper_case) | collect  # 'HELLO'
when to use map

You have a list and want to transform each element.

when to use apply

You have a single value and want to run a function on it.

the killer feature: apply with a dict or list

Pass a dict of functions to fan out:

5 | apply({
    'plus_one': add(1),
    'doubled':  multiply(2),
    'squared':  Z * Z,
}) | collect
# {'plus_one': 6, 'doubled': 10, 'squared': 25}

Or a list:

5 | apply([add(1), multiply(2), Z * Z]) | collect
# [6, 10, 25]

map + dict is powerful

map combined with apply({...}) gives you "for each thing, compute several summaries":

numbers = [10, 20, 30]
numbers | map(apply({
    'val': Z,
    'sqr': Z * Z,
    'bin': bin,
})) | collect
# [{'val':10,'sqr':100,'bin':'0b1010'},
#  {'val':20,'sqr':400,'bin':'0b10100'}, ...]

filter โ€” keep only matching elements

[1, 2, 3, 4, 5] | filter(Z > 2) | collect    # [3, 4, 5]

[{'age': 20}, {'age': 15}] | filter(F.age >= 18) | collect
# [{'age': 20}]

# filter by type
[1, 'a', 2.5, 'b'] | filter(String) | collect     # ['a', 'b']

Filter accepts a predicate. The predicate can be:

reduce โ€” fold into one value

[1, 2, 3, 4] | reduce(add) | collect       # 10
[2, 3, 4] | reduce(multiply) | collect     # 24
['a', 'b', 'c'] | reduce(add) | collect   # 'abc'

# with an initial value:
[1, 2, 3] | reduce(add, 100) | collect  # 106
[] | reduce(add, 0) | collect             # 0 (safe for empty)
the four shapes, side by side
INPUT OP OUTPUT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ map [a,b,c] โ†’ map(f) โ†’ [f(a), f(b), f(c)] apply a โ†’ apply(f) โ†’ f(a) filter [a,b,c] โ†’ filter(p) โ†’ [x for x in [a,b,c] if p(x)] reduce [a,b,c] โ†’ reduce(f) โ†’ f(f(a,b), c)

scan โ€” reduce, but keep the intermediates

[1, 2, 3, 4] | scan(add) | collect
# [1, 3, 6, 10]   โ€” running total

[1, 2, 3] | scan(add, 100) | collect
# [100, 101, 103, 106]

Super useful for running totals, cumulative counts, moving windows.

the "container" trio

Often you want to grab one thing from a collection:

[1, 2, 3] | first | collect     # 1
[1, 2, 3] | last | collect      # 3
[1, 2, 3] | nth(1) | collect    # 2 (0-indexed)
[1, 2, 3] | length | collect    # 3
[3, 1, 2] | sort | collect      # [1, 2, 3]
[1, 1, 2] | unique | collect    # [1, 2]
[[1,2],[3]] | flatten | collect  # [1, 2, 3]

reading values out of dicts: the F operator

F.field is shorthand for "grab the field attribute/key":

person = {'name': 'Alice', 'age': 30}
person | F.name | collect     # 'Alice'
# chain F.s for deep fields
data | F.user | F.address | F.city | collect

You can use F.xxx inside other ops:

people = [{'name':'a','age':20}, {'name':'b','age':15}]

people | filter(F.age >= 18) | map(F.name) | collect
# ['a']

a bigger worked example

Let's combine what we have. Given a list of orders, find the top 3 customers by total spend.

orders = [
    {'customer': 'A', 'amount': 100},
    {'customer': 'B', 'amount': 50},
    {'customer': 'A', 'amount': 200},
    {'customer': 'C', 'amount': 75},
    {'customer': 'B', 'amount': 300},
    {'customer': 'A', 'amount': 25},
]

top3 = (
    orders
    | group_by(F.customer)                # {'A':[...], 'B':[...], 'C':[...]}
    | items                               # [('A',[...]), ('B',[...]), ('C',[...])]
    | map(apply_functions([
        identity,                          # keep customer name
        map(F.amount) | reduce(add),       # sum amounts
      ]))                                 # [('A', 325), ('B', 350), ('C', 75)]
    | sort_by(nth(1), descending=True)
    | take(3)
    | collect
)
# => [('B', 350), ('A', 325), ('C', 75)]

your turn

Given scores = [72, 85, 90, 68, 74, 91, 88], write a pipeline that returns the average of the passing scores (โ‰ฅ 70).

show solution
scores | filter(Z >= 70) | apply({
    'sum': reduce(add),
    'n':   length,
}) | apply(Z['sum'] / Z['n']) | collect
# => 83.333...

the quick reference

opinput โ†’ outputone-liner
map(f)[a] โ†’ [f(a)]transform each
apply(f)a โ†’ f(a)transform one
filter(p)[a] โ†’ [a where p(a)]keep matching
reduce(f)[a] โ†’ afold
scan(f)[a] โ†’ [a]fold with history
first / last / nth(i)[a] โ†’ apick one
length[a] โ†’ Intcount
sort / sort_by(k)[a] โ†’ [a]order
unique[a] โ†’ [a]dedupe
flatten[[a]] โ†’ [a]one level
take(n) / drop(n)[a] โ†’ [a]slice
group_by(k)[a] โ†’ {k: [a]}bucket
reverse[a] โ†’ [a]flip
F.xdict โ†’ valueproject field
loga โ†’ aprint & pass through (debug!)

when in doubt: log

Insert | log anywhere in a pipeline to see the value at that point:

data | op1 | log | op2 | log | op3 | collect

Prints ๐Ÿชต: <value> at each log and passes the value through unchanged. Best debugger Zef has.

Next up: the fancier ops โ€” match, rearrange, and the full power of Z. โ†’