Skip to content

Commit cdc0b2a

Browse files
committed
itertools: add collect for associative containers
1 parent 536f1e0 commit cdc0b2a

File tree

2 files changed

+113
-31
lines changed

2 files changed

+113
-31
lines changed

lib/pure/itertools.nim

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,17 @@ Descartes is a famous philosopher from France.
9595
## ----------------------
9696
##
9797
## **Adaptors** return a new iterable and can be chained indefinitely:
98-
## `map`_, `mapIt`_, `filter`_, `filterIt`_, `skip`_, `skipWhile`_,
98+
## `map<#map.t,iterable[T],proc(T)>`_, `mapIt`_, `filter`_, `filterIt`_, `skip`_, `skipWhile`_,
9999
## `skipWhileIt`_, `take`_, `takeWhile`_, `takeWhileIt`_, `stepBy`_,
100100
## `enumerate`_, `group`_, `flatten`_.
101101
##
102102
## **Consumers** drive evaluation and return a plain value:
103-
## `collect`_, `fold`_, `foldIt`_, `sum`_, `product`_, `count`_,
103+
## `collect<#collect.t,iterable[T],Natural>`_,
104+
## `fold<#fold.t,iterable[T],U,proc(sinkU,T)>`_, `foldIt`_, `sum`_, `product`_, `count`_,
104105
## `min`_, `max`_, `any`_, `anyIt`_, `all`_, `allIt`_,
105-
## `find`_, `findIt`_, `position`_, `positionIt`_, `nth`_.
106+
## `find<#find.t,iterable[T],proc(T)>`_, `findIt<findIt.t,iterable[T]>`_,
107+
## `position<#position.t,iterable[T],proc(T)>`_,
108+
## `positionIt<#positionIt.t,iterable[T],untyped>`_, `nth`_.
106109
##
107110
## Adaptor templates named `...It` inject the `it` symbol in the inner
108111
## scope for the current element. `foldIt` also injects `acc` for the
@@ -170,8 +173,9 @@ runnableExamples:
170173
## -------
171174
##
172175
## #. Iterators can only be consumed once. Unlike a `seq`, an iterator has no
173-
## rewind. If you need to traverse the same data more than once, `collect`_
174-
## to a `seq` first and restart from `.items`.
176+
## rewind. If you need to traverse the same data more than once,
177+
## `collect<collect.t,iterable[T],Natural>`_ to a `seq` first and restart
178+
## from `.items`.
175179
##
176180
## #. `foldIt`_ expects an expression, not a statement. The argument must
177181
## evaluate to the new accumulated value, which is assigned back to `acc`
@@ -180,11 +184,11 @@ runnableExamples:
180184
## `proc(acc: var U; it: T)` instead.
181185
##
182186
## #. Closure iterators are not accepted by `iterable[T]`. A value of type
183-
## `iterator(): T {.closure.}` does not satisfy the `iterable[T]` type.
184-
## Only direct inline-iterator calls - including `.items`, `.pairs`, and
185-
## named iterators such as `countUp` or `split` - work as pipeline sources.
186-
## If you hold a closure iterator value, wrap it in a helper inline iterator
187-
## that yields from it to use itertools.
187+
## `iterator(): T {.closure.}` does not satisfy the `iterable[T]` type.
188+
## Only direct inline-iterator calls - including `.items`, `.pairs`, and
189+
## named iterators such as `countUp` or `split` - work as pipeline sources.
190+
## If you hold a closure iterator value, wrap it in a helper inline iterator
191+
## that yields from it to use itertools.
188192
##
189193
## See also
190194
## --------
@@ -250,9 +254,34 @@ macro genIter*[T](iter: iterable[T], body: varargs[untyped]): untyped =
250254
name()
251255

