4 Algebraic Operators
(require relation/algebraic) | package: Relation |
Generic algebraic operators for composing data.
The built-in operators + and * operate on numbers specifically. Often, however, we are interested in performing operations "similar" to these for datatypes that aren’t numbers, for which we would resort to type-specific operators like append for lists.
This module generalizes the standard algebraic operators to work on any type that supports a "canonical" notion of addition, multiplication, or concatenation. This allows our intuitions about addition and other forms of composition to extend over all appropriate types via the use of the common generic operators +, * and ...
4.1 Interfaces
This module provides three generic interfaces – gen:appendable, gen:multipliable, and gen:addable. These are meant to represent the canonical "idea" of the operations of concatenation, multiplication and addition, respectively, whose behavior may be customized for each type via these generic interfaces, and used via the common operators .. (concatenation), * (multiplication), and + (addition).
In order to support generic composition seamlessly, all of the composition interfaces support a generic (rather than type- and operation-specific) identity value that is employed in cases where type information is not available.
value
ID : composition-identity?
In the event no operands are received in the course of a computation, the result of composition would be ID, which would not be a usable result in utilities that are expecting a specific type such as a string. In such cases, the result could be converted to the expected type using one of the transformers in relation/transform such as ->string. If you are not using a built-in type but rather a custom type, however, you could use the following more general utility to "reify" the generic identity value to a type of your choosing:
> (reify ID 3) 0
> (reify ID "cherry") ""
> (reify ID (list)) '()
> (reify "hi" (list)) "hi"
> (reify '(1 2 3) "") '(1 2 3)
4.1.1 Concatenation
value
procedure
(appendable? v) → boolean?
v : any/c
> (appendable? 3) #t
> (appendable? #\a) #f
> (appendable? "cherry") #t
> (appendable? (list)) #t
> (appendable? (set)) #t
> (appendable? (hash)) #t
> (appendable? (vector)) #t
To implement this interface for custom types, the following methods need to be implemented, unless the type already implements another interface for which a default implementation exists for gen:appendable (such as gen:sequence) and if more specific handling is not needed for the custom type.
procedure
(append a b) → appendable?
a : appendable? b : appendable?
In addition to providing a definition of concatenation appropriate to the type, implementations of this method must also handle the special value ID in the following way: if the operand b is eq? to ID, then the result of the function must be a.
procedure
a : appendable?
procedure
a : appendable?
Providing an implementation for this method is optional, and most types would usually not define an inverse for an append-like operation. That is, in mathematical terms, append-like operations typically form an algebraic semigroup or monoid rather than a group.
4.1.2 Multiplication
value
Since numbers are the only common type for which the multiply operation is defined, this interface is present mostly for uniformity, being leveraged by utilities like fold, and of course also for use in custom types.
> (* 1 2 3 4) 24
procedure
(multipliable? v) → boolean?
v : any/c
> (multipliable? 3) #t
> (multipliable? #\a) #f
> (multipliable? "cherry") #f
> (multipliable? (list)) #f
To implement this interface for custom types, the following methods need to be implemented:
procedure
(multiply a b) → multipliable?
a : multipliable? b : multipliable?
In addition to providing a definition of multiplication appropriate to the type, implementations of this method must also handle the special value ID in the following way: if the operand b is eq? to ID, then the result of the function must be a.
procedure
a : multipliable?
procedure
a : multipliable?
Providing an implementation for this method is optional; a multiply operation need not admit an inverse, for instance, for a custom integer type, multiplication would not define an inverse since multiplicative inverses for numbers would be rational numbers, rather than integers, thus being excluded by the "same type" requirement above.
4.1.3 Addition
value
> (addable? 3) #t
> (addable? #\a) #f
> (addable? "cherry") #f
> (addable? (list)) #f
> (addable? (set)) #f
> (addable? (hash)) #f
> (addable? (vector)) #t
To implement this interface for custom types, the following methods need to be implemented:
In addition to providing a definition of addition appropriate to the type, implementations of this method must also handle the special value ID in the following way: if the operand b is eq? to ID, then the result of the function must be a.
procedure
(addable-identity a) → addable?
a : addable?
procedure
(addable-inverse a) → addable?
a : addable?
Providing an implementation for this method is required; an addition operation must admit an inverse. That is, in mathematical terms, addition-like operations are expected to form an algebraic group.
4.2 Utilities
procedure
(.. v ...) → appendable?
v : appendable?
procedure
(∘ v ...) → appendable?
v : appendable?
> (.. "hi" " " "there") "hi there"
> (.. '(1 2 3) '(4 5 6)) '(1 2 3 4 5 6)
> (.. (hash 'a 1 'b 2) (hash 'c 3)) '#hash((a . 1) (c . 3) (b . 2))
> ((.. ->string +) 3 4) "7"
> (∘ "hi" " " "there") "hi there"
procedure
(* v ...) → multipliable?
v : multipliable?
> (* 1 2 3 4) 24
procedure
(id operation) → procedure?
operation : procedure?
> ((id add) 3) 0
> ((id multiply) 3) 1
> ((id add) #(1 2 -3)) '#(0 0 0)
> ((id append) "hi") ""
> ((id append) '(1 2 3)) '()
> ((id ..) "hi") ""
> ((id +) 3) 0
> ((id *) 3) 1
procedure
(inverse operation) → procedure?
operation : procedure?
procedure
(/ v ...) → multipliable?
v : multipliable?
procedure
(fold f seqs [ #:into base #:order order #:direction direction #:with-steps? with-steps?]) → any/c f : procedure? seqs : (listof (sequenceof any/c)) base : any/c = #f order : (one-of/c 'abb 'bab) = 'abb direction : (one-of/c 'left 'right) = 'right with-steps? : boolean? = #f
procedure
(foldl f seqs [ #:into base #:order order #:with-steps? with-steps?]) → any/c f : procedure? seqs : (listof (sequenceof any/c)) base : any/c = #f order : (one-of/c 'abb 'bab) = 'abb with-steps? : boolean? = #f
procedure
(foldr f seqs [ #:into base #:order order #:with-steps? with-steps?]) → any/c f : procedure? seqs : (listof (sequenceof any/c)) base : any/c = #f order : (one-of/c 'abb 'bab) = 'abb with-steps? : boolean? = #f
With folding operations there are two parameters that one may wish to tweak. The first is the direction of the fold, either left or right, for which one may use either foldl or foldr. The second is the order in which arguments are supplied to the folding function f, which may be controlled by the keyword argument #:order, with a value of 'abb corresponding to the accumulator always being passed last, consistent with Racket’s built-in foldl, and a value of 'bab corresponding to the accumulator always being passed first, consistent with the version of foldl found in data/collection and also in some other functional languages like Haskell.
In many common cases, modulating the folding direction and/or the argument order does not make a difference to the result. Specifically, in those cases where the operation is commutative and closed, it doesn’t matter whether you use foldl or foldr, or whether you use argument order 'abb or 'bab. The result would be the same. However, in cases where the operation is not closed, argument order becomes significant. As a general guideline, choose between foldl and foldr in cases where the operation is not commutative (a relatively common case, such as string concatenation), and between the two argument orders in cases where the operation isn’t closed (a less common case, such as type constructors).
foldl is equivalent to calling fold with #:direction 'left, and foldr is equivalent to calling fold with #:direction 'right. fold/steps is equivalent to calling fold with #:with-steps? #t.
> (fold + '(1 2 3 4)) 10
> (fold * '(1 2 3 4)) 24
> (fold .. '("hi" " " "there")) "hi there"
> (foldr + '(1 2 3 4)) 10
> (foldl + '(1 2 3 4)) 10
> (foldr + '(1 2 3 4) #:order 'bab) 10
> (foldl + '(1 2 3 4) #:order 'bab) 10
> (foldr .. '("hi" " " "there")) "hi there"
> (foldl .. '("hi" " " "there")) "there hi"
> (foldr cons '(1 2 3) #:into '() #:order 'abb) '(1 2 3)
> (foldl cons '(1 2 3) #:into '() #:order 'abb) '(3 2 1)
> (foldr cons '(1 2 3) #:into '() #:order 'bab) '(((() . 3) . 2) . 1)
> (foldl cons '(1 2 3) #:into '() #:order 'bab) '(((() . 1) . 2) . 3)
procedure
(fold/steps f seqs [ #:into base #:order order #:direction direction]) → any/c f : (-> any/c any/c any/c) seqs : (listof (sequenceof any/c)) base : any/c = #f order : (one-of/c 'abb 'bab) = 'abb direction : (one-of/c 'left 'right) = 'right
procedure
(foldl/steps f seqs [ #:into base #:order order]) → any/c f : (-> any/c any/c any/c) seqs : (listof (sequenceof any/c)) base : any/c = #f order : (one-of/c 'abb 'bab) = 'abb
procedure
(foldr/steps f seqs [ #:into base #:order order]) → any/c f : (-> any/c any/c any/c) seqs : (listof (sequenceof any/c)) base : any/c = #f order : (one-of/c 'abb 'bab) = 'abb
> (->list (fold/steps + '(1 2 3 4))) '(0 4 7 9 10)
> (->list (foldr/steps + '(1 2 3 4))) '(0 4 7 9 10)
> (->list (foldl/steps + '(1 2 3 4))) '(0 1 3 6 10)
> (->list (foldr/steps * '(1 2 3 4))) '(1 4 12 24 24)
> (->list (foldl/steps * '(1 2 3 4))) '(1 1 2 6 24)
> (->list (foldr/steps .. '("hi" " " "there"))) '("" "there" " there" "hi there")
> (->list (foldl/steps .. '("hi" " " "there"))) '("" "hi" " hi" "there hi")
procedure
(product vs) → multipliable?
vs : (listof multipliable?)