Once a young man approached the Prophet Muhammad and asked him for some advice. The Prophet replied, “Do not become angry,” and he repeated this three times
iterate faster ... 🏎️. Write higher-order functions, get its imperative style at the compile time!
Writing a full nested loop is a boring task, std/sequtils
creates lots of temporary seq
s, iterutils uses closure iterators (which is slow).
iterrr
uses the ultimate power of meta-programming to bring you the speed that Nim programmers deserve.
"hello".pairs |>
filter((i, _) => i > 1)
.map((_, ch) => ch)
.strjoin() ## llo
block:
template iterrrFn3(_; ch): untyped {.dirty.} =
ch
template iterrrFn4(i; _): untyped {.dirty.} =
i > 1
var iterrrAcc2 = strjoinInit[typeof(iterrrFn3(
default(typeof("hello".pairs))[0], default(typeof("hello".pairs))[1]))]()
block mainLoop:
for li1 in "hello".pairs:
if iterrrFn4(li1[0], li1[1]):
block:
let li1 = iterrrFn3(li1[0], li1[1])
if not strjoinUpdate(iterrrAcc2, li1):
break mainLoop
strjoinFinalizer(iterrrAcc2)
There is 3 type of usage:
# predefined reducer
iterable |> entity1(_).entity2(_)...Reducer()
# custom reducer
iterable |> entity1(_).entity2(_)...reduce(loopIdents, accIdent = initial_value, [Finalizer]):
# update accIdent
# custom code
iterable |> entity1(_).entity2(_)...each(...loopIdents):
# do with loopIdents
- map :: similar to
mapIt
fromstd/sequtils
- filter :: similar to
filterIt
fromstd/sequtils
- breakif :: similar to
takeWhile
in functional programming languages but negative. - inject :: injects custom code
NOTE: you can chain as many map
/filter
/... as you want in any order, but there is only one reducer.
There are some predefined reducers in iterrr library:
toSeq
:: stores elements into aseq
count
:: counts elementssum
:: calculates summationmin
:: calculates minimummax
:: calculates maximumfirst
:: returns the first itemlast
:: returns the last itemany
:: similar toany
fromstd/sequtils
all
:: similar toall
fromstd/sequtils
toHashSet
:: stores elements into aHashSet
strJoin
:: similar tojoin
fromstd/strutils
toCountTable
:: similar totoCountTable
fromstd/tables
here's how you can get maximum x, when flatPoints
is: [x0, y0, x1, y1, x2, y2, ...]
let xmax = flatPoints.pairs |> filter(it[0] mod 2 == 0).map(it[1]).max()
# or
let xmax = countup(0, flatPoints.high, 2) |> map(flatPoints[it]).max()
NOTE: see more examples in tests/test.nim
using just it
in mapIt
and filterIt
is just ... and makes code a little unreadable.
- if there was no custom idents,
it
is assumed - if there was only 1 custom ident, the custom ident is replaced with
it
- if there was more than 1 custom idents,
it
is unpacked
Here's some examples:
(1..10) |> map( _ ) # "it" is available inside the "map"
(1..10) |> map(n => _ )
(1..10) |> map((n) => _ )
(1..10) |> map((a1, a2, ...) => _ )
(1..10) |> reduce((a1, a2, ...), acc = 2)
(1..10) |> each(a1, a2)
Custom idents work with both op:
and op()
style syntax:
(1..10).items.iterrr:
map: n => _
# or map n => _
...
(1..10).items.iterrr:
filter: (n, k) => _
# or filter (n, k) => _
...
example:
"hello".pairs |> filter((i, c) => i > 2).map((_, c) => ord c)
you have to specify the iterator for seq
and other iterable objects [HSlice
is an exception]
example:
let s = [1, 2, 3]
echo s |> map($it).toseq() # doesn't work
echo s.items |> map($it).toseq() # works fine
echo s.pairs |> map($it).toseq() # works fine
every reducer have: [let't name our custom reducer zzz
]
zzzInit[T](args...): ...
:: initializes the value of accumulator(state) :: must be generic.zzzUpdate(var acc, newValue): bool
:: updates the accumulator based onnewValue
, if returns false, the iteration stops.zzzFinalizer(n): ...
:: returns the result of the accumulator.
NOTE: see implementations in src/iterrr/reducers.nim
pattern:
ITER |> ...reduce(idents, acc = initial_value, [finalizer]):
update acc here
Notes:
- acc can be any ident like
result
oranswer
, ... - Finalizer:
- it's optional
- it's an experssion inside of it you have access to the
acc
ident - the default finalizer is
acc
ident
Example of searching for a number:
let element = (1..10) |> reduce(it, answer = none int, answer.get):
if your_condition(it):
answer = some MyNumber
break mainLoop
Note: if the item has not found, raises UnpackDefect
error as result of get
function in finalizer answer.get
.
My view is that a lot of the time in Nim when you're doing filter or map you're just going to operate it on afterwards :: @beef331 AKA beef.
I'm agree with beef. it happens a lot.
you can do it with each(arg1, arg2,...)
. [arguments semantic is the same as custom idents]
(1..10) |> filter(it in 3..5).each(num):
echo num
if num < 7:
break mainLoop
Note: mainLoop
is the main loop block
adapters are inspired from implmentation of iterators in Nim. TODO: explain more
Limitations: you have to import the dependencies of adapters in order to use them.
Built-in adapter:
group
window
cycle
flatten
drop
take
Usage: example:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
matrix.items |> flatten().map(-it).cycle(11).group(4).toseq()
result:
@[
@[-1, -2, -3, -4],
@[-5, -6, -7, -8],
@[-9, -1, -2]
]
see tests
/test.nim
for more.
Define your custom adapter:
for now the name of loop iterator are limited to it
.
TODO;
see src
/iterrr
/adapters
.
don't like |>
operator? no problem! use iterrr
keyword:
pattern:
iterable.iterator.iterrr:
filter(...)
map(...)
reducer(...)
reduce(...)/each(...):
# code ...
example:
let points = @[(1, 2), (-3, 4), (12, 3), (-1, -6), (5, -9)]
points.items.iterrr:
map (x, y) => x # or map((x, y) => x)
filter it > 0 # or filter(it > 0)
each n: # or each(n):
echo n
Both the |>
operator and iterrr
macro can be nested.
Examples:
matrix.pairs |> map((ia, a) => (
a.pairs |> map((ib, _) => (ia, ib)).toseq()
)).toseq()
matrix.pairs.iterrr:
map: (ia, a) => a.pairs.iterrr:
map: (ib, _) => (ia, ib)
toseq()
toseq()
use -d:iterrrDebug
flag to see generated code.
- using brackets for defining custom idents is no longer supported.
iterrr
targets the same problem as zero_functional
, while being better at extensibility.
Is it fully "zero cost" like zero_functional
though?
well NO, most of it is because of reducer update calls, however the speed difference is soooo tiny and you can't even measure it. I could define all reducer updates as
template
instead of function but IMO it's better to have call stack when you hit errors ...
writing macro is kind of addicting... :: PMunch