ranges
implements the nearest implementation of D range-based algorithms in
Go, for fun and to experiment with what is possible in Go 1.18 and above.
Ranges are a concept popular in D and C++ for creating a language by which generic algorithms can be define which operate on potentially any type of container or sequence of data. Instead of redundantly defining the same algorithms over and over again for different types, you instead define how to create a range that lazily evaluates a given container or sequence in potentially many directions, and existing generic algorithms can be applied to the range.
The library defines the following ranges primitives, as Go interfaces.
OutputRange[T any]
- Any type you can write the following operations:Put(element T) error
Write to the output range.
InputRange[T any]
- Anything iterable, with the following operations:Empty() bool
- Check if a range is empty.Front() T
- Get the current element. May panic if an empty range.PopFront()
- Remove the front element. May panic if an empty range.
ForwardRange[T any]
- AnInputRange[T]
with additional operations:Save() ForwardRange[T]
- Copy the range and its position.
BidirectionalRange[T any]
-ForwardRange[T]
with additional operations:Back() T
- Get the current end element. May panic if an empty range.PopBack() T
- Remove the back/end element. May panic if an empty range.SaveB() BidirectionalRange[T]
- Save the position in both directions.
RandomAccessRange[T any]
- ABidirectionalRange[T]
with additional operations:Get(index int) T
Return an element of the range. May panic if out of bounds.Len() int
- Return the length of the range.SaveR() RandomAccessRange[T]
- Save the position with random access.
The ranges library defines the following types, which may be used as constraints for generic functions.
Signed
- Any signed integer primitive type.Unsigned
- Any signed integer primitive type.Integer
- Any integer primitive type.Float
- Any floating point primitive type.RealNumber
- Any integer or floating point primitive type.Ordered
- Any primitive type that be ordered.
To express tuples in Go, there are different types declared for different numbers of items. This library has the following:
Pair
- Holds 2 values of any mix of types.Triplet
- Holds 3 values of any mix of types.Quartet
- Holds 4 values of any mix of types.Quintet
- Holds 5 values of any mix of types.Sextet
- Holds 6 values of any mix of types.Septet
- Holds 7 values of any mix of types.Octet
- Holds 8 values of any mix of types.Ennead
- Holds 9 values of any mix of types.Decade
- Holds 10 values of any mix of types.
For convenience, every tuple has a Make
function for creating them, and a
Get()
function for returning the values as a Go native tuple, so tuples
can be and split into multiple values without redundantly naming types.
// Inferred as Pair[int, string]
pair := MakePair(1, "hello")
// Split into int and string values.
num, str := pair.Get()
Ranges other than OutputRange
do not include error
values as part of their
types. Algorithms in this library do not result in runtime errors. They may only
panic when input to the functions producing the ranges is invalid, or when
attempting to access elements that do not exist. When you wish to place values
that result in errors, make the errors an explicit part of the type of your
range such as InputRange[Pair[T, error]]
.
Remember that in Go you can forward Go's native return tuples as arguments and return values, and this integrates well with the ranges library tuple types. This can make forwarding errors easier.
func OtherFunc() int, error { /* ... */ }
func ReturnsValueAndError() int, error {
// Create Pair[int, error]
// Go can spread the multiple return into the arguments for us.
pair := MakePair(OtherFunc())
// Return both values.
return pair.Get()
}
All ranges are lazily-evaluated and compute values anew on demand. This means
each call to Front()
or Back()
on a range will return a new value of T
.
Modifications to the returned objects may not be reflected in a container, such
as returning a copy of a struct instead of a pointer to it. When modification
to elements of an underlying container is necessary, you should create a range
of pointers, as in *T
. This will permit modification of underlying values.
Because values are computed on the fly, a call to a callback function may be
executed each time Front()
or Back()
are called for ranges. You may wish to
cache the results of computation in a chain of ranges by calling one the Cache
functions, including Cache
, CacheF
, CacheB
, and CacheR
.
Nearly all algorithms accepting or producing InputRange
can accept or produce
a ForwardRange
instead by calling a variation of the function with an F
suffix. Most algorithms that can accept or produce a ForwardRange
can accept
or produce a BidirectionalRange
with a B
suffix. Some functions such as Map
can be called with shortcuts for slices
with an S
suffix.
operators
Lt
- Implementsa < b
for all orderable types.Le
- Implementsa <= b
for all orderable types.Eq
- Implementsa == b
for all comparable types.Ne
- Implementsa != b
for all comparable types.Ge
- Implementsa >= b
for all orderable types.Gt
- Implementsa > b
for all orderable types.
functional
Compose*
- Composes several functions in a sequence, such thatCompose*(f1, ..., fn)(x)
producesf1(...(fn(x)))
Pipe*
- Pipes several functions in a sequence, such thatPipe*(f1, ..., fn)(x)
producesfn(...(f1(x)))
ranges
Chain
- Produces anInputRange
iterating over a slice of ranges.Chunks
- TakesInputRange
chunks of a given size from anInputRange
.Cycle
- Repeats aForwardRange
infinitely.Drop
- Drops up to a number of elements from the start of a range.Enumerate
- Yields elements with indexes (Pair[int, T]
) from0
.EnumerateN
-Enumerate
with a provided start index.Flatten
- Produces anInputRange
from a range of ranges.FlattenSB
- A special variation to flatten a slice ofBidirectionalRange
s into a `BidirectionalRange.FlattenSS
- A special variation to flatten a slice of slices into a `BidirectionalRange.FrontTransversal
- Produces anInputRange
iterating over the first value of every non-empty range in a range of ranges.Generate
- Creates an infiniteBidirectionalRange
by calling a function repeatedly.Iota
- ABidirectionalRange
producing values from0
value up to and excluding anend
value, incrementing by1
.IotaStart
-Iota
with a givenstart
value to use in place of0
.IotaStep
-IotaStart
with astep
value to use in place of1
.Null
- Returns aBidirectionalRange
that is always empty and consumes zero memory.Only
- Returns aBidirectionalRange
through the arguments provided.PadRight
- Produces anInputRange
with up tocount
items by padding the range with a given value.Repeat
- Creates an infiniteBidirectionalRange
repeating a value.Retro
- Returns a reversedBidirectionalRange
.RoundRobin
- Produces anInputRange
iterating over the first value of every non-empty range in a range of ranges in a cycle until all ranges are exhausted.Slide
- Produces aForwardRange
of chunks of a givenwindowSize
stepping forward1
element at a time.SlideStep
-Slide
with astepSize
for stepping over elements.Stride
- Produces everystep
element in anInputRange
.Take
- Takes up to a number of elements from a range.Tee
- Produces anInputRange
that produces elements from a given range and outputs values to a givenOutputRange
when elements are popped.ZipN
- Produce a range stepping overN
ranges in parallel. There are severalZip
functions for different numbers of arguments.
output
AssignSink
- Creates anOutputRange
that assigns values to a givenInputRange
of pointers by dereferencing the pointers.NullSink
- Creates anOutputRange
that discards all data.SliceSink
- Creates anOutputRange
that appends to the given slice.
slices
Bytes
- ProducesBidirectionalRange[byte]
from astring
like[]byte(s)
Runes
- ProducesBidirectionalRange[rune]
from astring
like[]rune(s)
SliceRange
- ProducesBidirectionalRange[T]
from[]T
SliceRetro
- ProducesBidirectionalRange[T]
from[]T
in reverse.SlicePtrRange
- Produces aBidirectionalRange[*T]
from[]T
SlicePtrRetro
- Produces aBidirectionalRange[*T]
from[]T
in reverse.Slice
- Produces[]T
fromInputRange[T]
String
- Producesstring
fromInputRange[rune]
comparison
Among
- Returnstrue
if avalue
is equal to any of thevalues
according to aneq
callback.AmongEq
- Returnstrue
if avalue
is equal to any of thevalues
using a simple==
comparison.Cmp
- Steps through two ranges comparing values with acmp
function and returns the result if it's nonzero. Returns-1
or1
if ranges are different lengths.CmpFunc
- Produces a comparison function for all types that support<
and>
.Equal
- Returnstrue
if two ranges are equal according to a comparison defined in a callback.EqualComparable
- Returnstrue
if two ranges are equal, element by element, for all comparable types.IsPermutation
- Returnstrue
if two ranges are permutations of each other inO(m + n)
time by allocating a temporary map.IsPermutationNoAlloc
- Returnstrue
if two ranges are permutations of each other inO(m * n)
without allocating a map.IsSameLength
- Checks if two ranges are the same length inO(n)
time.Max
- Returns the maximum value among all values given as arguments.Min
- Returns the minimum value among all values given as arguments.Mismatch
- Eagerly advances all ranges until the first element is found where any two elements are not equal according to a callback.
iteration
Cache
- Caches results in anInputRange
soFront()
will be called only once per element on the original range.CacheF
- Caches results soFront()
will only be called once per element on the original range, unless the range is saved and traversed over multiple times.CacheB
ORCacheR
- Caching ofBidirectionalRange
andRandomAccessRange
elements.ChunkBy
- Returns anInputRange
that splits a range into sub-ranges whencb(a, b)
returnsfalse
.ChunkByValue
Returns anInputRange
that splits a range into sub-ranges wherecb(a) == c(b)
.Each
- Calls a callback with each value of a range.Exhaust
- Steps through every element of a range until it's empty. Similar toEach
with an empty callback function, onlyFront()
will never be called for the range.Filter
- Filter anyInputRange
with a callback.FilterB
- Filter aBidirectionalRange
, producing a range that can be advanced in both directions. Less efficient for moving forwards, as it requires priming the range in both directions.Group
- Yields pairs of(value, size)
counting how many values are equal in each group according tocb(a, b)
.GroupComparable
-Group
wherea == b
for any comparable value.Joiner
- Joins ranges with aseparator
ForwardRange
between ranges.JoinerSS
- A special variation to join a slice of slices into aForwardRange
.JoinStrings
- A convenience function for creating astring
from aForwardRange
ofstring
values with aseparator
string
.Map
- Map elements in anyInputRange
with a callback. The result of calling the callback is not stored, so useCache
when generating ranges withMap
.Permutations
- Given a slice of values, produce aForwardRange
of all permutations of the given slice.ReduceNoSeed
- Eagerly reduces a range to a single value without a seed value. Panics when a range is empty.SplitWhen
- Splits a range wherecb(a, b) == true
for adjacent elements.Splitter
- Splits forward ranges with aseparator
ForwardRange
between ranges wherecb(a, b) == true
SplitterSS
- A special variation to split a slice with a slice.SplitterComparable
-Splitter
wherea == b
.SplitString
- A convenience function for splitting astring
with a `string.Uniq
- Yields unique adjacent elements wherecb(a, b) == true
.UniqComparable
-Uniq
wherea == b
.
mutation
Copy
- Copies all values from anInputRange
into anOutputRange
.Fill
- Assigns a value to all locations in a range of pointers.FillPattern
- Assigns a pattern of values from aForwardRange
to all locations in a range of pointers.StripLeft
- Removes elements wherecb(a) == true
from the front of a range.StripLeftComparable
-StripLeft
wherea == value
, given a provided value.StripRight
- Removes elements wherecb(a) == true
from the back of a range.StripRightComparable
- Removes elements wherea == value
from the back of a range.Strip
- Removes elements wherecb(a) == true
from the front and back of a range.StripComparable
- Removes elements wherea == value
from the front and back of a range.
searching
All
- Checks if all elements in anInputRange
satisfy a callback.Any
- Checks if any elements in anInputRange
satisfy a callback.CanFind
-Find
, but simply returnstrue
if anything can be found.CanFindComparable
-FindComparable
but simply returnstrue
if anything can be found.Length
- Returns the number of elements in anInputRange
inO(n)
time.Count
- Returns the number of elements in anInputRange
where the callback returnstrue
CountUntil
- Returns the number of elements in anInputRange
until the callback returnstrue
.CommonPrefix
- Returns aFowardRange
over the common prefix of two ranges. The first range must be aForwardRange
.DropWhile
- Advances a range while a callback returnstrue
.Find
- Advances a range untilcb(x, needle)
returnstrue
, comparing elements with aneedle
.FindComparable
- Advances a range untilx == needle
.FindEqual
- Advances a range untilcb(a, b)
returnstrue
for every element of aneedle
range.FindEqualComparable
- Advances a range untila == b
is satisfied for all elements of aneedle
range.FindAdjacent
- Advances a range untilcb(a, b)
returnstrue
for two adjacent elements in aForwardRange
.FindAdjacentComparable
- Advances a range untila == b
is satisfied for two adjacent elements in aForwardRange
.FindAmong
- Advances a range untilcb(a, b)
returnstrue
for any element of aneedle
range.FindAmongComparable
- Advances a range untila == b
is satisfied for any element of aneedle
range.SkipOver
- Skips over elements in ahaystack
ForwardRange
if the range starts with aneedle
range, according tocb(a, b) == true
.StartsWith
- Checks if oneInputRange
starts with another one, through a callback.TakeWhile
- Advances a range until the callback returnstrue
.
setops
CartesianProduct
- Computes theCartesian
product of a series of forward ranges.
New Ranges can be implemented by creating a struct with pointer receivers for all of the methods needed to satisfy a type of range. For example:
// A pointer to this is an InputRange[T] because of the implementation below.
type newRangeType[T any] struct {
// Add whatever data you need.
}
func (r *newRangeType[T]) Empty() bool { /* ... */ }
func (r *newRangeType[T]) Front() T { /* ... */ }
func (r *newRangeType[T]) PopFront() { /* ... */ }
func MakeNewRangeType[T any](/* ... */) InputRange[T] {
return &newRangeType{/* ... */}
}
It's important to return pointers to your structs so ranges can be freely passed around as a reference type.
Wherever possible, algorithms will attempt to minimize allocations. The Len()
of any RandomAccessRange
objects at runtime, or the len()
of slices may be
used to determine how much memory to allocate, or to optimize algorithms to
avoid unnecessary operations.
Go's compiler is capable of inlining many range function calls, which will reduce overhead. Ranges are likely to be stored in the garbage-collected heap. The performance of using ranges will therefore be hard to predict. You should benchmark and debug your code, and add optimizations where appropriate. Significant improvements easily obtained are never premature.
It's not possible to implement a Filter
function that returns
ForwardRange[T]
if given ForwardRange[T]
and returns InputRange[T]
if
given InputRange[T]
.
The only available alternative in Go is to write multiple function names for
each range type, such as FilterF
for ForwardRange[T]
and Filter
for InputRange[T]
.
We need SaveB
and SaveR
methods because there are no covariant return types
in Go. This means a user can be led to choosing a suboptimal save function.
There's no way around this in Go. In D, saving a range automatically carries
across the more specific details of the range type, such as RandomAccessRange
save
method returning a RandomAccessRange
.
Variadic generic types are difficult to implement correctly, and Go does not have generic tuple types to represent any combination of values. This means that just as in classic Java generic interfaces, functions that take different combinations of types must be redundantly defined up to some maximum number of arguments in a programmer's imagination of how many arguments someone will call a function with. For example:
func Args0()[] { }
func Args1[V1 any](v1 V1) { }
func Args2[V1 any, V2](v1 V1, v2 V2) { }
func Args3[V1 any, V2 any, V3 any](v1 V1, v2 V2, v3 V3) { }
func Args3[V1 any, V2 any, V3 any, V4 any](v1 V1, v2 V2, v3 V3, v4 V4) { }
It's not possible to write something like the following for all of the above:
func Args[V any...](v V...) { }
Because of this, multiple variations of the Zip
function are required.
Go does not have exceptions outside of panic
. Ranges that can yield errors at
any point have to explicitly include them in the type of result returned by
Front()
, so all ranges should be considered exception safe as long as the
contract of calling Empty()
before Front()
or PopFront()
is not broken.