252256
type
257+
Comparable* = concept
258+
proc `<`(x, y: Self): bool
259+
Summable* = concept # Additive?
260+
proc `+`(a, b: Self): Self
261+
Multipliable* = concept # Multiplicative?
262+
proc `*`(a, b: Self): Self
253263
Iterable*[T] = concept
254264
iterator items(a: Self): T
255265

266+
Addable*[T] = concept
267+
proc add(x: var Self; a: T)
268+
Includable*[T] = concept
269+
proc incl(x: var Self; a: T)
270+
Pushable*[T] = concept
271+
proc push(x: var Self; a: T)
272+
273+
Growable*[T] = Addable[T] | Includable[T] | Pushable[T]
274+
## Matches any mutable container that supports adding elements via
275+
## `add`, `incl` or `push`.
276+
277+
AssociativeContainer*[K, V] = concept
278+
## Matches any mutable container that supports key-value assignment via
279+
## `[]=`. This includes `Table[K, V]`, `TableRef[K, V]`,
280+
## `OrderedTable[K, V]`, `strtabs.StringTableRef`
281+
## (with `K = string`, `V = string`), and any user-defined type that
282+
## provides a matching `[]=` operator.
283+
proc `[]=`(x: var Self; key: K; val: V)
284+
256285
#--------------------------------------------------------------------------
257286
# Adaptors
258287
#--------------------------------------------------------------------------
@@ -526,19 +555,11 @@ else:
526555
# Consumers
527556
#--------------------------------------------------------------------------
528557

529-
type
530-
Comparable* = concept
531-
proc `<`(x, y: Self): bool
532-
Summable* = concept # Additive?
533-
proc `+`(a, b: Self): Self
534-
Multipliable* = concept # Multiplicative?
535-
proc `*`(a, b: Self): Self
536-
537558
template collect*[T](iter: iterable[T]; capacityHint: Natural = 1): seq[T] =
538559
## Collects the elements of the iterator into a new sequence.
539560
##
540561
## For collecting into the user-specified type of container, see
541-
## `collect<#collect.t,iterable[T],typedesc[U]>`_.
562+
## `collect<#collect.t,iterable[T],typedesc[C]>`_.
542563
##
543564
## .. Note:: `capacityHint` is optional and can be used to provide an initial
544565
## capacity for the sequence.
@@ -552,34 +573,77 @@ template collect*[T](iter: iterable[T]; capacityHint: Natural = 1): seq[T] =
552573
val.add x
553574
val
554575

555-
template collect*[T, U](iter: iterable[T]; t: typeDesc[U]): U =
576+
template collect*[T; C: Growable[T]](iter: iterable[T]; toType: typeDesc[C]): C =
556577
## Collects the elements of the iterator into a new container.
557578
##
558579
## `collect` creates a new collection and fills it with the first available
559580
## proc of `add`, `incl` or `push`. The resulting collection is returned.
560581
##
561-
## .. Note:: The type `U` should be compatible with the elements in the iterator.
562-
## If `U` is a reference type, a new instance is created. Otherwise, the
563-
## default value of `U` is used.
582+
## .. Note:: The type `C` should be compatible with the elements in the iterator.
583+
## If `C` is a reference type, a new instance is created. Otherwise, the
584+
## default value of `C` is used.
564585
##
565586
runnableExamples:
566587
let nums = [1, 2, 3, 4, 5]
567588
let minusOne = nums.items.mapIt(it - 1).collect(seq[int])
568-
assert minusOne == @[0, 1, 2, 3, 4]
569-
# TODO? Replace `when compiles` with specialisation on concepts
570-
var acc = when t is ref:
571-
new t
589+
doAssert minusOne == @[0, 1, 2, 3, 4]
590+
591+
import std/stats
592+
let rs = [10.0, 20.0, 30.0].items.collect(RunningStat)
593+
doAssert rs.sum == 60.0
594+
doAssert rs.mean == 20.0
595+
var acc = when C is ref:
596+
new C
572597
else:
573-
default t
598+
default C
574599
for x in iter:
575-
when compiles(acc.add(default(typeOf(T)))):
600+
when acc is Addable:
576601
acc.add x
577-
elif compiles(acc.incl(default(typeOf(T)))):
602+
elif acc is Includable:
578603
acc.incl x
579-
elif compiles(acc.push(default(typeOf(T)))):
604+
elif acc is Pushable:
580605
acc.push x
581606
acc
582607

