3.1 Reducers
(require rebellion/streaming/reducer) | package: rebellion |
A reducer is an object that can combine a (possibly infinite) sequence of elements into a single result value. Reducers form the final step of a stream pipeline and specify how the output elements of the pipeline should be collected into a final result. To use a reducer, pass it as the #:into argument to the transduce function.
Reducers are state machines; performing a reduction involves starting the reducer to get an initial state, then consuming elements one at a time to transform the current state into a new, updated state. When no more elements are available, the reducer’s finisher is called to transform the final state into a result value. Optionally, a reducer may terminate the reduction early, before the sequence is fully consumed. This process is known as the reduction protocol: see make-reducer for a more concrete description of the protocol.
> (transduce (list 1 2 3) #:into into-sum) 6
> (transduce empty-list #:into into-sum) 0
> (transduce (in-range 10000) #:into into-sum) 49995000
value
> (transduce (list 2 3 4) #:into into-product) 24
> (transduce empty-list #:into into-product) 1
> (transduce (in-range 1 20) #:into into-product) 121645100408832000
value
> (transduce (list 'a 'b 'c) #:into into-count) 3
> (transduce "hello world" #:into into-count) 11
value
> (transduce "hello world" #:into into-first) (present #\h)
value
> (transduce "hello world" #:into nonempty-into-first) #\h
> (transduce "" #:into nonempty-into-first) nonempty-into-first: expected at least one element
value
> (transduce (list 4) #:into into-option) (present 4)
> (transduce empty-list #:into into-option) #<absent>
> (transduce (list 4 7) #:into into-option) into-option: expected at most one element
first element: 4
second element: 7
value
> (transduce (list 4) #:into into-only-element) 4
> (transduce (list 4 7) #:into into-only-element) into-only-element: expected exactly one element, but
multiple elements were received
first element: 4
second element: 7
> (transduce empty-list #:into into-only-element) into-only-element: expected exactly one element, but zero
elements were received
value
> (transduce "hello world" #:into nonempty-into-last) #\d
> (transduce "" #:into nonempty-into-last) nonempty-into-last: expected at least one element
> (transduce "hello world" #:into (into-nth 8)) (present #\r)
> (transduce "hello world" #:into (into-nth 20)) #<absent>
> (transduce "hello world" #:into (into-nth 0)) (present #\h)
> (transduce "battery" #:into (into-index-of #\e)) (present 4)
> (transduce "cat" #:into (into-index-of #\e)) #<absent>
procedure
(into-index-where pred) → (reducer/c any/c (option/c natural?))
pred : predicate/c
> (transduce "hello world" #:into (into-index-where char-whitespace?)) (present 5)
> (transduce "hello world" #:into (into-index-where char-numeric?)) #<absent>
procedure
(into-any-match? pred) → (reducer/c any/c boolean?)
pred : predicate/c
> (transduce "hello world" #:into (into-any-match? char-whitespace?)) #t
> (transduce "hello" #:into (into-any-match? char-whitespace?)) #f
> (transduce "" #:into (into-any-match? char-whitespace?)) #f
procedure
(into-all-match? pred) → (reducer/c any/c boolean?)
pred : predicate/c
> (transduce "hello" #:into (into-all-match? char-alphabetic?)) #t
> (transduce "hello world" #:into (into-all-match? char-alphabetic?)) #f
> (transduce "" #:into (into-all-match? char-alphabetic?)) #t
procedure
(into-none-match? pred) → (reducer/c any/c boolean?)
pred : predicate/c
> (transduce "hello" #:into (into-none-match? char-whitespace?)) #t
> (transduce "hello world" #:into (into-none-match? char-whitespace?)) #f
> (transduce "" #:into (into-none-match? char-whitespace?)) #t
> (transduce (in-range 5) #:into (into-for-each displayln))
0
1
2
3
4
procedure
(into-max [comparator #:key key-function])
→ (reducer/c any/c option?) comparator : comparator? = real<=> key-function : (-> any/c any/c) = values
procedure
(into-min [comparator #:key key-function])
→ (reducer/c any/c option?) comparator : comparator? = real<=> key-function : (-> any/c any/c) = values
> (transduce (in-range 1 10) #:into (into-max)) (present 9)
> (transduce (in-range 1 10) #:into (into-min)) (present 1)
> (transduce empty-list #:into (into-max)) #<absent>
Comparisons are performed with comparator (which defaults to a numeric comparison). If key-function is provided, it is used to extract the compared value from each element. When the sequence contains equivalent but distinct maximum or minimum elements, the first of them is returned.
> (transduce (list "goodbye" "cruel" "world") #:into (into-min string<=>)) (present "cruel")
(define-record-type gemstone (color weight)) (define gems (list (gemstone #:color 'red #:weight 5) (gemstone #:color 'blue #:weight 7) (gemstone #:color 'green #:weight 3) (gemstone #:color 'yellow #:weight 7)))
> (transduce gems #:into (into-max #:key gemstone-weight)) (present (gemstone #:color 'blue #:weight 7))
procedure
(nonempty-into-max [ comparator #:key key-function]) → reducer? comparator : comparator? = real<=> key-function : (-> any/c any/c) = values
procedure
(nonempty-into-min [ comparator #:key key-function]) → reducer? comparator : comparator? = real<=> key-function : (-> any/c any/c) = values
> (transduce (in-range 1 10) #:into (nonempty-into-max)) 9
> (transduce (in-range 1 10) #:into (nonempty-into-min)) 1
> (transduce empty-list #:into (nonempty-into-max)) nonempty-into-max: expected at least one element
Comparisons are performed with comparator (which defaults to a numeric comparison). If key-function is provided, it is used to extract the compared value from each element. When the sequence contains equivalent but distinct maximum or minimum elements, the first of them is returned.
> (transduce (list "goodbye" "cruel" "world") #:into (nonempty-into-min string<=>)) "cruel"
(define-record-type gemstone (color weight)) (define gems (list (gemstone #:color 'red #:weight 5) (gemstone #:color 'blue #:weight 7) (gemstone #:color 'green #:weight 3) (gemstone #:color 'yellow #:weight 7)))
> (transduce gems #:into (nonempty-into-max #:key gemstone-weight)) (gemstone #:color 'blue #:weight 7)
procedure
(into-sorted? [ comparator #:key key-function #:descending? descending? #:strict? strict?]) → (reducer/c any/c boolean?) comparator : comparator? = real<=> key-function : (-> any/c any/c) = values descending? : boolean? = #false strict? : boolean? = #false
> (transduce (list 1 2 3 4 5) #:into (into-sorted?)) #t
If key-function is provided, it is applied to each element to extract a key and the keys are compared instead of the elements themselves.
> (transduce (list "cat" "dog" "horse" "zebra") #:into (into-sorted? #:key string-length)) #t
If strict? is true, the elements must be strictly ascending or descending —
> (transduce (list 1 2 2 3) #:into (into-sorted?)) #t
> (transduce (list 1 2 2 3) #:into (into-sorted? #:strict? #true)) #f
value
> (transduce (list #\h #\e #\l #\l #\o) #:into into-string) "hello"
value
> (transduce "Haikus are easy\nBut sometimes they don't make sense\nRefrigerator" #:into into-line) "Haikus are easy"
procedure
(join-into-string sep [ #:before-first before-first #:before-last before-last #:after-last after-last]) → (reducer/c immutable-string? immutable-string?) sep : immutable-string? before-first : immutable-string? = "" before-last : immutable-string? = sep after-last : immutable-string? = ""
> (transduce (in-range 1 10) (mapping number->immutable-string) #:into (join-into-string " + ")) "1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9"
> (transduce (in-range 1 10) (mapping number->immutable-string) #:into (join-into-string ", " #:before-last ", and ")) "1, 2, 3, 4, 5, 6, 7, 8, and 9"
> (transduce (in-range 1 10) (mapping number->immutable-string) #:into (join-into-string ", " #:before-first "func(" #:after-last ")")) "func(1, 2, 3, 4, 5, 6, 7, 8, 9)"
3.1.1 Reducer Constructors
The full reducer interface is captured by the make-reducer constructor, but this is sufficiently more power than most users should need. Three separate constructors are provided, each designed for three different categories of reducers with increasing power and complexity:
make-fold-reducer constructs fold reducers, which can express the same reductions as foldl.
make-effectful-fold-reducer constructs effectful fold reducers, which can express folds where a private, potentially mutable state value is initialized at the start of reduction and converted into a public result value at the end of reduction. Effectful fold reducers are a superset of fold reducers.
make-reducer constructs general reducers, with the full power of the reduction protocol and the ability to terminate the sequence early.
procedure
(make-fold-reducer consumer init-state [ #:name name]) → reducer? consumer : (-> any/c any/c any/c) init-state : any/c name : (or/c interned-symbol? #f) = #f
> (define into-reversed-list (make-fold-reducer (λ (lst v) (cons v lst)) (list))) > (transduce (in-range 5 25) #:into into-reversed-list) '(24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5)
procedure
(make-effectful-fold-reducer consumer init-state-maker finisher [ #:name name]) → reducer? consumer : (-> any/c any/c any/c) init-state-maker : (-> any/c) finisher : (-> any/c any/c) name : (or/c interned-symbol? #f) = #f
> (define into-list (make-effectful-fold-reducer (λ (lst v) (cons v lst)) list reverse)) > (transduce (in-range 5 25) #:into into-list) '(5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24)
procedure
(make-reducer #:starter starter #:consumer consumer #:finisher finisher #:early-finisher early-finisher [ #:name name]) → reducer? starter : (-> (variant/c #:consume any/c #:early-finish any/c)) consumer : (-> any/c any/c (variant/c #:consume any/c #:early-finish any/c)) finisher : (-> any/c any/c) early-finisher : (-> any/c any/c) name : (or/c interned-symbol? #f) = #f
Start the reduction by calling (starter) to create the initial reduction state, which must be a variant tagged as either #:consume or #:early-finish.
If the current state is tagged as #:consume, and the sequence is not empty, call (consumer (variant-value state) element) with the next sequence element to get the updated reduction state. Repeat this step until either no more elements are available or until the reduction state is tagged as #:early-finish.
If the current state is tagged as #:early-finish, call (early-finisher (variant-value state)) to determine the reduction result. Otherwise, call (finisher (variant-value state)) to get the reduction result.
> (define-record-type state (vector position))
> (define into-small-immutable-vector (make-reducer #:starter (λ () (variant #:consume (state #:vector (make-vector 10 #f) #:position 0))) #:consumer (λ (st v) (define i (state-position st)) (define vec (state-vector st)) (vector-set! vec i v) (define i* (add1 i)) (if (< i* 10) (variant #:consume (state #:vector vec #:position i*)) (variant #:early-finish vec))) #:finisher (λ (st) (define vec (state-vector st)) (define i (state-position st)) (vector->immutable-vector (vector-copy vec 0 i))) #:early-finisher vector->immutable-vector)) > (transduce (list 1 2 3) #:into into-small-immutable-vector) '#(1 2 3)
> (transduce (in-naturals) #:into into-small-immutable-vector) '#(0 1 2 3 4 5 6 7 8 9)
procedure
(reducer-starter red)
→ (-> (variant/c #:consume any/c #:early-finish any/c)) red : reducer?
procedure
(reducer-consumer red)
→ (-> any/c any/c (variant/c #:consume any/c #:early-finish any/c)) red : reducer?
procedure
(reducer-finisher red) → (-> any/c any/c)
red : reducer?
procedure
(reducer-early-finisher red) → (-> any/c any/c)
red : reducer?
3.1.2 Reducer Operators
procedure
(reducer-map red [#:domain f #:range g]) → reducer?
red : reducer? f : (-> any/c any/c) = values g : (-> any/c any/c) = values
> (define into-total-letters (reducer-map into-sum #:domain string-length)) > (transduce (list "the" "quick" "brown" "fox") #:into into-total-letters) 16
> (define stringly-typed-into-sum (reducer-map into-sum #:domain string->number #:range number->string)) > (transduce (list "12" "5" "42" "17") #:into stringly-typed-into-sum) "76"
procedure
(reducer-zip zip-function subreducer ...) → reducer?
zip-function : procedure? subreducer : reducer?
(define-tuple-type endpoints (first last)) (define into-endpoints (reducer-zip endpoints nonempty-into-first nonempty-into-last))
> (transduce (list 1 2 3 4 5) #:into into-endpoints) (endpoints 1 5)
procedure
(reducer-filter red pred) → reducer?
red : reducer? pred : predicate/c
> (transduce (list 1 'a 2 3 'b 'c 'd 4 'e 5) #:into (reducer-filter into-sum number?)) 15
procedure
(reducer-limit red amount) → reducer?
red : reducer? amount : natural?
> (transduce "hello world" #:into (reducer-limit into-string 5)) "hello"
3.1.3 Iteration and Comprehension with Reducers
syntax
(for/reducer reducer-expr (for-clause ...) body-or-break ... body)
reducer-expr : reducer?
> (for/reducer into-sum ([char (in-string "aaa0aa00a0aa")]) (if (char-alphabetic? char) 1 -1)) 4
syntax
(for*/reducer reducer-expr (for-clause ...) body-or-break ... body)
reducer-expr : reducer?
procedure
(make-reducer-based-for-comprehensions reducer-expression)
→
(-> syntax? syntax?) (-> syntax? syntax?) reducer-expression : syntax?
In order to prevent confusion over how many times reducer-expression is expanded and evaluated, strongly prefer using a single identifier for reducer-expression instead of an expression using reducer-map, make-fold-reducer, etc.
> (require (for-syntax racket/base))
> (define-syntaxes (for/sum for*/sum) (make-reducer-based-for-comprehensions #'into-sum))
> (for/sum ([str (in-list (list "apple" "orange" "banana" "grapefruit"))]) (string-length str)) 27
3.1.4 Reducer Contracts
(define/contract into-string-append (reducer/c string? string?) (make-fold-reducer string-append "" #:name 'into-string-append))
> (transduce (list "Red" "Blue" "Green") #:into into-string-append) "RedBlueGreen"
> (transduce (list "Red" 42 "Green") #:into into-string-append) into-string-append: contract violation
expected: string?
given: 42
in: an element reduced by
(reducer/c string? string?)
contract from:
(definition into-string-append)
blaming: top-level
(assuming the contract is correct)
at: eval:2:0
3.1.5 Reducer Chaperones and Impersonators
procedure
(reducer-impersonate reducer [ #:domain-guard domain-guard #:range-guard range-guard #:properties properties #:chaperone? chaperone?]) → reducer? reducer : reducer? domain-guard : (or/c (-> any/c any/c) #f) = #f range-guard : (or/c (-> any/c any/c) #f) = #f
properties : (hash/c impersonator-property? any/c #:immutable #t) = empty-hash
chaperone? : boolean? = (and (false? domain-guard) (false? range-guard))
If chaperone? is true, then the returned impersonator is a chaperone. In that case, both domain-guard and range-guard must always return values equal to whatever they’re given. Additionally, if a returned value is an impersonator, it must also be a chaperone.
(define (print-domain v) (printf "Reducing ~a\n" v) v) (define (print-range v) (printf "Reduction finished, result is ~a\n" v) v) (define into-list/printing (reducer-impersonate into-list #:domain-guard print-domain #:range-guard print-range))
> (transduce (in-range 5) #:into into-list/printing)
Reducing 0
Reducing 1
Reducing 2
Reducing 3
Reducing 4
Reduction finished, result is (0 1 2 3 4)
'(0 1 2 3 4)
3.1.6 Legacy Reduction APIs
> (reduce into-sum 1 2 3 4 5 6 7 8 9 10) 55
> (reduce into-product 2 3 5 7 11 13 17 19 23) 223092870
> (reduce into-count 'a 'b 'c 'd 'e) 5
procedure
(reduce-all red seq) → any/c
red : reducer? seq : sequence?
> (reduce-all into-sum (in-range 1 100)) 4950
> (reduce-all into-product (in-range 1 20)) 121645100408832000
> (reduce-all into-count (in-hash-values (hash 'a 1 'b 2 'c 3 'd 4))) 4