Repository: rakeeb-hossain/functools Branch: master Commit: 58f775af362d Files: 20 Total size: 42.8 KB Directory structure: gitextract_s452whe8/ ├── .github/ │ └── workflows/ │ └── go.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── doc.go ├── examples/ │ ├── error-handling/ │ │ └── error_handling.go │ └── portfolio/ │ └── portfolio.go ├── functionals.go ├── functionals_test.go ├── go.mod ├── go.sum ├── spined_buffer.go ├── spined_buffer_test.go ├── spliterator.go ├── spliterator_test.go ├── stream.go ├── stream_test.go ├── terminals.go └── terminals_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: stable: false go-version: 1.18.0-beta1 - name: Build run: go build -v ./... - name: Test run: go test -v ./... ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Project files .idea/ java_tests/ ================================================ FILE: CONTRIBUTING.md ================================================ # functools Contributing guide Thank you for considering contributing to functools! To get an overview of the project, read the [README](README.md) and peruse through the codebase. ### Open an Issue All contributions should be linked to issues, so if you think a functional component or optimization should be added to functools, start by filing a request there. After discussion with the community, the issue will be assigned to you if deemed a good fit for the library. ### Fork and PR Make a fork of this repository and commit your approriate changes. Afterwards, submit a PR for review. ### PR merged! If approved, your work will be merged and added to the Golang functools library 🎉🎉 Thank you for your contribution! ### Questions? Email me at rakeeb.hossain1@gmail.com and I'll get back to you ASAP ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Rakeeb Hossain Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # functools [![Go](https://github.com/rakeeb-hossain/functools/actions/workflows/go.yml/badge.svg)](https://github.com/rakeeb-hossain/functools/actions/workflows/go.yml) functools is a simple Go library that brings you your favourite functional paradigms without sacrificing type-safety using `interface{}` or `reflect` Made possible by Go 1.18 using the newly introduced generics. ## Features - Any - All - Count - Filter - ForEach - Map - Reduce - ReduceRight - Sum - Chunk ## Installation `go get -u github.com/rakeeb-hossain/functools` ## Usage ```go import ( "github.com/rakeeb-hossain/functools" "fmt" ) type User struct { username string hasPortfolio bool } var users = []User{ {"gopher", true}, {"rakeeb", false}, {"jack", true}} func main() { // Count users with linked portfolios fmt.Printf("num users with linked portfolios: %d", functools.Count(users, func(u User) bool { return u.hasPortfolio })) // Print usernames of users with linked portfolios functools.ForEach( functools.Filter(users, func(u User) bool { return u.hasPortfolio }), func(u User) { fmt.Printf("%s has a linked portfolio\n", u.username) }) } ``` ## Documentation https://pkg.go.dev does not yet support Go 1.18 packages that use generics: https://github.com/golang/go/issues/48264 For now, documentation is provided via comments and by running `go doc -all` from the package directory. ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) ================================================ FILE: doc.go ================================================ // Package functools provides type-safe functional helpers using Go 1.18 generics. // // functools is intended to allow writing concise functionally-inspired code without sacrificing performance or type-safety, // as is the case for functional libraries pre-Go generics that had to use `interface{}` or `reflect`. package functools ================================================ FILE: examples/error-handling/error_handling.go ================================================ // This showcases how we can handle errors using functools helpers, despite there being no explicit error // return types by the functools functions. We make use of passing errors via closures. // // This is more explicit than it likely would be in practice just to showcase what safe handling capabilities // this functools still enables. In practice, checking for errant input as a pre-step before dividing would likely // be more effective. package main import ( "errors" "github.com/rakeeb-hossain/functools" "log" ) type fraction struct { dividend int divisor int } func main() { fractions := []fraction{{5, 1}, {3, 6}, {2, 0}} // We handle errors by populating an error which we pass to our mapper function via a closure. // We also return a pointer to a float64 instead of a float64 itself, so we can handle nil types // in case we encounter an error. var err error safeDivide := func(f fraction) *float64 { if f.divisor == 0 { err = errors.New("cannot divide by 0") return nil } res := float64(f.dividend) / float64(f.divisor) return &res } rationalResults := functools.Map(fractions, safeDivide) if err != nil { log.Println(err) } // We can sum the safe rational results using a custom Reduce function res := functools.Reduce(rationalResults, 0.0, func(accum float64, n *float64) float64 { if n == nil { return accum } return accum + *n }) log.Printf("the safe sum of fractions %v is %f\n", fractions, res) } ================================================ FILE: examples/portfolio/portfolio.go ================================================ // This showcases several applications of functools helpers to a store of user objects and their associated // portfolio holdings. package main import ( "github.com/rakeeb-hossain/functools" "log" ) type user struct { username string age int hasPortfolio bool } type holding struct { ticker string boughtTime int quantity float64 price float64 } type portfolio struct { holdings []holding } var ( users = []user{ {"gopher", 21, true}, {"rakeeb", 20, false}, {"jack", 22, true}} usersPortfolioMap = map[string]portfolio{ "gopher": {[]holding{{"TSLA", 1639768692, 4.5, 1000}, {"ABNB", 1639163892, 2.5, 200}}}, "jack": {[]holding{{"BTC", 1512933492, 5, 1000}, {"ETH", 1639768692, 10, 100}}}} ) func main() { // Count users with linked portfolios log.Printf("num users with linked portfolios: %d", functools.Count(users, func(u user) bool { return u.hasPortfolio })) // Print usernames of users with linked portfolios functools.ForEach( functools.Filter(users, func(u user) bool { return u.hasPortfolio }), func(u user) { log.Printf("%s has a linked portfolio\n", u.username) }, ) // For users with connected portfolios, get portfolio values usersWithPortfolio := functools.Filter(users, func(u user) bool { return u.hasPortfolio }) userPortfolioValues := functools.Map(usersWithPortfolio, func(u user) float64 { return functools.Reduce(usersPortfolioMap[u.username].holdings, 0, func(accum float64, h holding) float64 { return accum + h.quantity*h.price }) }) for i, _ := range usersWithPortfolio { log.Printf("user %s has portfolio value %f\n", usersWithPortfolio[i].username, userPortfolioValues[i]) } // Get total price of assets in all connected portfolios totalVal := functools.Sum(userPortfolioValues) log.Printf("total asset value: %f", totalVal) } ================================================ FILE: functionals.go ================================================ // Contains classic generic functional methods package functools // //// Map consumes a slice of a generic type and returns a slice with the supplied mapping function //// applied to each element. //// //// mapper should be error-safe. It should handle any errors internally and return the desired type. //// If other arguments are required by mapper, mapper should be made a closure with the appropriate //// variables referenced. //func Map_[A any, B any](mapper func(A) B, iter Spliterator[A]) (res Spliterator[B]) { // res.tryAdvance = func(fn func(B)) bool { // _mapper := func(a A) { // fn(mapper(a)) // } // return iter.tryAdvance(_mapper) // } // // res.trySplit = iter.trySplit // // return res //} // //// Stateful op // //func Sorted[T any](iter Spliterator[T]) (res Spliterator[T]) { // return res //} // ////func ChunkIter[T any](iter Iterator[T], len int) Iterator[[]T] { //// return func() (lst []T, b bool) { //// res, ok := Next(iter) //// if !ok { //// return lst, ok //// } //// //// lst = make([]T, len) //// lst[0] = res //// for i := 1; i < len; i++ { //// res, ok := Next(iter) //// if !ok { //// return lst, true //// } //// lst[i] = res //// } //// return lst, true //// } ////} // //// Filter consumes a slice of a generic type and returns a slice with only the elements which returned true after //// applying the predicate. //// //// Elements are returned in the same order that they were supplied in the slice. //// //// predicate should be error-safe. It should handle any errors internally and return only a bool. //// If other arguments are required by predicate, predicate should be made a closure with the appropriate //// variables referenced. //func Filter[T any, A ~[]T](slice A, predicate func(T) bool) A { // res := make(A, 0, len(slice)) // for _, v := range slice { // if predicate(v) { // res = append(res, v) // } // } // return res //} // //func FilterIter[T any](iter Iterator[T], predicate func(T) bool) Iterator[T] { // return func() (t T, b bool) { // for val, ok := Next(iter); ok; val, ok = Next(iter) { // if !ok || predicate(val) { // return val, ok // } // } // return t, b // b is false here // } //} // //// Reduce consumes a slice of a generic type and an initial value. It reduces the slice to a single value by applying //// the binary reducer function to each successive element in the slice. //// //// Vacuously, empty slices return the initial value provided. //// //// reducer should be error-safe. It should handle any errors internally and return the desired type. //// If other arguments are required by reducer, reducer should be made a closure with the appropriate //// variables referenced. //func Reduce[T any, R any](iter Spliterator[T], initial R, reducer func(R, T) R) R { // accum := initial // return accum //} // //// ReduceRight consumes a slice of a generic type and an initial value. It //// reduces the slice to a single value by applying the binary reducer function //// to each element in the slice. ReduceRight differs from Reduce by iterating //// from the last element to the first element. //// //// Vacuously, empty slices return the initial value provided. //// //// reducer should error-safe. It should handle any errors internally and return //// the desired type. If other arguments are required by reducer, reducer should //// be made a closure with the appropriate variables referenced. //func ReduceRight[T any, A ~[]T, R any](slice A, initial R, reducer func(R, T) R) R { // accum := initial // for i := len(slice) - 1; i >= 0; i-- { // accum = reducer(accum, slice[i]) // } // return accum //} // //// ForEach applies fun to each element in slice //// //// fun should be error-safe and handle errors internally. If other arguments are required by predicate, //// predicate should be made a closure with the appropriate variables referenced. //func ForEach[T any, A ~[]T](slice A, fun func(T)) { // for _, v := range slice { // fun(v) // } //} // //// Count consumes a slice of a generic type and counts the elements that return true after applying //// the predicate. //// //// Vacuously, empty slices return 0 regardless of the predicate. //// //// predicate should be error-safe. It should handle any errors internally and return only a bool. //// If other arguments are required by predicate, predicate should be made a closure with the appropriate //// variables referenced. //func Count[T any, A ~[]T](slice A, predicate func(T) bool) int { // res := 0 // for _, v := range slice { // if predicate(v) { // res++ // } // } // return res //} ================================================ FILE: functionals_test.go ================================================ package functools //type user struct { // age int //} // //// Filter tests //func TestGeqFilter(t *testing.T) { // slice := []user{{17}, {21}, {18}, {32}, {49}, {76}} // geqTwentyOne := func(u user) bool { return u.age >= 21 } // res := Map(Filter(slice, geqTwentyOne), func(u user) int { return u.age }) // expect := []int{21, 32, 49, 76} // // for i, _ := range res { // if i >= len(expect) || res[i] != expect[i] { // t.Errorf("TestGeqFilter was incorrect, got: %v, expected: %v", res, expect) // return // } // } // // mapper := func(val user) int { return val.age } // Map(func(val int) int { return val + 1 }, Map(mapper, SliceIter[user](slice))) //} // //// Map tests //func TestAddMap(t *testing.T) { // slice := []int{1, 2, 3} // adder := func(val int) int { return val + 1 } // res := Map(slice, adder) // expect := []int{2, 3, 4} // // for i, _ := range res { // if i >= len(expect) || res[i] != expect[i] { // t.Errorf("TestAddMap was incorrect, got: %v, expected: %v", res, expect) // return // } // } //} // //func TestAddMapIter(t *testing.T) { // slice := Iter([]int{1, 2, 3}) // adder := func(val int) int { return val + 1 } // res := Slice(MapIter(slice, adder)) // expect := []int{2, 3, 4} // // for i, _ := range res { // if i >= len(expect) || res[i] != expect[i] { // t.Errorf("TestAddMapIter was incorrect, got: %v, expected: %v", res, expect) // return // } // } //} // //func TestUserMap(t *testing.T) { // slice := []user{{32}, {29}, {42}} // ageTransformer := func(val user) int { return val.age } // res := Map[user, []user, int](slice, ageTransformer) // expect := []int{32, 29, 42} // // for i, _ := range res { // if i >= len(expect) || res[i] != expect[i] { // t.Errorf("TestUserMap was incorrect, got: %v, expected: %v", res, expect) // return // } // } //} // //// Reduce tests //func TestReduceSum(t *testing.T) { // slice := []int{1, 2, 3} // adder := func(a, b int) int { return a + b } // res := Reduce(slice, 0, adder) // expect := 6 // // if res != expect { // t.Errorf("TestReduceSum was incorrect, got: %d, expected: %d", res, expect) // } //} // //func TestReduceUserAge(t *testing.T) { // slice := []user{{32}, {29}, {42}} // adder := func(accum int, val user) int { return accum + val.age } // res := Reduce[user, []user, int](slice, 0, adder) // expect := 103 // // if res != expect { // t.Errorf("TestReduceUserAge was incorrect, got: %d, expected: %d", res, expect) // } //} // //// ReduceRight tests //type reduceRightCase[T any, R comparable] struct { // name string // slice []T // initial R // reducer func(R, T) R // want R //} // //func TestReduceRight(t *testing.T) { // t.Run("integers", func(t *testing.T) { // // cases := []reduceRightCase[int, int]{ // { // name: "addition", // slice: []int{1, 2, 3}, // initial: 0, // reducer: func(a, b int) int { return a + b }, // want: 6, // }, // { // name: "subtraction", // slice: []int{1, 2, 3}, // initial: 0, // reducer: func(a, b int) int { return a - b }, // want: -6, // }, // { // name: "multiplication", // slice: []int{1, 2, 3}, // initial: 1, // reducer: func(a, b int) int { return a * b }, // want: 6, // }, // } // // for _, c := range cases { // t.Run(c.name, func(t *testing.T) { // got := ReduceRight(c.slice, c.initial, c.reducer) // // if got != c.want { // t.Errorf("got %v, want %v", got, c.want) // } // }) // } // }) // // t.Run("integers and floats", func(t *testing.T) { // cases := []reduceRightCase[int, float64]{ // { // name: "division", // slice: []int{1, 2, 3}, // initial: 1.0, // reducer: func(accum float64, curr int) float64 { return float64(curr) / accum }, // want: 1.5, // }, // } // // for _, c := range cases { // t.Run(c.name, func(t *testing.T) { // got := ReduceRight(c.slice, c.initial, c.reducer) // // if got != c.want { // t.Errorf("got %v, want %v", got, c.want) // } // }) // } // }) //} // //// ForEach tests //func TestClosureForEach(t *testing.T) { // slice := []int{1, 2, 3} // res := 0 // ForEach(slice, func(val int) { res += val }) // expect := 6 // // if res != expect { // t.Errorf("TestClosureForEach was incorrect, got: %d, expected: %d", res, expect) // } //} // //// Count tests //func TestGeqCount(t *testing.T) { // slice := []int{1, 100, 200, 3, 14, 21, 32} // res := Count(slice, func(val int) bool { return val >= 21 }) // expected := 4 // // if res != expected { // t.Errorf("TestLtAny with %v was incorrect, got: %d, expected: %d", slice, res, expected) // } //} ================================================ FILE: go.mod ================================================ module github.com/rakeeb-hossain/functools go 1.18 require golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect ================================================ FILE: go.sum ================================================ golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4= golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= ================================================ FILE: spined_buffer.go ================================================ package functools import ( "fmt" ) const FirstBuffPower int = 4 const MinSpineSize int = 2 // must be >= 1 const SpineExtendCount int = 1 type AbstractBuffer[T any] interface { Push(T) At(int) Flatten() Len() int Capacity() int } // SpinedBuffer is an optimization on a regular slice that doesn't require copying elements on re-sizing. // This has good performance in cases where an unknown size stream is being processed, since copying from // re-sizing is minimized. type SpinedBuffer[T any] struct { // Spine data structures // We optimistically assume everything will fit into currBuff in most cases currBuff []T spines [][]T // Spine state management sizeOfPrevBuffers []int spineIdx int flatIdx int sizePower int capacity int inflated bool } // Checks if copy is required and copies currBuff to spines func (s *SpinedBuffer[T]) inflateSpine() { if s.spineIdx == 0 && s.flatIdx == s.capacity { // Create spines s.spines = make([][]T, MinSpineSize) // Assign currBuff to first spine and set sizeOfPrevBuffers s.spines[0] = s.currBuff[:] // should be O(1) since just copying slice s.sizeOfPrevBuffers = make([]int, 1, MinSpineSize) s.sizeOfPrevBuffers[0] = s.flatIdx // Update subsequent spines for i := 1; i < MinSpineSize; i++ { s.sizePower++ s.spines[i] = make([]T, 0, 1<= 0 { if s.spineIdx == 0 { res = s.currBuff[index] } else { // binary-search for upper-bound; gives index of first elem in sizeOfPrevBuffers that is >= index // this index is guaranteed to be valid since sizeOfPrevBuffers last elem is s.flatIdx and index < s.flatIdx spineSizeIdx := upperBoundGuaranteed(index, s.sizeOfPrevBuffers) // Equality-case where index actually belongs to next spine if s.sizeOfPrevBuffers[spineSizeIdx] == index { res = s.spines[spineSizeIdx+1][0] } else { offset := index // case where index belongs to first spine so spineSizeIdx == 0 if spineSizeIdx > 0 { offset = index - s.sizeOfPrevBuffers[spineSizeIdx-1] } res = s.spines[spineSizeIdx][offset] } } } return res } func (s SpinedBuffer[T]) PrintStats() { fmt.Printf("spineIdx: %d\n", s.spineIdx) fmt.Printf("flatIdx: %d\n", s.flatIdx) fmt.Printf("capacity: %d\n", s.capacity) fmt.Printf("sizePower: %d\n", s.sizePower) } func upperBoundGuaranteed(val int, arr []int) int { lo := 0 hi := len(arr) for lo < hi { mid := (hi-lo)/2 + lo if arr[mid] >= val { hi = mid } else { lo = mid + 1 } } return lo } ================================================ FILE: spined_buffer_test.go ================================================ package functools import ( "math/rand" "reflect" "testing" ) func TestSpinedBuffer_Push(t *testing.T) { NUM_TRIALS := 10000 buff := CreateSpinedBuffer[int]() arr := make([]int, 0, NUM_TRIALS) for i := 1; i <= NUM_TRIALS; i++ { buff.Push(i) arr = append(arr, i) if !reflect.DeepEqual(arr, buff.Flatten()) { buff.PrintStats() t.Errorf("error at %d", i) } } } func TestSpinedBuffer_At(t *testing.T) { NUM_TRIALS := 10000 buff := CreateSpinedBuffer[int]() for i := 1; i <= NUM_TRIALS; i++ { buff.Push(i) if buff.Len() != i { t.Errorf("error at %d", i) } r := rand.Int() % buff.Len() if buff.At(r) != (r + 1) { t.Errorf("error at index %d on iteration %d", r, i) } } } const BENCHMARK_SIZE int = 10000 func BenchmarkCreateSpinedBuffer_Push(b *testing.B) { buff := CreateSpinedBuffer[int]() for i := 0; i < BENCHMARK_SIZE; i++ { buff.Push(i) } } func BenchmarkSlicePush(b *testing.B) { slice := make([]int, 0) for i := 0; i < BENCHMARK_SIZE; i++ { slice = append(slice, i) } } //func BenchmarkArrayPush(b *testing.B) { // arr := [BENCHMARK_SIZE]int{} // // for i := 0; i < BENCHMARK_SIZE; i++ { // arr[i] = i // } //} ================================================ FILE: spliterator.go ================================================ package functools import ( "golang.org/x/exp/constraints" ) type Spliterator[T any] struct { tryAdvance func(func(T)) bool forEachRemaining func(func(T)) trySplit func() (Spliterator[T], bool) characteristics uint } func EmptyIter[T any]() (res Spliterator[T]) { res.tryAdvance = func(func(T)) bool { return false } res.forEachRemaining = func(func(T)) {} res.trySplit = func() (r Spliterator[T], b bool) { return r, b } return res } func sliceIterRec[T any, A ~[]T](slice A, lo int, hi int) (res Spliterator[T]) { res.tryAdvance = func(fn func(T)) bool { if lo >= hi { return false } fn(slice[lo]) lo++ return true } res.forEachRemaining = func(fn func(T)) { for ; lo < hi; lo++ { fn(slice[lo]) } } res.trySplit = func() (s Spliterator[T], b bool) { mid := (hi-lo)/2 + lo if mid != lo { s, b = sliceIterRec[T, A](slice, mid, hi), true // Modify current sliceIter before returning hi = mid } return s, b } return res } func SliceIter[T any, A ~[]T](slice A) (res Spliterator[T]) { return sliceIterRec[T, A](slice, 0, len(slice)) } func RuleIter[T any, A ~func() (T, bool)](rule A) (res Spliterator[T]) { res.tryAdvance = func(fn func(T)) bool { r, b := rule() if b { fn(r) } return b } res.forEachRemaining = func(fn func(T)) { for r, b := rule(); b; r, b = rule() { fn(r) } } res.trySplit = func() (s Spliterator[T], b bool) { return s, b } return res } func sign[T constraints.Integer](x T) int8 { if x > 0 { return 1 } else if x < 0 { return -1 } else { return 0 } } func RangeIter[T constraints.Integer](start, stop T, step T) (res Spliterator[T]) { // Check to ensure no infinite-loop if sign(stop-start)*sign(step) < 0 { return EmptyIter[T]() } res.tryAdvance = func(fn func(T)) bool { if start >= stop { return false } fn(start) start += step return true } res.forEachRemaining = func(fn func(T)) { for ; start < stop; start += step { fn(start) } } res.trySplit = func() (s Spliterator[T], b bool) { mid := (stop-start)/2 + start if mid != start { s, b = RangeIter[T](mid, stop, step), true // Modify stop for this iter stop = mid } return s, b } return res } func ChanIter[T any, C ~chan T](ch C) (res Spliterator[T]) { res.tryAdvance = func(fn func(T)) bool { v, ok := <-ch if ok { fn(v) } return ok } //res.forNextK = func(fn func(T)) { // for elem := range ch { // fn(elem) // } //} res.trySplit = func() (s Spliterator[T], b bool) { return s, b } return res } // Iterator is a generic iterator on a slice that lazily evaluates the next element in the slice. // This is used to lazily evaluate a slice's next value, allowing several applications of functional // methods on a single list while only incurring a O(1) memory overhead. type Iterator[T any] func() (T, bool) // Iter consumes a generic slice and generates a forward-advancing Iterator // // Iter is passed a copy of the slice. This does not copy the contents of the slice, but the size of // the slice is fixed. Therefore, modifications of element the internal slice will affect the Iterator func Iter[T any, A ~[]T](slice A) Iterator[T] { index := 0 return func() (t T, b bool) { if index >= len(slice) { return t, b // b is false here } index++ return slice[index-1], true } } // ReverseIter consumes a generic slice and generates a reverse-advancing Iterator func ReverseIter[T any, A ~[]T](slice A) Iterator[T] { index := len(slice) - 1 return func() (t T, b bool) { if index < 0 || index >= len(slice) { return t, b // b is false here } index-- return slice[index+1], true } } // Slice converts a generic Iterator to a slice of the appropriate type func Slice[T any](iter Iterator[T]) []T { res := make([]T, 0) for val, ok := iter(); ok; val, ok = iter() { res = append(res, val) } return res } // Next is an alias for advancing the Iterator func Next[T any](iter Iterator[T]) (T, bool) { return iter() } ================================================ FILE: spliterator_test.go ================================================ package functools import ( "reflect" "testing" ) type iterTestCase[T any] struct { name string slice []T } func TestIter(t *testing.T) { t.Run("integer", func(t *testing.T) { cases := []iterTestCase[int]{ { "integer1", []int{1, 2, 3}, }, { "integer2", []int{100, 0, -1, -1000}, }, { "empty slice", []int{}, }, } for _, c := range cases { iter := Iter(c.slice) for i, _ := range c.slice { val, ok := Next(iter) if !ok { t.Errorf("got %v, want %v", nil, c.slice[i]) continue } if val != c.slice[i] { t.Errorf("got %v, want %v", val, c.slice[i]) } } // Make sure iter is empty val, ok := Next(iter) if ok { t.Errorf("got %v, want %v", val, nil) } } }) t.Run("slice of string slices", func(t *testing.T) { cases := []iterTestCase[[]string]{ { "empty string slice of slices", [][]string{}, }, { "string1", [][]string{{"asdf", "rakeeb", "gopher"}, {"kevin", "trevor"}}, }, } for _, c := range cases { iter := Iter(c.slice) for i, _ := range c.slice { val, ok := Next(iter) if !ok { t.Errorf("got %v, want %v", nil, c.slice[i]) } if !reflect.DeepEqual(val, c.slice[i]) { t.Errorf("got %v, want %v", val, c.slice[i]) } } // Make sure iter is empty val, ok := Next(iter) if ok { t.Errorf("got %v, want %v", val, nil) } } }) } func TestReverseIter(t *testing.T) { t.Run("integer", func(t *testing.T) { cases := []iterTestCase[int]{ { "integer1", []int{1, 2, 3}, }, { "integer2", []int{100, 0, -1, -1000}, }, { "empty slice", []int{}, }, } for _, c := range cases { iter := ReverseIter(c.slice) for i := len(c.slice) - 1; i >= 0; i-- { val, ok := Next(iter) if !ok { t.Errorf("got %v, want %v", nil, c.slice[i]) } if val != c.slice[i] { t.Errorf("got %v, want %v", val, c.slice[i]) } } // Make sure iter is empty val, ok := Next(iter) if ok { t.Errorf("got %v, want %v", val, nil) } } }) t.Run("slice of string slices", func(t *testing.T) { cases := []iterTestCase[[]string]{ { "empty string slice of slices", [][]string{}, }, { "string1", [][]string{{"asdf", "rakeeb", "gopher"}, {"kevin", "trevor"}}, }, } for _, c := range cases { iter := ReverseIter(c.slice) for i := len(c.slice) - 1; i >= 0; i-- { val, ok := Next(iter) if !ok { t.Errorf("got %v, want %v", nil, c.slice[i]) } if !reflect.DeepEqual(val, c.slice[i]) { t.Errorf("got %v, want %v", val, c.slice[i]) } } // Make sure iter is empty val, ok := Next(iter) if ok { t.Errorf("got %v, want %v", val, nil) } } }) } func reverse[T any](slice []T) { start := 0 last := len(slice) - 1 for start < last { slice[start], slice[last] = slice[last], slice[start] start++ last-- } } func TestSlice(t *testing.T) { t.Run("integer", func(t *testing.T) { cases := []iterTestCase[int]{ { "integer1", []int{1, 2, 3}, }, { "integer2", []int{100, 0, -1, -1000}, }, { "empty slice", []int{}, }, } for _, c := range cases { slice := Slice(Iter(c.slice)) if !reflect.DeepEqual(slice, c.slice) { t.Errorf("got %v, want %v", slice, c.slice) } } for _, c := range cases { slice := Slice(ReverseIter(c.slice)) reverse(slice) if !reflect.DeepEqual(slice, c.slice) { t.Errorf("got %v, want %v", slice, c.slice) } } }) t.Run("slice of string slices", func(t *testing.T) { cases := []iterTestCase[[]string]{ { "empty string slice of slices", [][]string{}, }, { "string1", [][]string{{"asdf", "rakeeb", "gopher"}, {"kevin", "trevor"}}, }, } for _, c := range cases { slice := Slice(Iter(c.slice)) if !reflect.DeepEqual(slice, c.slice) { t.Errorf("got %v, want %v", slice, c.slice) } } for _, c := range cases { slice := Slice(ReverseIter(c.slice)) reverse(slice) if !reflect.DeepEqual(slice, c.slice) { t.Errorf("got %v, want %v", slice, c.slice) } } }) } ================================================ FILE: stream.go ================================================ package functools const ( SIZED = 1 << iota SORTED DISTINCT ORDERED UNORDERED ) type StreamStage[T any] interface { spliterator() Spliterator[T] isStateful() bool getParallelism() int characteristics() uint opEvalParallelLazy(int) } // StatelessOp struct embedding type InheritUpstream[T any] struct { upstream *StreamStage[T] } func (s InheritUpstream[T]) getParallelism() int { return (*s.upstream).getParallelism() } func (s InheritUpstream[T]) characteristics() uint { return (*s.upstream).characteristics() } func (s InheritUpstream[T]) opEvalParallelLazy(n int) { (*s.upstream).opEvalParallelLazy(n) } type StatelessOp struct{} func (s StatelessOp) isStateful() bool { return false } // SourceStage definition type SourceStage[T any] struct { StatelessOp src Spliterator[T] parallelism int } func Stream[T any](spliterator Spliterator[T]) StreamStage[T] { return SourceStage[T]{src: spliterator} } func ParallelStream[T any](spliterator Spliterator[T], parallelism int) StreamStage[T] { return SourceStage[T]{src: spliterator, parallelism: parallelism} } func (s SourceStage[T]) spliterator() Spliterator[T] { return s.src } func (s SourceStage[T]) getParallelism() int { return s.parallelism } func (s SourceStage[T]) characteristics() uint { return s.src.characteristics } func (s SourceStage[T]) opEvalParallelLazy(n int) { } // All this stuff should probably go into a separate file // Helpers func UpstreamToBuffer[T any](src StreamStage[T]) []T { slice := make([]T, 0) src.spliterator().forEachRemaining(func(e T) { slice = append(slice, e) }) return slice } // Map type MapOp[TIn any, TOut any] struct { StatelessOp InheritUpstream[TIn] mapper func(TIn) TOut } func Map[TIn any, TOut any](mapper func(TIn) TOut, upstream StreamStage[TIn]) StreamStage[TOut] { return MapOp[TIn, TOut]{ StatelessOp{}, InheritUpstream[TIn]{upstream: &upstream}, mapper, } } func mapSpliterator[T any, O any](mapper func(T) O, src Spliterator[T]) (res Spliterator[O]) { res.tryAdvance = func(fn func(O)) bool { wrapper_fn := func(e T) { v := mapper(e) fn(v) } return src.tryAdvance(wrapper_fn) } res.forEachRemaining = func(fn func(O)) { wrapper_fn := func(e T) { v := mapper(e) fn(v) } src.forEachRemaining(wrapper_fn) } // Recursive split!!! res.trySplit = func() (Spliterator[O], bool) { r, b := src.trySplit() if !b { return Spliterator[O]{}, false } else { return mapSpliterator[T, O](mapper, r), true } } return res } func (m MapOp[TIn, TOut]) spliterator() (res Spliterator[TOut]) { s := (*m.InheritUpstream.upstream).spliterator() return mapSpliterator[TIn, TOut](m.mapper, s) } // SortOp type SortOp[T any] struct { InheritUpstream[T] cmp func(T, T) bool } func Sort[T any](cmp func(T, T) bool, upstream StreamStage[T]) StreamStage[T] { return SortOp[T]{ InheritUpstream[T]{upstream: &upstream}, cmp, } } func quicksort[T any](cmp func(T, T) bool, slice []T) { for i, _ := range slice { min_so_far := slice[i] min_ind := i for j := i + 1; j < len(slice); j++ { if cmp(slice[j], min_so_far) { min_so_far = slice[j] min_ind = j } } tmp := slice[i] slice[i] = slice[min_ind] slice[min_ind] = tmp } } func (m SortOp[T]) spliteratorRec(src Spliterator[T]) (res Spliterator[T]) { done := false buffer := make([]T, 0, 2) index := 0 res.tryAdvance = func(fn func(T)) bool { if !done { src.forEachRemaining(func(e T) { buffer = append(buffer, e) }) quicksort(m.cmp, buffer) done = true } if index >= len(buffer) { return false } fn(buffer[index]) index++ return true } res.forEachRemaining = func(fn func(T)) { if !done { src.forEachRemaining(func(e T) { buffer = append(buffer, e) }) quicksort(m.cmp, buffer) done = true } for _, x := range buffer { fn(x) } } res.trySplit = func() (Spliterator[T], bool) { r, b := src.trySplit() if !b { return r, b } return m.spliteratorRec(r), b } return res } func (m SortOp[T]) spliterator() (res Spliterator[T]) { s := (*m.upstream).spliterator() return m.spliteratorRec(s) } func (s SortOp[T]) isStateful() bool { return true } func (s SortOp[T]) characteristics() uint { return (*s.upstream).characteristics() | SIZED | SORTED | ORDERED } func (s SortOp[T]) opEvalParallelLazy(n int) { (*s.upstream).opEvalParallelLazy(n) } ================================================ FILE: stream_test.go ================================================ package functools import ( "testing" ) func isPrime(n int) bool { if n <= 1 { return false } for i := 2; i < n; i++ { if n%i == 0 { return false } } return true } func TestAny(t *testing.T) { slice := make([]int, 10000000) for i, _ := range slice { slice[i] = i } iter := ParallelStream(SliceIter(slice), 5) // s2 := Sort(func(x int, y int) bool { return x < y }, s1) print(Any(isPrime, iter)) } func BenchmarkMap(b *testing.B) { slice := make([]int, 10000000) for i, _ := range slice { slice[i] = i } iter := ParallelStream(SliceIter(slice), 100) s1 := Map(func(e int) int { return e * -1 }, iter) // s2 := Sort(func(x int, y int) bool { return x < y }, s1) Sum(s1) } func BenchmarkMapSeq(b *testing.B) { slice := make([]int, 10000000) for i, _ := range slice { slice[i] = i } iter := Stream(SliceIter(slice)) s1 := Map(func(e int) int { return e * -1 }, iter) // s2 := Sort(func(x int, y int) bool { return x < y }, s1) Sum(s1) } func BenchmarkMapFor(b *testing.B) { slice := make([]int, 10000000) for i, _ := range slice { slice[i] = i } for i, _ := range slice { slice[i] *= -1 } res := 0 for _, v := range slice { res += v } } ================================================ FILE: terminals.go ================================================ // Terminal ops package functools import "sync" // Helpers func buildNSplits[T any](n uint32, src Spliterator[T]) []Spliterator[T] { if n <= 1 { return []Spliterator[T]{src} } // Round N down to a power of 2 var mask uint32 = 1 << 31 for n&mask == 0 { mask >>= 1 } // Alloc results slice res := make([]Spliterator[T], 0, mask) // In-order traversal of split tree var buildNSplitsRec func(uint32, Spliterator[T]) buildNSplitsRec = func(n uint32, src Spliterator[T]) { if n == 1 { res = append(res, src) } else { split, ok := src.trySplit() buildNSplitsRec(n/2, src) if ok { buildNSplitsRec(n/2, split) } } } buildNSplitsRec(mask, src) return res } // ForEach // TODO: figure out if you can abstract most of this. opEvalParallelLazy, buildNSplits, etc. always happen so we might be able to make ForEachOp implement a TerminalOp interface and abstract these func ForEach[T any](fn func(T), stream StreamStage[T]) { n := stream.getParallelism() if n <= 1 { stream.spliterator().forEachRemaining(fn) } else { // Evaluate up to last stateful op stream.opEvalParallelLazy(n) // Get n splits splits := buildNSplits(uint32(n), stream.spliterator()) n = len(splits) // Perform go-routines var wg sync.WaitGroup for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() // TODO: abstract into evalSequential so you don't need to rewrite this everytime splits[i].forEachRemaining(fn) }(i) } wg.Wait() } } // Summable encompasses all builtin types with the + operator defined on them or any type aliases // of these types type Summable interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } // Sum func Sum[T Summable](stream StreamStage[T]) T { n := stream.getParallelism() if n <= 1 { var res T stream.spliterator().forEachRemaining(func(e T) { res += e }) return res } else { // Evaluate up to last stateful op stream.opEvalParallelLazy(n) // Get n splits splits := buildNSplits(uint32(n), stream.spliterator()) n = len(splits) var wg sync.WaitGroup var mutex sync.Mutex var res T for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() var tmp T splits[i].forEachRemaining(func(e T) { tmp += e }) mutex.Lock() res += tmp mutex.Unlock() }(i) } wg.Wait() return res } } // Any func Any[T any](pred func(T) bool, stream StreamStage[T]) bool { n := stream.getParallelism() res := false if n <= 1 { wrapPred := func(e T) { if pred(e) { res = true } } s := stream.spliterator() for ok := s.tryAdvance(wrapPred); ok && !res; ok = s.tryAdvance(wrapPred) { } return res } else { // Evaluate up to last stateful op stream.opEvalParallelLazy(n) // Get n splits splits := buildNSplits(uint32(n), stream.spliterator()) n = len(splits) var wg sync.WaitGroup var mutex sync.Mutex for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() tmp := false wrapPred := func(e T) { if pred(e) { tmp = true } } for ok := splits[i].tryAdvance(wrapPred); ok && !tmp && !res; ok = splits[i].tryAdvance(wrapPred) { } if tmp { mutex.Lock() res = tmp mutex.Unlock() } }(i) } wg.Wait() return res } } // CollectSlice // Reduce //// All consumes a slice of a generic type and applies the predicate to each element in the slice. //// All return true if and only if no element returns false after applying the predicate. //// //// Vacuously, empty slices return true regardless of the predicate. //// //// predicate should be error-safe. It should handle any errors internally and return only a bool. //// If other arguments are required by predicate, predicate should be made a closure with the appropriate //// variables referenced. //func All[T any, A ~[]T](slice A, predicate func(T) bool) bool { // for _, v := range slice { // if !predicate(v) { // return false // } // } // return true //} // //// Any consumes a slice of a generic type and applies the predicate to each element in the slice. //// If any element returns true after applying the predicate, Any returns true. //// //// Vacuously, empty slices return false regardless of the predicate. //// //// predicate should be error-safe. It should handle any errors internally and return only a bool. //// If other arguments are required by predicate, predicate should be made a closure with the appropriate //// variables referenced. //func Any[T any, A ~[]T](slice A, predicate func(T) bool) bool { // for _, v := range slice { // if predicate(v) { // return true // } // } // return false //} // // //// Sum consumes a slice of a Summable type and sums the elements //// //// Vacuously, empty slices return the zero value of the provided Summable //func Sum[S Summable, A ~[]S](slice A) S { // var res S // for _, v := range slice { // res += v // } // return res //} ================================================ FILE: terminals_test.go ================================================ package functools // All tests //func TestGeqAll(t *testing.T) { // slice := []int{100, 25, 20, 31, 30} // if All(slice, func(val int) bool { return val >= 21 }) { // t.Errorf("TestGeqAll with %v was incorrect, got: %v, expected: %v", slice, true, false) // } // slice[2] = 21 // if !All(slice, func(val int) bool { return val >= 21 }) { // t.Errorf("TestGeqAll with %v was incorrect, got: %v, expected: %v", slice, false, true) // } //} // //// Any tests //func TestLtAny(t *testing.T) { // slice := []int{20, 31, 22} // // if !Any(slice, func(val int) bool { return val < 21 }) { // t.Errorf("TestLtAny with %v was incorrect, got: %v, expected: %v", slice, false, true) // } // slice[0] = 21 // if Any(slice, func(val int) bool { return val < 21 }) { // t.Errorf("TestLtAny with %v was incorrect, got: %v, expected: %v", slice, true, false) // } //} // //func TestIntSum(t *testing.T) { // slice := []int{1, 2, 3} // res := Sum(slice) // expect := 6 // // if res != expect { // t.Errorf("TestIntSum was incorrect, got: %d, expected: %d", res, expect) // } //} // //func TestUintptrSum(t *testing.T) { // slice := []uintptr{1, 2, 3} // res := Sum(slice) // expect := uintptr(6) // // if res != expect { // t.Errorf("TestUintptrSum was incorrect, got: %d, expected: %d", res, expect) // } //} // //func TestFloatSum(t *testing.T) { // slice := []float64{0.668, 0.666, 0.666} // res := Sum(slice) // expect := 2.0 // // if res != expect { // t.Errorf("TestFloatSum was incorrect, got: %f, expected: %f", res, expect) // } //} // //func TestStringSum(t *testing.T) { // slice := []string{"a", "b", "c"} // res := Sum(slice) // expect := "abc" // // if res != expect { // t.Errorf("TestStringSum was incorrect, got: %s, expected: %s", res, expect) // } //} // //func TestByteSum(t *testing.T) { // slice := []byte{1, 2, 3} // res := Sum(slice) // expect := byte(6) // // if res != expect { // t.Errorf("TestByteSum was incorrect, got: %d, expected: %d", res, expect) // } //}