608+
template collect*[K, V; C: AssociativeContainer[K, V]](
609+
iter: iterable[(K, V)]; toType: typedesc[C]): C =
610+
## Collects a `(key, value)` iterator into an associative container.
611+
##
612+
## Each element yielded by `iter` must be a 2-tuple `(K, V)`.
613+
## A fresh instance of `C` is created and populated by assigning each
614+
## tuple as `container[key] = value`.
615+
##
616+
## `C` must satisfy `AssociativeContainer[K, V]<#AssociativeContainer>`_,
617+
## meaning it provides ``proc \`[]=\`(c: var C; key: K; val: V)``.
618+
## All standard library table types qualify.
619+
##
620+
runnableExamples:
621+
import std/[tables, strutils]
622+
623+
# Collect (word, length) pairs into a Table.
624+
let wordLens = ["one", "two", "three"].items
625+
.mapIt((it, it.len))
626+
.collect(Table[string, int])
627+
doAssert wordLens["three"] == 5
628+
629+
# Build a reverse-lookup table from an enum.
630+
type Color = enum Red, Green, Blue
631+
let byName = [Red, Green, Blue].items
632+
.mapIt(($it, it))
633+
.collect(Table[string, Color])
634+
doAssert byName["Green"] == Green
635+
636+
# Collect into an OrderedTable, preserving insertion order.
637+
import std/algorithm
638+
let ordered = [(3, "c"), (1, "a"), (2, "b")].items
639+
.collect(OrderedTable[int, string])
640+
doAssert ordered.values.collect() == @["c", "a", "b"]
641+
642+
var acc = when C is ref: new C else: default(C)
643+
for (k, v) in iter:
644+
acc[k]=v
645+
acc
646+
583647
template fold*[T, U](iter: iterable[T]; init: U; fn: proc(acc: sink U; it: T): U): U =
584648
## Accumulates the values of the iterator using an accumulation function `fn`.
585649
## This operation is also commonly known as "reduce" and is useful for

tests/stdlib/titertools.nim

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,24 @@ block test_collectToSpecificContainers:
8888
doAssert ints.items.collect(seq[int]) == @[-2, -1, 1, 3, -4, 5]
8989
doAssert strs.items.collect(HashSet[string]) == toHashSet(strs)
9090
doAssert chars.items.collect(string) == "a.bCzd"
91+
type Foo[int] = object
92+
v*: seq[int]
93+
proc push(f: var Foo; val: int) {.used.} =
94+
f.v.add val
95+
let foo = ints.items.filterIt(it > 0).collect(Foo[int])
96+
doAssert foo.v == @[1, 3, 5]
97+
98+
block test_collectToAssociativeContainer:
99+
const kvs = [(3, "c"), (1, "a"), (2, "b")]
100+
let t = kvs.items.collect(OrderedTable[int, string])
101+
doAssert t.keys.collect(3) == @[3, 1, 2]
102+
103+
type Foo[char, int] = object
104+
v: array[char, int]
105+
proc `[]=`(c: var Foo[char, int]; k: char; v: int) {.used.} =
106+
c.v[k] = v
107+
let f = kvs.items.mapIt((it[1][0], it[0])).collect(toType = Foo[char, int])
108+
doAssert f.v['\0'] == 0 and f.v['a'] == 1 and f.v['b'] == 2 and f.v['c'] == 3
91109

92110
block test_minMax:
93111
doAssert ints.items.min() == -4

0 commit comments

Comments
 (0)