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) errorWrite 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) TReturn 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.
operatorsLt- Implementsa < bfor all orderable types.Le- Implementsa <= bfor all orderable types.Eq- Implementsa == bfor all comparable types.Ne- Implementsa != bfor all comparable types.Ge- Implementsa >= bfor all orderable types.Gt- Implementsa > bfor all orderable types.
functionalCompose*- 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)))
rangesChain- Produces anInputRangeiterating over a slice of ranges.Chunks- TakesInputRangechunks of a given size from anInputRange.Cycle- Repeats aForwardRangeinfinitely.Drop- Drops up to a number of elements from the start of a range.Enumerate- Yields elements with indexes (Pair[int, T]) from0.EnumerateN-Enumeratewith a provided start index.Flatten- Produces anInputRangefrom a range of ranges.FlattenSB- A special variation to flatten a slice ofBidirectionalRanges into a `BidirectionalRange.FlattenSS- A special variation to flatten a slice of slices into a `BidirectionalRange.FrontTransversal- Produces anInputRangeiterating over the first value of every non-empty range in a range of ranges.Generate- Creates an infiniteBidirectionalRangeby calling a function repeatedly.Iota- ABidirectionalRangeproducing values from0value up to and excluding anendvalue, incrementing by1.IotaStart-Iotawith a givenstartvalue to use in place of0.IotaStep-IotaStartwith astepvalue to use in place of1.Null- Returns aBidirectionalRangethat is always empty and consumes zero memory.Only- Returns aBidirectionalRangethrough the arguments provided.PadRight- Produces anInputRangewith up tocountitems by padding the range with a given value.Repeat- Creates an infiniteBidirectionalRangerepeating a value.Retro- Returns a reversedBidirectionalRange.RoundRobin- Produces anInputRangeiterating over the first value of every non-empty range in a range of ranges in a cycle until all ranges are exhausted.Slide- Produces aForwardRangeof chunks of a givenwindowSizestepping forward1element at a time.SlideStep-Slidewith astepSizefor stepping over elements.Stride- Produces everystepelement in anInputRange.Take- Takes up to a number of elements from a range.Tee- Produces anInputRangethat produces elements from a given range and outputs values to a givenOutputRangewhen elements are popped.ZipN- Produce a range stepping overNranges in parallel. There are severalZipfunctions for different numbers of arguments.
outputAssignSink- Creates anOutputRangethat assigns values to a givenInputRangeof pointers by dereferencing the pointers.NullSink- Creates anOutputRangethat discards all data.SliceSink- Creates anOutputRangethat appends to the given slice.
slicesBytes- ProducesBidirectionalRange[byte]from astringlike[]byte(s)Runes- ProducesBidirectionalRange[rune]from astringlike[]rune(s)SliceRange- ProducesBidirectionalRange[T]from[]TSliceRetro- ProducesBidirectionalRange[T]from[]Tin reverse.SlicePtrRange- Produces aBidirectionalRange[*T]from[]TSlicePtrRetro- Produces aBidirectionalRange[*T]from[]Tin reverse.Slice- Produces[]TfromInputRange[T]String- ProducesstringfromInputRange[rune]
comparisonAmong- Returnstrueif avalueis equal to any of thevaluesaccording to aneqcallback.AmongEq- Returnstrueif avalueis equal to any of thevaluesusing a simple==comparison.Cmp- Steps through two ranges comparing values with acmpfunction and returns the result if it's nonzero. Returns-1or1if ranges are different lengths.CmpFunc- Produces a comparison function for all types that support<and>.Equal- Returnstrueif two ranges are equal according to a comparison defined in a callback.EqualComparable- Returnstrueif two ranges are equal, element by element, for all comparable types.IsPermutation- Returnstrueif two ranges are permutations of each other inO(m + n)time by allocating a temporary map.IsPermutationNoAlloc- Returnstrueif 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.
iterationCache- Caches results in anInputRangesoFront()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.CacheBORCacheR- Caching ofBidirectionalRangeandRandomAccessRangeelements.ChunkBy- Returns anInputRangethat splits a range into sub-ranges whencb(a, b)returnsfalse.ChunkByValueReturns anInputRangethat 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 toEachwith an empty callback function, onlyFront()will never be called for the range.Filter- Filter anyInputRangewith 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-Groupwherea == bfor any comparable value.Joiner- Joins ranges with aseparatorForwardRangebetween ranges.JoinerSS- A special variation to join a slice of slices into aForwardRange.JoinStrings- A convenience function for creating astringfrom aForwardRangeofstringvalues with aseparatorstring.Map- Map elements in anyInputRangewith a callback. The result of calling the callback is not stored, so useCachewhen generating ranges withMap.Permutations- Given a slice of values, produce aForwardRangeof 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) == truefor adjacent elements.Splitter- Splits forward ranges with aseparatorForwardRangebetween ranges wherecb(a, b) == trueSplitterSS- A special variation to split a slice with a slice.SplitterComparable-Splitterwherea == b.SplitString- A convenience function for splitting astringwith a `string.Uniq- Yields unique adjacent elements wherecb(a, b) == true.UniqComparable-Uniqwherea == b.
mutationCopy- Copies all values from anInputRangeinto anOutputRange.Fill- Assigns a value to all locations in a range of pointers.FillPattern- Assigns a pattern of values from aForwardRangeto all locations in a range of pointers.StripLeft- Removes elements wherecb(a) == truefrom the front of a range.StripLeftComparable-StripLeftwherea == value, given a provided value.StripRight- Removes elements wherecb(a) == truefrom the back of a range.StripRightComparable- Removes elements wherea == valuefrom the back of a range.Strip- Removes elements wherecb(a) == truefrom the front and back of a range.StripComparable- Removes elements wherea == valuefrom the front and back of a range.
searchingAll- Checks if all elements in anInputRangesatisfy a callback.Any- Checks if any elements in anInputRangesatisfy a callback.CanFind-Find, but simply returnstrueif anything can be found.CanFindComparable-FindComparablebut simply returnstrueif anything can be found.Length- Returns the number of elements in anInputRangeinO(n)time.Count- Returns the number of elements in anInputRangewhere the callback returnstrueCountUntil- Returns the number of elements in anInputRangeuntil the callback returnstrue.CommonPrefix- Returns aFowardRangeover 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)returnstruefor every element of aneedlerange.FindEqualComparable- Advances a range untila == bis satisfied for all elements of aneedlerange.FindAdjacent- Advances a range untilcb(a, b)returnstruefor two adjacent elements in aForwardRange.FindAdjacentComparable- Advances a range untila == bis satisfied for two adjacent elements in aForwardRange.FindAmong- Advances a range untilcb(a, b)returnstruefor any element of aneedlerange.FindAmongComparable- Advances a range untila == bis satisfied for any element of aneedlerange.SkipOver- Skips over elements in ahaystackForwardRangeif the range starts with aneedlerange, according tocb(a, b) == true.StartsWith- Checks if oneInputRangestarts with another one, through a callback.TakeWhile- Advances a range until the callback returnstrue.
setopsCartesianProduct- Computes theCartesianproduct 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.