├── .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 /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | stable: false 20 | go-version: 1.18.0-beta1 21 | 22 | - name: Build 23 | run: go build -v ./... 24 | 25 | - name: Test 26 | run: go test -v ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Project files 18 | .idea/ 19 | java_tests/ 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # functools Contributing guide 2 | 3 | Thank you for considering contributing to functools! To get an overview of the project, read the [README](README.md) and peruse through the codebase. 4 | 5 | ### Open an Issue 6 | 7 | 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. 8 | 9 | ### Fork and PR 10 | 11 | Make a fork of this repository and commit your approriate changes. Afterwards, submit a PR for review. 12 | 13 | ### PR merged! 14 | 15 | If approved, your work will be merged and added to the Golang functools library 🎉🎉 Thank you for your contribution! 16 | 17 | ### Questions? 18 | 19 | Email me at rakeeb.hossain1@gmail.com and I'll get back to you ASAP 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rakeeb Hossain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # functools 2 | 3 | [![Go](https://github.com/rakeeb-hossain/functools/actions/workflows/go.yml/badge.svg)](https://github.com/rakeeb-hossain/functools/actions/workflows/go.yml) 4 | 5 | functools is a simple Go library that brings you your favourite functional paradigms without sacrificing type-safety using 6 | `interface{}` or `reflect` 7 | 8 | Made possible by Go 1.18 using the newly introduced generics. 9 | 10 | ## Features 11 | 12 | - Any 13 | - All 14 | - Count 15 | - Filter 16 | - ForEach 17 | - Map 18 | - Reduce 19 | - ReduceRight 20 | - Sum 21 | - Chunk 22 | 23 | ## Installation 24 | 25 | `go get -u github.com/rakeeb-hossain/functools` 26 | 27 | ## Usage 28 | 29 | ```go 30 | import ( 31 | "github.com/rakeeb-hossain/functools" 32 | "fmt" 33 | ) 34 | 35 | type User struct { 36 | username string 37 | hasPortfolio bool 38 | } 39 | 40 | var users = []User{ 41 | {"gopher", true}, 42 | {"rakeeb", false}, 43 | {"jack", true}} 44 | 45 | func main() { 46 | // Count users with linked portfolios 47 | fmt.Printf("num users with linked portfolios: %d", 48 | functools.Count(users, func(u User) bool { return u.hasPortfolio })) 49 | 50 | // Print usernames of users with linked portfolios 51 | functools.ForEach( 52 | functools.Filter(users, func(u User) bool { return u.hasPortfolio }), 53 | func(u User) { fmt.Printf("%s has a linked portfolio\n", u.username) }) 54 | } 55 | ``` 56 | 57 | ## Documentation 58 | 59 | https://pkg.go.dev does not yet support Go 1.18 packages that use generics: https://github.com/golang/go/issues/48264 60 | 61 | For now, documentation is provided via comments and by running `go doc -all` from the package directory. 62 | 63 | ## Contributing 64 | 65 | Please see [CONTRIBUTING](CONTRIBUTING.md) 66 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package functools provides type-safe functional helpers using Go 1.18 generics. 2 | // 3 | // functools is intended to allow writing concise functionally-inspired code without sacrificing performance or type-safety, 4 | // as is the case for functional libraries pre-Go generics that had to use `interface{}` or `reflect`. 5 | 6 | package functools 7 | -------------------------------------------------------------------------------- /examples/error-handling/error_handling.go: -------------------------------------------------------------------------------- 1 | // This showcases how we can handle errors using functools helpers, despite there being no explicit error 2 | // return types by the functools functions. We make use of passing errors via closures. 3 | // 4 | // This is more explicit than it likely would be in practice just to showcase what safe handling capabilities 5 | // this functools still enables. In practice, checking for errant input as a pre-step before dividing would likely 6 | // be more effective. 7 | package main 8 | 9 | import ( 10 | "errors" 11 | "github.com/rakeeb-hossain/functools" 12 | "log" 13 | ) 14 | 15 | type fraction struct { 16 | dividend int 17 | divisor int 18 | } 19 | 20 | func main() { 21 | fractions := []fraction{{5, 1}, {3, 6}, {2, 0}} 22 | 23 | // We handle errors by populating an error which we pass to our mapper function via a closure. 24 | // We also return a pointer to a float64 instead of a float64 itself, so we can handle nil types 25 | // in case we encounter an error. 26 | var err error 27 | safeDivide := func(f fraction) *float64 { 28 | if f.divisor == 0 { 29 | err = errors.New("cannot divide by 0") 30 | return nil 31 | } 32 | res := float64(f.dividend) / float64(f.divisor) 33 | return &res 34 | } 35 | rationalResults := functools.Map(fractions, safeDivide) 36 | if err != nil { 37 | log.Println(err) 38 | } 39 | 40 | // We can sum the safe rational results using a custom Reduce function 41 | res := functools.Reduce(rationalResults, 0.0, func(accum float64, n *float64) float64 { 42 | if n == nil { 43 | return accum 44 | } 45 | return accum + *n 46 | }) 47 | 48 | log.Printf("the safe sum of fractions %v is %f\n", fractions, res) 49 | } 50 | -------------------------------------------------------------------------------- /examples/portfolio/portfolio.go: -------------------------------------------------------------------------------- 1 | // This showcases several applications of functools helpers to a store of user objects and their associated 2 | // portfolio holdings. 3 | package main 4 | 5 | import ( 6 | "github.com/rakeeb-hossain/functools" 7 | "log" 8 | ) 9 | 10 | type user struct { 11 | username string 12 | age int 13 | hasPortfolio bool 14 | } 15 | 16 | type holding struct { 17 | ticker string 18 | boughtTime int 19 | quantity float64 20 | price float64 21 | } 22 | 23 | type portfolio struct { 24 | holdings []holding 25 | } 26 | 27 | var ( 28 | users = []user{ 29 | {"gopher", 21, true}, 30 | {"rakeeb", 20, false}, 31 | {"jack", 22, true}} 32 | 33 | usersPortfolioMap = map[string]portfolio{ 34 | "gopher": {[]holding{{"TSLA", 1639768692, 4.5, 1000}, {"ABNB", 1639163892, 2.5, 200}}}, 35 | "jack": {[]holding{{"BTC", 1512933492, 5, 1000}, {"ETH", 1639768692, 10, 100}}}} 36 | ) 37 | 38 | func main() { 39 | // Count users with linked portfolios 40 | log.Printf("num users with linked portfolios: %d", functools.Count(users, func(u user) bool { return u.hasPortfolio })) 41 | 42 | // Print usernames of users with linked portfolios 43 | functools.ForEach( 44 | functools.Filter(users, func(u user) bool { return u.hasPortfolio }), 45 | func(u user) { log.Printf("%s has a linked portfolio\n", u.username) }, 46 | ) 47 | 48 | // For users with connected portfolios, get portfolio values 49 | usersWithPortfolio := functools.Filter(users, func(u user) bool { return u.hasPortfolio }) 50 | userPortfolioValues := functools.Map(usersWithPortfolio, func(u user) float64 { 51 | return functools.Reduce(usersPortfolioMap[u.username].holdings, 0, func(accum float64, h holding) float64 { 52 | return accum + h.quantity*h.price 53 | }) 54 | }) 55 | 56 | for i, _ := range usersWithPortfolio { 57 | log.Printf("user %s has portfolio value %f\n", usersWithPortfolio[i].username, userPortfolioValues[i]) 58 | } 59 | 60 | // Get total price of assets in all connected portfolios 61 | totalVal := functools.Sum(userPortfolioValues) 62 | log.Printf("total asset value: %f", totalVal) 63 | } 64 | -------------------------------------------------------------------------------- /functionals.go: -------------------------------------------------------------------------------- 1 | // Contains classic generic functional methods 2 | 3 | package functools 4 | 5 | // 6 | //// Map consumes a slice of a generic type and returns a slice with the supplied mapping function 7 | //// applied to each element. 8 | //// 9 | //// mapper should be error-safe. It should handle any errors internally and return the desired type. 10 | //// If other arguments are required by mapper, mapper should be made a closure with the appropriate 11 | //// variables referenced. 12 | //func Map_[A any, B any](mapper func(A) B, iter Spliterator[A]) (res Spliterator[B]) { 13 | // res.tryAdvance = func(fn func(B)) bool { 14 | // _mapper := func(a A) { 15 | // fn(mapper(a)) 16 | // } 17 | // return iter.tryAdvance(_mapper) 18 | // } 19 | // // res.trySplit = iter.trySplit 20 | // 21 | // return res 22 | //} 23 | // 24 | //// Stateful op 25 | // 26 | //func Sorted[T any](iter Spliterator[T]) (res Spliterator[T]) { 27 | // return res 28 | //} 29 | // 30 | ////func ChunkIter[T any](iter Iterator[T], len int) Iterator[[]T] { 31 | //// return func() (lst []T, b bool) { 32 | //// res, ok := Next(iter) 33 | //// if !ok { 34 | //// return lst, ok 35 | //// } 36 | //// 37 | //// lst = make([]T, len) 38 | //// lst[0] = res 39 | //// for i := 1; i < len; i++ { 40 | //// res, ok := Next(iter) 41 | //// if !ok { 42 | //// return lst, true 43 | //// } 44 | //// lst[i] = res 45 | //// } 46 | //// return lst, true 47 | //// } 48 | ////} 49 | // 50 | //// Filter consumes a slice of a generic type and returns a slice with only the elements which returned true after 51 | //// applying the predicate. 52 | //// 53 | //// Elements are returned in the same order that they were supplied in the slice. 54 | //// 55 | //// predicate should be error-safe. It should handle any errors internally and return only a bool. 56 | //// If other arguments are required by predicate, predicate should be made a closure with the appropriate 57 | //// variables referenced. 58 | //func Filter[T any, A ~[]T](slice A, predicate func(T) bool) A { 59 | // res := make(A, 0, len(slice)) 60 | // for _, v := range slice { 61 | // if predicate(v) { 62 | // res = append(res, v) 63 | // } 64 | // } 65 | // return res 66 | //} 67 | // 68 | //func FilterIter[T any](iter Iterator[T], predicate func(T) bool) Iterator[T] { 69 | // return func() (t T, b bool) { 70 | // for val, ok := Next(iter); ok; val, ok = Next(iter) { 71 | // if !ok || predicate(val) { 72 | // return val, ok 73 | // } 74 | // } 75 | // return t, b // b is false here 76 | // } 77 | //} 78 | // 79 | //// Reduce consumes a slice of a generic type and an initial value. It reduces the slice to a single value by applying 80 | //// the binary reducer function to each successive element in the slice. 81 | //// 82 | //// Vacuously, empty slices return the initial value provided. 83 | //// 84 | //// reducer should be error-safe. It should handle any errors internally and return the desired type. 85 | //// If other arguments are required by reducer, reducer should be made a closure with the appropriate 86 | //// variables referenced. 87 | //func Reduce[T any, R any](iter Spliterator[T], initial R, reducer func(R, T) R) R { 88 | // accum := initial 89 | // return accum 90 | //} 91 | // 92 | //// ReduceRight consumes a slice of a generic type and an initial value. It 93 | //// reduces the slice to a single value by applying the binary reducer function 94 | //// to each element in the slice. ReduceRight differs from Reduce by iterating 95 | //// from the last element to the first element. 96 | //// 97 | //// Vacuously, empty slices return the initial value provided. 98 | //// 99 | //// reducer should error-safe. It should handle any errors internally and return 100 | //// the desired type. If other arguments are required by reducer, reducer should 101 | //// be made a closure with the appropriate variables referenced. 102 | //func ReduceRight[T any, A ~[]T, R any](slice A, initial R, reducer func(R, T) R) R { 103 | // accum := initial 104 | // for i := len(slice) - 1; i >= 0; i-- { 105 | // accum = reducer(accum, slice[i]) 106 | // } 107 | // return accum 108 | //} 109 | // 110 | //// ForEach applies fun to each element in slice 111 | //// 112 | //// fun should be error-safe and handle errors internally. If other arguments are required by predicate, 113 | //// predicate should be made a closure with the appropriate variables referenced. 114 | //func ForEach[T any, A ~[]T](slice A, fun func(T)) { 115 | // for _, v := range slice { 116 | // fun(v) 117 | // } 118 | //} 119 | // 120 | //// Count consumes a slice of a generic type and counts the elements that return true after applying 121 | //// the predicate. 122 | //// 123 | //// Vacuously, empty slices return 0 regardless of the predicate. 124 | //// 125 | //// predicate should be error-safe. It should handle any errors internally and return only a bool. 126 | //// If other arguments are required by predicate, predicate should be made a closure with the appropriate 127 | //// variables referenced. 128 | //func Count[T any, A ~[]T](slice A, predicate func(T) bool) int { 129 | // res := 0 130 | // for _, v := range slice { 131 | // if predicate(v) { 132 | // res++ 133 | // } 134 | // } 135 | // return res 136 | //} 137 | -------------------------------------------------------------------------------- /functionals_test.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | //type user struct { 4 | // age int 5 | //} 6 | // 7 | //// Filter tests 8 | //func TestGeqFilter(t *testing.T) { 9 | // slice := []user{{17}, {21}, {18}, {32}, {49}, {76}} 10 | // geqTwentyOne := func(u user) bool { return u.age >= 21 } 11 | // res := Map(Filter(slice, geqTwentyOne), func(u user) int { return u.age }) 12 | // expect := []int{21, 32, 49, 76} 13 | // 14 | // for i, _ := range res { 15 | // if i >= len(expect) || res[i] != expect[i] { 16 | // t.Errorf("TestGeqFilter was incorrect, got: %v, expected: %v", res, expect) 17 | // return 18 | // } 19 | // } 20 | // 21 | // mapper := func(val user) int { return val.age } 22 | // Map(func(val int) int { return val + 1 }, Map(mapper, SliceIter[user](slice))) 23 | //} 24 | // 25 | //// Map tests 26 | //func TestAddMap(t *testing.T) { 27 | // slice := []int{1, 2, 3} 28 | // adder := func(val int) int { return val + 1 } 29 | // res := Map(slice, adder) 30 | // expect := []int{2, 3, 4} 31 | // 32 | // for i, _ := range res { 33 | // if i >= len(expect) || res[i] != expect[i] { 34 | // t.Errorf("TestAddMap was incorrect, got: %v, expected: %v", res, expect) 35 | // return 36 | // } 37 | // } 38 | //} 39 | // 40 | //func TestAddMapIter(t *testing.T) { 41 | // slice := Iter([]int{1, 2, 3}) 42 | // adder := func(val int) int { return val + 1 } 43 | // res := Slice(MapIter(slice, adder)) 44 | // expect := []int{2, 3, 4} 45 | // 46 | // for i, _ := range res { 47 | // if i >= len(expect) || res[i] != expect[i] { 48 | // t.Errorf("TestAddMapIter was incorrect, got: %v, expected: %v", res, expect) 49 | // return 50 | // } 51 | // } 52 | //} 53 | // 54 | //func TestUserMap(t *testing.T) { 55 | // slice := []user{{32}, {29}, {42}} 56 | // ageTransformer := func(val user) int { return val.age } 57 | // res := Map[user, []user, int](slice, ageTransformer) 58 | // expect := []int{32, 29, 42} 59 | // 60 | // for i, _ := range res { 61 | // if i >= len(expect) || res[i] != expect[i] { 62 | // t.Errorf("TestUserMap was incorrect, got: %v, expected: %v", res, expect) 63 | // return 64 | // } 65 | // } 66 | //} 67 | // 68 | //// Reduce tests 69 | //func TestReduceSum(t *testing.T) { 70 | // slice := []int{1, 2, 3} 71 | // adder := func(a, b int) int { return a + b } 72 | // res := Reduce(slice, 0, adder) 73 | // expect := 6 74 | // 75 | // if res != expect { 76 | // t.Errorf("TestReduceSum was incorrect, got: %d, expected: %d", res, expect) 77 | // } 78 | //} 79 | // 80 | //func TestReduceUserAge(t *testing.T) { 81 | // slice := []user{{32}, {29}, {42}} 82 | // adder := func(accum int, val user) int { return accum + val.age } 83 | // res := Reduce[user, []user, int](slice, 0, adder) 84 | // expect := 103 85 | // 86 | // if res != expect { 87 | // t.Errorf("TestReduceUserAge was incorrect, got: %d, expected: %d", res, expect) 88 | // } 89 | //} 90 | // 91 | //// ReduceRight tests 92 | //type reduceRightCase[T any, R comparable] struct { 93 | // name string 94 | // slice []T 95 | // initial R 96 | // reducer func(R, T) R 97 | // want R 98 | //} 99 | // 100 | //func TestReduceRight(t *testing.T) { 101 | // t.Run("integers", func(t *testing.T) { 102 | // 103 | // cases := []reduceRightCase[int, int]{ 104 | // { 105 | // name: "addition", 106 | // slice: []int{1, 2, 3}, 107 | // initial: 0, 108 | // reducer: func(a, b int) int { return a + b }, 109 | // want: 6, 110 | // }, 111 | // { 112 | // name: "subtraction", 113 | // slice: []int{1, 2, 3}, 114 | // initial: 0, 115 | // reducer: func(a, b int) int { return a - b }, 116 | // want: -6, 117 | // }, 118 | // { 119 | // name: "multiplication", 120 | // slice: []int{1, 2, 3}, 121 | // initial: 1, 122 | // reducer: func(a, b int) int { return a * b }, 123 | // want: 6, 124 | // }, 125 | // } 126 | // 127 | // for _, c := range cases { 128 | // t.Run(c.name, func(t *testing.T) { 129 | // got := ReduceRight(c.slice, c.initial, c.reducer) 130 | // 131 | // if got != c.want { 132 | // t.Errorf("got %v, want %v", got, c.want) 133 | // } 134 | // }) 135 | // } 136 | // }) 137 | // 138 | // t.Run("integers and floats", func(t *testing.T) { 139 | // cases := []reduceRightCase[int, float64]{ 140 | // { 141 | // name: "division", 142 | // slice: []int{1, 2, 3}, 143 | // initial: 1.0, 144 | // reducer: func(accum float64, curr int) float64 { return float64(curr) / accum }, 145 | // want: 1.5, 146 | // }, 147 | // } 148 | // 149 | // for _, c := range cases { 150 | // t.Run(c.name, func(t *testing.T) { 151 | // got := ReduceRight(c.slice, c.initial, c.reducer) 152 | // 153 | // if got != c.want { 154 | // t.Errorf("got %v, want %v", got, c.want) 155 | // } 156 | // }) 157 | // } 158 | // }) 159 | //} 160 | // 161 | //// ForEach tests 162 | //func TestClosureForEach(t *testing.T) { 163 | // slice := []int{1, 2, 3} 164 | // res := 0 165 | // ForEach(slice, func(val int) { res += val }) 166 | // expect := 6 167 | // 168 | // if res != expect { 169 | // t.Errorf("TestClosureForEach was incorrect, got: %d, expected: %d", res, expect) 170 | // } 171 | //} 172 | // 173 | //// Count tests 174 | //func TestGeqCount(t *testing.T) { 175 | // slice := []int{1, 100, 200, 3, 14, 21, 32} 176 | // res := Count(slice, func(val int) bool { return val >= 21 }) 177 | // expected := 4 178 | // 179 | // if res != expected { 180 | // t.Errorf("TestLtAny with %v was incorrect, got: %d, expected: %d", slice, res, expected) 181 | // } 182 | //} 183 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rakeeb-hossain/functools 2 | 3 | go 1.18 4 | 5 | require golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4= 2 | golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 3 | -------------------------------------------------------------------------------- /spined_buffer.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const FirstBuffPower int = 4 8 | const MinSpineSize int = 2 // must be >= 1 9 | const SpineExtendCount int = 1 10 | 11 | type AbstractBuffer[T any] interface { 12 | Push(T) 13 | At(int) 14 | Flatten() 15 | Len() int 16 | Capacity() int 17 | } 18 | 19 | // SpinedBuffer is an optimization on a regular slice that doesn't require copying elements on re-sizing. 20 | // This has good performance in cases where an unknown size stream is being processed, since copying from 21 | // re-sizing is minimized. 22 | type SpinedBuffer[T any] struct { 23 | // Spine data structures 24 | // We optimistically assume everything will fit into currBuff in most cases 25 | currBuff []T 26 | spines [][]T 27 | 28 | // Spine state management 29 | sizeOfPrevBuffers []int 30 | spineIdx int 31 | flatIdx int 32 | sizePower int 33 | capacity int 34 | inflated bool 35 | } 36 | 37 | // Checks if copy is required and copies currBuff to spines 38 | func (s *SpinedBuffer[T]) inflateSpine() { 39 | if s.spineIdx == 0 && s.flatIdx == s.capacity { 40 | // Create spines 41 | s.spines = make([][]T, MinSpineSize) 42 | 43 | // Assign currBuff to first spine and set sizeOfPrevBuffers 44 | s.spines[0] = s.currBuff[:] // should be O(1) since just copying slice 45 | s.sizeOfPrevBuffers = make([]int, 1, MinSpineSize) 46 | s.sizeOfPrevBuffers[0] = s.flatIdx 47 | 48 | // Update subsequent spines 49 | for i := 1; i < MinSpineSize; i++ { 50 | s.sizePower++ 51 | s.spines[i] = make([]T, 0, 1<= 0 { 139 | if s.spineIdx == 0 { 140 | res = s.currBuff[index] 141 | } else { 142 | // binary-search for upper-bound; gives index of first elem in sizeOfPrevBuffers that is >= index 143 | // this index is guaranteed to be valid since sizeOfPrevBuffers last elem is s.flatIdx and index < s.flatIdx 144 | spineSizeIdx := upperBoundGuaranteed(index, s.sizeOfPrevBuffers) 145 | 146 | // Equality-case where index actually belongs to next spine 147 | if s.sizeOfPrevBuffers[spineSizeIdx] == index { 148 | res = s.spines[spineSizeIdx+1][0] 149 | } else { 150 | offset := index // case where index belongs to first spine so spineSizeIdx == 0 151 | if spineSizeIdx > 0 { 152 | offset = index - s.sizeOfPrevBuffers[spineSizeIdx-1] 153 | } 154 | res = s.spines[spineSizeIdx][offset] 155 | } 156 | 157 | } 158 | } 159 | return res 160 | } 161 | 162 | func (s SpinedBuffer[T]) PrintStats() { 163 | fmt.Printf("spineIdx: %d\n", s.spineIdx) 164 | fmt.Printf("flatIdx: %d\n", s.flatIdx) 165 | fmt.Printf("capacity: %d\n", s.capacity) 166 | fmt.Printf("sizePower: %d\n", s.sizePower) 167 | } 168 | 169 | func upperBoundGuaranteed(val int, arr []int) int { 170 | lo := 0 171 | hi := len(arr) 172 | for lo < hi { 173 | mid := (hi-lo)/2 + lo 174 | 175 | if arr[mid] >= val { 176 | hi = mid 177 | } else { 178 | lo = mid + 1 179 | } 180 | } 181 | return lo 182 | } 183 | -------------------------------------------------------------------------------- /spined_buffer_test.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSpinedBuffer_Push(t *testing.T) { 10 | NUM_TRIALS := 10000 11 | buff := CreateSpinedBuffer[int]() 12 | arr := make([]int, 0, NUM_TRIALS) 13 | 14 | for i := 1; i <= NUM_TRIALS; i++ { 15 | buff.Push(i) 16 | arr = append(arr, i) 17 | if !reflect.DeepEqual(arr, buff.Flatten()) { 18 | buff.PrintStats() 19 | t.Errorf("error at %d", i) 20 | } 21 | } 22 | } 23 | 24 | func TestSpinedBuffer_At(t *testing.T) { 25 | NUM_TRIALS := 10000 26 | buff := CreateSpinedBuffer[int]() 27 | 28 | for i := 1; i <= NUM_TRIALS; i++ { 29 | buff.Push(i) 30 | 31 | if buff.Len() != i { 32 | t.Errorf("error at %d", i) 33 | } 34 | 35 | r := rand.Int() % buff.Len() 36 | if buff.At(r) != (r + 1) { 37 | t.Errorf("error at index %d on iteration %d", r, i) 38 | } 39 | } 40 | } 41 | 42 | const BENCHMARK_SIZE int = 10000 43 | 44 | func BenchmarkCreateSpinedBuffer_Push(b *testing.B) { 45 | buff := CreateSpinedBuffer[int]() 46 | 47 | for i := 0; i < BENCHMARK_SIZE; i++ { 48 | buff.Push(i) 49 | } 50 | } 51 | 52 | func BenchmarkSlicePush(b *testing.B) { 53 | slice := make([]int, 0) 54 | 55 | for i := 0; i < BENCHMARK_SIZE; i++ { 56 | slice = append(slice, i) 57 | } 58 | } 59 | 60 | //func BenchmarkArrayPush(b *testing.B) { 61 | // arr := [BENCHMARK_SIZE]int{} 62 | // 63 | // for i := 0; i < BENCHMARK_SIZE; i++ { 64 | // arr[i] = i 65 | // } 66 | //} 67 | -------------------------------------------------------------------------------- /spliterator.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | import ( 4 | "golang.org/x/exp/constraints" 5 | ) 6 | 7 | type Spliterator[T any] struct { 8 | tryAdvance func(func(T)) bool 9 | forEachRemaining func(func(T)) 10 | trySplit func() (Spliterator[T], bool) 11 | characteristics uint 12 | } 13 | 14 | func EmptyIter[T any]() (res Spliterator[T]) { 15 | res.tryAdvance = func(func(T)) bool { 16 | return false 17 | } 18 | res.forEachRemaining = func(func(T)) {} 19 | res.trySplit = func() (r Spliterator[T], b bool) { 20 | return r, b 21 | } 22 | return res 23 | } 24 | 25 | func sliceIterRec[T any, A ~[]T](slice A, lo int, hi int) (res Spliterator[T]) { 26 | res.tryAdvance = func(fn func(T)) bool { 27 | if lo >= hi { 28 | return false 29 | } 30 | fn(slice[lo]) 31 | lo++ 32 | return true 33 | } 34 | 35 | res.forEachRemaining = func(fn func(T)) { 36 | for ; lo < hi; lo++ { 37 | fn(slice[lo]) 38 | } 39 | } 40 | 41 | res.trySplit = func() (s Spliterator[T], b bool) { 42 | mid := (hi-lo)/2 + lo 43 | if mid != lo { 44 | s, b = sliceIterRec[T, A](slice, mid, hi), true 45 | // Modify current sliceIter before returning 46 | hi = mid 47 | } 48 | return s, b 49 | } 50 | return res 51 | } 52 | 53 | func SliceIter[T any, A ~[]T](slice A) (res Spliterator[T]) { 54 | return sliceIterRec[T, A](slice, 0, len(slice)) 55 | } 56 | 57 | func RuleIter[T any, A ~func() (T, bool)](rule A) (res Spliterator[T]) { 58 | res.tryAdvance = func(fn func(T)) bool { 59 | r, b := rule() 60 | if b { 61 | fn(r) 62 | } 63 | return b 64 | } 65 | res.forEachRemaining = func(fn func(T)) { 66 | for r, b := rule(); b; r, b = rule() { 67 | fn(r) 68 | } 69 | } 70 | res.trySplit = func() (s Spliterator[T], b bool) { 71 | return s, b 72 | } 73 | return res 74 | } 75 | 76 | func sign[T constraints.Integer](x T) int8 { 77 | if x > 0 { 78 | return 1 79 | } else if x < 0 { 80 | return -1 81 | } else { 82 | return 0 83 | } 84 | } 85 | 86 | func RangeIter[T constraints.Integer](start, stop T, step T) (res Spliterator[T]) { 87 | // Check to ensure no infinite-loop 88 | if sign(stop-start)*sign(step) < 0 { 89 | return EmptyIter[T]() 90 | } 91 | 92 | res.tryAdvance = func(fn func(T)) bool { 93 | if start >= stop { 94 | return false 95 | } 96 | fn(start) 97 | start += step 98 | return true 99 | } 100 | res.forEachRemaining = func(fn func(T)) { 101 | for ; start < stop; start += step { 102 | fn(start) 103 | } 104 | } 105 | res.trySplit = func() (s Spliterator[T], b bool) { 106 | mid := (stop-start)/2 + start 107 | if mid != start { 108 | s, b = RangeIter[T](mid, stop, step), true 109 | // Modify stop for this iter 110 | stop = mid 111 | } 112 | return s, b 113 | } 114 | return res 115 | } 116 | 117 | func ChanIter[T any, C ~chan T](ch C) (res Spliterator[T]) { 118 | res.tryAdvance = func(fn func(T)) bool { 119 | v, ok := <-ch 120 | if ok { 121 | fn(v) 122 | } 123 | return ok 124 | } 125 | //res.forNextK = func(fn func(T)) { 126 | // for elem := range ch { 127 | // fn(elem) 128 | // } 129 | //} 130 | res.trySplit = func() (s Spliterator[T], b bool) { 131 | return s, b 132 | } 133 | return res 134 | } 135 | 136 | // Iterator is a generic iterator on a slice that lazily evaluates the next element in the slice. 137 | // This is used to lazily evaluate a slice's next value, allowing several applications of functional 138 | // methods on a single list while only incurring a O(1) memory overhead. 139 | type Iterator[T any] func() (T, bool) 140 | 141 | // Iter consumes a generic slice and generates a forward-advancing Iterator 142 | // 143 | // Iter is passed a copy of the slice. This does not copy the contents of the slice, but the size of 144 | // the slice is fixed. Therefore, modifications of element the internal slice will affect the Iterator 145 | func Iter[T any, A ~[]T](slice A) Iterator[T] { 146 | index := 0 147 | return func() (t T, b bool) { 148 | if index >= len(slice) { 149 | return t, b // b is false here 150 | } 151 | index++ 152 | return slice[index-1], true 153 | } 154 | } 155 | 156 | // ReverseIter consumes a generic slice and generates a reverse-advancing Iterator 157 | func ReverseIter[T any, A ~[]T](slice A) Iterator[T] { 158 | index := len(slice) - 1 159 | return func() (t T, b bool) { 160 | if index < 0 || index >= len(slice) { 161 | return t, b // b is false here 162 | } 163 | index-- 164 | return slice[index+1], true 165 | } 166 | } 167 | 168 | // Slice converts a generic Iterator to a slice of the appropriate type 169 | func Slice[T any](iter Iterator[T]) []T { 170 | res := make([]T, 0) 171 | for val, ok := iter(); ok; val, ok = iter() { 172 | res = append(res, val) 173 | } 174 | return res 175 | } 176 | 177 | // Next is an alias for advancing the Iterator 178 | func Next[T any](iter Iterator[T]) (T, bool) { 179 | return iter() 180 | } 181 | -------------------------------------------------------------------------------- /spliterator_test.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type iterTestCase[T any] struct { 9 | name string 10 | slice []T 11 | } 12 | 13 | func TestIter(t *testing.T) { 14 | t.Run("integer", func(t *testing.T) { 15 | cases := []iterTestCase[int]{ 16 | { 17 | "integer1", 18 | []int{1, 2, 3}, 19 | }, 20 | { 21 | "integer2", 22 | []int{100, 0, -1, -1000}, 23 | }, 24 | { 25 | "empty slice", 26 | []int{}, 27 | }, 28 | } 29 | 30 | for _, c := range cases { 31 | iter := Iter(c.slice) 32 | for i, _ := range c.slice { 33 | val, ok := Next(iter) 34 | if !ok { 35 | t.Errorf("got %v, want %v", nil, c.slice[i]) 36 | continue 37 | } 38 | if val != c.slice[i] { 39 | t.Errorf("got %v, want %v", val, c.slice[i]) 40 | } 41 | } 42 | 43 | // Make sure iter is empty 44 | val, ok := Next(iter) 45 | if ok { 46 | t.Errorf("got %v, want %v", val, nil) 47 | } 48 | } 49 | }) 50 | 51 | t.Run("slice of string slices", func(t *testing.T) { 52 | cases := []iterTestCase[[]string]{ 53 | { 54 | "empty string slice of slices", 55 | [][]string{}, 56 | }, 57 | { 58 | "string1", 59 | [][]string{{"asdf", "rakeeb", "gopher"}, {"kevin", "trevor"}}, 60 | }, 61 | } 62 | 63 | for _, c := range cases { 64 | iter := Iter(c.slice) 65 | 66 | for i, _ := range c.slice { 67 | val, ok := Next(iter) 68 | if !ok { 69 | t.Errorf("got %v, want %v", nil, c.slice[i]) 70 | } 71 | if !reflect.DeepEqual(val, c.slice[i]) { 72 | t.Errorf("got %v, want %v", val, c.slice[i]) 73 | } 74 | } 75 | 76 | // Make sure iter is empty 77 | val, ok := Next(iter) 78 | if ok { 79 | t.Errorf("got %v, want %v", val, nil) 80 | } 81 | } 82 | }) 83 | } 84 | 85 | func TestReverseIter(t *testing.T) { 86 | t.Run("integer", func(t *testing.T) { 87 | cases := []iterTestCase[int]{ 88 | { 89 | "integer1", 90 | []int{1, 2, 3}, 91 | }, 92 | { 93 | "integer2", 94 | []int{100, 0, -1, -1000}, 95 | }, 96 | { 97 | "empty slice", 98 | []int{}, 99 | }, 100 | } 101 | 102 | for _, c := range cases { 103 | iter := ReverseIter(c.slice) 104 | 105 | for i := len(c.slice) - 1; i >= 0; i-- { 106 | val, ok := Next(iter) 107 | if !ok { 108 | t.Errorf("got %v, want %v", nil, c.slice[i]) 109 | } 110 | if val != c.slice[i] { 111 | t.Errorf("got %v, want %v", val, c.slice[i]) 112 | } 113 | } 114 | 115 | // Make sure iter is empty 116 | val, ok := Next(iter) 117 | if ok { 118 | t.Errorf("got %v, want %v", val, nil) 119 | } 120 | } 121 | }) 122 | 123 | t.Run("slice of string slices", func(t *testing.T) { 124 | cases := []iterTestCase[[]string]{ 125 | { 126 | "empty string slice of slices", 127 | [][]string{}, 128 | }, 129 | { 130 | "string1", 131 | [][]string{{"asdf", "rakeeb", "gopher"}, {"kevin", "trevor"}}, 132 | }, 133 | } 134 | 135 | for _, c := range cases { 136 | iter := ReverseIter(c.slice) 137 | 138 | for i := len(c.slice) - 1; i >= 0; i-- { 139 | val, ok := Next(iter) 140 | if !ok { 141 | t.Errorf("got %v, want %v", nil, c.slice[i]) 142 | } 143 | if !reflect.DeepEqual(val, c.slice[i]) { 144 | t.Errorf("got %v, want %v", val, c.slice[i]) 145 | } 146 | } 147 | 148 | // Make sure iter is empty 149 | val, ok := Next(iter) 150 | if ok { 151 | t.Errorf("got %v, want %v", val, nil) 152 | } 153 | } 154 | }) 155 | } 156 | 157 | func reverse[T any](slice []T) { 158 | start := 0 159 | last := len(slice) - 1 160 | for start < last { 161 | slice[start], slice[last] = slice[last], slice[start] 162 | start++ 163 | last-- 164 | } 165 | } 166 | 167 | func TestSlice(t *testing.T) { 168 | t.Run("integer", func(t *testing.T) { 169 | cases := []iterTestCase[int]{ 170 | { 171 | "integer1", 172 | []int{1, 2, 3}, 173 | }, 174 | { 175 | "integer2", 176 | []int{100, 0, -1, -1000}, 177 | }, 178 | { 179 | "empty slice", 180 | []int{}, 181 | }, 182 | } 183 | 184 | for _, c := range cases { 185 | slice := Slice(Iter(c.slice)) 186 | 187 | if !reflect.DeepEqual(slice, c.slice) { 188 | t.Errorf("got %v, want %v", slice, c.slice) 189 | } 190 | } 191 | 192 | for _, c := range cases { 193 | slice := Slice(ReverseIter(c.slice)) 194 | reverse(slice) 195 | 196 | if !reflect.DeepEqual(slice, c.slice) { 197 | t.Errorf("got %v, want %v", slice, c.slice) 198 | } 199 | } 200 | }) 201 | 202 | t.Run("slice of string slices", func(t *testing.T) { 203 | cases := []iterTestCase[[]string]{ 204 | { 205 | "empty string slice of slices", 206 | [][]string{}, 207 | }, 208 | { 209 | "string1", 210 | [][]string{{"asdf", "rakeeb", "gopher"}, {"kevin", "trevor"}}, 211 | }, 212 | } 213 | 214 | for _, c := range cases { 215 | slice := Slice(Iter(c.slice)) 216 | 217 | if !reflect.DeepEqual(slice, c.slice) { 218 | t.Errorf("got %v, want %v", slice, c.slice) 219 | } 220 | } 221 | 222 | for _, c := range cases { 223 | slice := Slice(ReverseIter(c.slice)) 224 | reverse(slice) 225 | 226 | if !reflect.DeepEqual(slice, c.slice) { 227 | t.Errorf("got %v, want %v", slice, c.slice) 228 | } 229 | } 230 | }) 231 | } 232 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | const ( 4 | SIZED = 1 << iota 5 | SORTED 6 | DISTINCT 7 | ORDERED 8 | UNORDERED 9 | ) 10 | 11 | type StreamStage[T any] interface { 12 | spliterator() Spliterator[T] 13 | 14 | isStateful() bool 15 | getParallelism() int 16 | characteristics() uint 17 | 18 | opEvalParallelLazy(int) 19 | } 20 | 21 | // StatelessOp struct embedding 22 | type InheritUpstream[T any] struct { 23 | upstream *StreamStage[T] 24 | } 25 | 26 | func (s InheritUpstream[T]) getParallelism() int { 27 | return (*s.upstream).getParallelism() 28 | } 29 | 30 | func (s InheritUpstream[T]) characteristics() uint { 31 | return (*s.upstream).characteristics() 32 | } 33 | 34 | func (s InheritUpstream[T]) opEvalParallelLazy(n int) { 35 | (*s.upstream).opEvalParallelLazy(n) 36 | } 37 | 38 | type StatelessOp struct{} 39 | 40 | func (s StatelessOp) isStateful() bool { 41 | return false 42 | } 43 | 44 | // SourceStage definition 45 | type SourceStage[T any] struct { 46 | StatelessOp 47 | src Spliterator[T] 48 | parallelism int 49 | } 50 | 51 | func Stream[T any](spliterator Spliterator[T]) StreamStage[T] { 52 | return SourceStage[T]{src: spliterator} 53 | } 54 | 55 | func ParallelStream[T any](spliterator Spliterator[T], parallelism int) StreamStage[T] { 56 | return SourceStage[T]{src: spliterator, parallelism: parallelism} 57 | } 58 | 59 | func (s SourceStage[T]) spliterator() Spliterator[T] { 60 | return s.src 61 | } 62 | 63 | func (s SourceStage[T]) getParallelism() int { 64 | return s.parallelism 65 | } 66 | 67 | func (s SourceStage[T]) characteristics() uint { 68 | return s.src.characteristics 69 | } 70 | 71 | func (s SourceStage[T]) opEvalParallelLazy(n int) { 72 | 73 | } 74 | 75 | // All this stuff should probably go into a separate file 76 | 77 | // Helpers 78 | func UpstreamToBuffer[T any](src StreamStage[T]) []T { 79 | slice := make([]T, 0) 80 | src.spliterator().forEachRemaining(func(e T) { 81 | slice = append(slice, e) 82 | }) 83 | return slice 84 | } 85 | 86 | // Map 87 | type MapOp[TIn any, TOut any] struct { 88 | StatelessOp 89 | InheritUpstream[TIn] 90 | mapper func(TIn) TOut 91 | } 92 | 93 | func Map[TIn any, TOut any](mapper func(TIn) TOut, upstream StreamStage[TIn]) StreamStage[TOut] { 94 | return MapOp[TIn, TOut]{ 95 | StatelessOp{}, 96 | InheritUpstream[TIn]{upstream: &upstream}, 97 | mapper, 98 | } 99 | } 100 | 101 | func mapSpliterator[T any, O any](mapper func(T) O, src Spliterator[T]) (res Spliterator[O]) { 102 | res.tryAdvance = func(fn func(O)) bool { 103 | wrapper_fn := func(e T) { 104 | v := mapper(e) 105 | fn(v) 106 | } 107 | return src.tryAdvance(wrapper_fn) 108 | } 109 | res.forEachRemaining = func(fn func(O)) { 110 | wrapper_fn := func(e T) { 111 | v := mapper(e) 112 | fn(v) 113 | } 114 | src.forEachRemaining(wrapper_fn) 115 | } 116 | // Recursive split!!! 117 | res.trySplit = func() (Spliterator[O], bool) { 118 | r, b := src.trySplit() 119 | if !b { 120 | return Spliterator[O]{}, false 121 | } else { 122 | return mapSpliterator[T, O](mapper, r), true 123 | } 124 | } 125 | return res 126 | } 127 | 128 | func (m MapOp[TIn, TOut]) spliterator() (res Spliterator[TOut]) { 129 | s := (*m.InheritUpstream.upstream).spliterator() 130 | return mapSpliterator[TIn, TOut](m.mapper, s) 131 | } 132 | 133 | // SortOp 134 | type SortOp[T any] struct { 135 | InheritUpstream[T] 136 | cmp func(T, T) bool 137 | } 138 | 139 | func Sort[T any](cmp func(T, T) bool, upstream StreamStage[T]) StreamStage[T] { 140 | return SortOp[T]{ 141 | InheritUpstream[T]{upstream: &upstream}, 142 | cmp, 143 | } 144 | } 145 | 146 | func quicksort[T any](cmp func(T, T) bool, slice []T) { 147 | for i, _ := range slice { 148 | min_so_far := slice[i] 149 | min_ind := i 150 | for j := i + 1; j < len(slice); j++ { 151 | if cmp(slice[j], min_so_far) { 152 | min_so_far = slice[j] 153 | min_ind = j 154 | } 155 | } 156 | tmp := slice[i] 157 | slice[i] = slice[min_ind] 158 | slice[min_ind] = tmp 159 | } 160 | } 161 | 162 | func (m SortOp[T]) spliteratorRec(src Spliterator[T]) (res Spliterator[T]) { 163 | done := false 164 | buffer := make([]T, 0, 2) 165 | index := 0 166 | res.tryAdvance = func(fn func(T)) bool { 167 | if !done { 168 | src.forEachRemaining(func(e T) { 169 | buffer = append(buffer, e) 170 | }) 171 | quicksort(m.cmp, buffer) 172 | done = true 173 | } 174 | if index >= len(buffer) { 175 | return false 176 | } 177 | fn(buffer[index]) 178 | index++ 179 | return true 180 | } 181 | res.forEachRemaining = func(fn func(T)) { 182 | if !done { 183 | src.forEachRemaining(func(e T) { 184 | buffer = append(buffer, e) 185 | }) 186 | quicksort(m.cmp, buffer) 187 | done = true 188 | } 189 | for _, x := range buffer { 190 | fn(x) 191 | } 192 | } 193 | res.trySplit = func() (Spliterator[T], bool) { 194 | r, b := src.trySplit() 195 | if !b { 196 | return r, b 197 | } 198 | return m.spliteratorRec(r), b 199 | } 200 | return res 201 | } 202 | 203 | func (m SortOp[T]) spliterator() (res Spliterator[T]) { 204 | s := (*m.upstream).spliterator() 205 | return m.spliteratorRec(s) 206 | } 207 | 208 | func (s SortOp[T]) isStateful() bool { 209 | return true 210 | } 211 | 212 | func (s SortOp[T]) characteristics() uint { 213 | return (*s.upstream).characteristics() | SIZED | SORTED | ORDERED 214 | } 215 | 216 | func (s SortOp[T]) opEvalParallelLazy(n int) { 217 | (*s.upstream).opEvalParallelLazy(n) 218 | } 219 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func isPrime(n int) bool { 8 | if n <= 1 { 9 | return false 10 | } 11 | for i := 2; i < n; i++ { 12 | if n%i == 0 { 13 | return false 14 | } 15 | } 16 | return true 17 | } 18 | 19 | func TestAny(t *testing.T) { 20 | slice := make([]int, 10000000) 21 | for i, _ := range slice { 22 | slice[i] = i 23 | } 24 | iter := ParallelStream(SliceIter(slice), 5) 25 | // s2 := Sort(func(x int, y int) bool { return x < y }, s1) 26 | 27 | print(Any(isPrime, iter)) 28 | } 29 | 30 | func BenchmarkMap(b *testing.B) { 31 | slice := make([]int, 10000000) 32 | for i, _ := range slice { 33 | slice[i] = i 34 | } 35 | iter := ParallelStream(SliceIter(slice), 100) 36 | s1 := Map(func(e int) int { return e * -1 }, iter) 37 | // s2 := Sort(func(x int, y int) bool { return x < y }, s1) 38 | Sum(s1) 39 | } 40 | 41 | func BenchmarkMapSeq(b *testing.B) { 42 | slice := make([]int, 10000000) 43 | for i, _ := range slice { 44 | slice[i] = i 45 | } 46 | iter := Stream(SliceIter(slice)) 47 | s1 := Map(func(e int) int { return e * -1 }, iter) 48 | // s2 := Sort(func(x int, y int) bool { return x < y }, s1) 49 | Sum(s1) 50 | } 51 | 52 | func BenchmarkMapFor(b *testing.B) { 53 | slice := make([]int, 10000000) 54 | for i, _ := range slice { 55 | slice[i] = i 56 | } 57 | for i, _ := range slice { 58 | slice[i] *= -1 59 | } 60 | res := 0 61 | for _, v := range slice { 62 | res += v 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /terminals.go: -------------------------------------------------------------------------------- 1 | // Terminal ops 2 | 3 | package functools 4 | 5 | import "sync" 6 | 7 | // Helpers 8 | func buildNSplits[T any](n uint32, src Spliterator[T]) []Spliterator[T] { 9 | if n <= 1 { 10 | return []Spliterator[T]{src} 11 | } 12 | // Round N down to a power of 2 13 | var mask uint32 = 1 << 31 14 | for n&mask == 0 { 15 | mask >>= 1 16 | } 17 | // Alloc results slice 18 | res := make([]Spliterator[T], 0, mask) 19 | 20 | // In-order traversal of split tree 21 | var buildNSplitsRec func(uint32, Spliterator[T]) 22 | buildNSplitsRec = func(n uint32, src Spliterator[T]) { 23 | if n == 1 { 24 | res = append(res, src) 25 | } else { 26 | split, ok := src.trySplit() 27 | buildNSplitsRec(n/2, src) 28 | if ok { 29 | buildNSplitsRec(n/2, split) 30 | } 31 | } 32 | } 33 | buildNSplitsRec(mask, src) 34 | 35 | return res 36 | } 37 | 38 | // ForEach 39 | // 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 40 | func ForEach[T any](fn func(T), stream StreamStage[T]) { 41 | n := stream.getParallelism() 42 | if n <= 1 { 43 | stream.spliterator().forEachRemaining(fn) 44 | } else { 45 | // Evaluate up to last stateful op 46 | stream.opEvalParallelLazy(n) 47 | 48 | // Get n splits 49 | splits := buildNSplits(uint32(n), stream.spliterator()) 50 | n = len(splits) 51 | 52 | // Perform go-routines 53 | var wg sync.WaitGroup 54 | 55 | for i := 0; i < n; i++ { 56 | wg.Add(1) 57 | 58 | go func(i int) { 59 | defer wg.Done() 60 | // TODO: abstract into evalSequential so you don't need to rewrite this everytime 61 | splits[i].forEachRemaining(fn) 62 | }(i) 63 | } 64 | wg.Wait() 65 | } 66 | } 67 | 68 | // Summable encompasses all builtin types with the + operator defined on them or any type aliases 69 | // of these types 70 | type Summable interface { 71 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | 72 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | 73 | ~float32 | ~float64 | 74 | ~string 75 | } 76 | 77 | // Sum 78 | func Sum[T Summable](stream StreamStage[T]) T { 79 | n := stream.getParallelism() 80 | if n <= 1 { 81 | var res T 82 | stream.spliterator().forEachRemaining(func(e T) { 83 | res += e 84 | }) 85 | return res 86 | } else { 87 | // Evaluate up to last stateful op 88 | stream.opEvalParallelLazy(n) 89 | 90 | // Get n splits 91 | splits := buildNSplits(uint32(n), stream.spliterator()) 92 | n = len(splits) 93 | 94 | var wg sync.WaitGroup 95 | var mutex sync.Mutex 96 | var res T 97 | 98 | for i := 0; i < n; i++ { 99 | wg.Add(1) 100 | 101 | go func(i int) { 102 | defer wg.Done() 103 | 104 | var tmp T 105 | splits[i].forEachRemaining(func(e T) { 106 | tmp += e 107 | }) 108 | mutex.Lock() 109 | res += tmp 110 | mutex.Unlock() 111 | }(i) 112 | } 113 | wg.Wait() 114 | 115 | return res 116 | } 117 | } 118 | 119 | // Any 120 | func Any[T any](pred func(T) bool, stream StreamStage[T]) bool { 121 | n := stream.getParallelism() 122 | res := false 123 | 124 | if n <= 1 { 125 | wrapPred := func(e T) { 126 | if pred(e) { 127 | res = true 128 | } 129 | } 130 | s := stream.spliterator() 131 | for ok := s.tryAdvance(wrapPred); ok && !res; ok = s.tryAdvance(wrapPred) { 132 | } 133 | return res 134 | } else { 135 | // Evaluate up to last stateful op 136 | stream.opEvalParallelLazy(n) 137 | 138 | // Get n splits 139 | splits := buildNSplits(uint32(n), stream.spliterator()) 140 | n = len(splits) 141 | 142 | var wg sync.WaitGroup 143 | var mutex sync.Mutex 144 | 145 | for i := 0; i < n; i++ { 146 | wg.Add(1) 147 | 148 | go func(i int) { 149 | defer wg.Done() 150 | 151 | tmp := false 152 | wrapPred := func(e T) { 153 | if pred(e) { 154 | tmp = true 155 | } 156 | } 157 | 158 | for ok := splits[i].tryAdvance(wrapPred); ok && !tmp && !res; ok = splits[i].tryAdvance(wrapPred) { 159 | } 160 | 161 | if tmp { 162 | mutex.Lock() 163 | res = tmp 164 | mutex.Unlock() 165 | } 166 | }(i) 167 | } 168 | wg.Wait() 169 | 170 | return res 171 | } 172 | } 173 | 174 | // CollectSlice 175 | 176 | // Reduce 177 | 178 | //// All consumes a slice of a generic type and applies the predicate to each element in the slice. 179 | //// All return true if and only if no element returns false after applying the predicate. 180 | //// 181 | //// Vacuously, empty slices return true regardless of the predicate. 182 | //// 183 | //// predicate should be error-safe. It should handle any errors internally and return only a bool. 184 | //// If other arguments are required by predicate, predicate should be made a closure with the appropriate 185 | //// variables referenced. 186 | //func All[T any, A ~[]T](slice A, predicate func(T) bool) bool { 187 | // for _, v := range slice { 188 | // if !predicate(v) { 189 | // return false 190 | // } 191 | // } 192 | // return true 193 | //} 194 | // 195 | //// Any consumes a slice of a generic type and applies the predicate to each element in the slice. 196 | //// If any element returns true after applying the predicate, Any returns true. 197 | //// 198 | //// Vacuously, empty slices return false regardless of the predicate. 199 | //// 200 | //// predicate should be error-safe. It should handle any errors internally and return only a bool. 201 | //// If other arguments are required by predicate, predicate should be made a closure with the appropriate 202 | //// variables referenced. 203 | //func Any[T any, A ~[]T](slice A, predicate func(T) bool) bool { 204 | // for _, v := range slice { 205 | // if predicate(v) { 206 | // return true 207 | // } 208 | // } 209 | // return false 210 | //} 211 | // 212 | // 213 | //// Sum consumes a slice of a Summable type and sums the elements 214 | //// 215 | //// Vacuously, empty slices return the zero value of the provided Summable 216 | //func Sum[S Summable, A ~[]S](slice A) S { 217 | // var res S 218 | // for _, v := range slice { 219 | // res += v 220 | // } 221 | // return res 222 | //} 223 | -------------------------------------------------------------------------------- /terminals_test.go: -------------------------------------------------------------------------------- 1 | package functools 2 | 3 | // All tests 4 | //func TestGeqAll(t *testing.T) { 5 | // slice := []int{100, 25, 20, 31, 30} 6 | // if All(slice, func(val int) bool { return val >= 21 }) { 7 | // t.Errorf("TestGeqAll with %v was incorrect, got: %v, expected: %v", slice, true, false) 8 | // } 9 | // slice[2] = 21 10 | // if !All(slice, func(val int) bool { return val >= 21 }) { 11 | // t.Errorf("TestGeqAll with %v was incorrect, got: %v, expected: %v", slice, false, true) 12 | // } 13 | //} 14 | // 15 | //// Any tests 16 | //func TestLtAny(t *testing.T) { 17 | // slice := []int{20, 31, 22} 18 | // 19 | // if !Any(slice, func(val int) bool { return val < 21 }) { 20 | // t.Errorf("TestLtAny with %v was incorrect, got: %v, expected: %v", slice, false, true) 21 | // } 22 | // slice[0] = 21 23 | // if Any(slice, func(val int) bool { return val < 21 }) { 24 | // t.Errorf("TestLtAny with %v was incorrect, got: %v, expected: %v", slice, true, false) 25 | // } 26 | //} 27 | // 28 | //func TestIntSum(t *testing.T) { 29 | // slice := []int{1, 2, 3} 30 | // res := Sum(slice) 31 | // expect := 6 32 | // 33 | // if res != expect { 34 | // t.Errorf("TestIntSum was incorrect, got: %d, expected: %d", res, expect) 35 | // } 36 | //} 37 | // 38 | //func TestUintptrSum(t *testing.T) { 39 | // slice := []uintptr{1, 2, 3} 40 | // res := Sum(slice) 41 | // expect := uintptr(6) 42 | // 43 | // if res != expect { 44 | // t.Errorf("TestUintptrSum was incorrect, got: %d, expected: %d", res, expect) 45 | // } 46 | //} 47 | // 48 | //func TestFloatSum(t *testing.T) { 49 | // slice := []float64{0.668, 0.666, 0.666} 50 | // res := Sum(slice) 51 | // expect := 2.0 52 | // 53 | // if res != expect { 54 | // t.Errorf("TestFloatSum was incorrect, got: %f, expected: %f", res, expect) 55 | // } 56 | //} 57 | // 58 | //func TestStringSum(t *testing.T) { 59 | // slice := []string{"a", "b", "c"} 60 | // res := Sum(slice) 61 | // expect := "abc" 62 | // 63 | // if res != expect { 64 | // t.Errorf("TestStringSum was incorrect, got: %s, expected: %s", res, expect) 65 | // } 66 | //} 67 | // 68 | //func TestByteSum(t *testing.T) { 69 | // slice := []byte{1, 2, 3} 70 | // res := Sum(slice) 71 | // expect := byte(6) 72 | // 73 | // if res != expect { 74 | // t.Errorf("TestByteSum was incorrect, got: %d, expected: %d", res, expect) 75 | // } 76 | //} 77 | --------------------------------------------------------------------------------