Skip to content

Commit

Permalink
feat: adding attempt + repeat + times
Browse files Browse the repository at this point in the history
  • Loading branch information
samber committed Mar 5, 2022
1 parent d90b852 commit 2d3ea84
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 11 deletions.
86 changes: 85 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Supported helpers for slices:
- Map
- Reduce
- ForEach
- Times
- Uniq
- UniqBy
- GroupBy
Expand All @@ -59,6 +60,7 @@ Supported helpers for slices:
- Shuffle
- Reverse
- Fill
- Repeat
- ToMap

Supported helpers for maps:
Expand Down Expand Up @@ -99,6 +101,7 @@ Other functional programming helpers:
- Switch / Case / Default
- ToPtr
- ToSlicePtr
- Attempt

Constraints:

Expand Down Expand Up @@ -183,6 +186,30 @@ lop.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) {
// prints "hello\nworld\n" or "world\nhello\n"
```

### Times

Times invokes the iteratee n times, returning an array of the results of each invocation. The iteratee is invoked with index as argument.

```go
import "github.com/samber/lo"

lo.Times[string](3, func(i int) string {
return strconv.FormatInt(int64(i), 10)
})
// []string{"0", "1", "2"}
```

Parallel processing: like `lo.Times()`, but callback is called in goroutine.

```go
import lop "github.com/samber/lo/parallel"

lop.Times[string](3, func(i int) string {
return strconv.FormatInt(int64(i), 10)
})
// []string{"0", "1", "2"}
```

### Uniq

Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array.
Expand All @@ -208,7 +235,9 @@ uniqValues := lo.UniqBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
Returns an object composed of keys generated from the results of running each element of collection through iteratee.

```go
groups := GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
import lo "github.com/samber/lo"

groups := lo.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
return i%3
})
// map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}}
Expand Down Expand Up @@ -248,6 +277,8 @@ lo.Chunk[int]([]int{0}, 2)
Returns an array of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee.

```go
import lo "github.com/samber/lo"

partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string {
if x < 0 {
return "negative"
Expand Down Expand Up @@ -319,6 +350,23 @@ initializedSlice := lo.Fill[foo]([]foo{foo{"a"}, foo{"a"}}, foo{"b"})
// []foo{foo{"b"}, foo{"b"}}
```

### Repeat

Builds a slice with N copies of initial value.

```go
type foo struct {
bar string
}

func (f foo) Clone() foo {
return foo{f.bar}
}

initializedSlice := lo.Repeat[foo](2, foo{"a"})
// []foo{foo{"a"}, foo{"a"}}
```

### ToMap

Transforms a slice or an array of structs to a map based on a pivot callback.
Expand Down Expand Up @@ -643,6 +691,42 @@ ptr := lo.ToSlicePtr[string]([]string{"hello", "world"})
// []*string{"hello", "world"}
```

### Attempt

Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned.

```go
iter, err := lo.Attempt(42, func(i int) error {
if i == 5 {
return nil
}

return fmt.Errorf("failed")
})
// 6
// nil

iter, err := lo.Attempt(2, func(i int) error {
if i == 5 {
return nil
}

return fmt.Errorf("failed")
})
// 2
// error "failed"

iter, err := lo.Attempt(0, func(i int) error {
if i < 42 {
return fmt.Errorf("failed")
}

return nil
})
// 43
// nil
```

## 🛩 Benchmark

We executed a simple benchmark with the a dead-simple `lo.Map` loop:
Expand Down
27 changes: 27 additions & 0 deletions parallel/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ func ForEach[T any](collection []T, iteratee func(T, int)) {
wg.Wait()
}

// Times invokes the iteratee n times, returning an array of the results of each invocation.
// The iteratee is invoked with index as argument.
// `iteratee` is call in parallel.
func Times[T any](count int, iteratee func(int) T) []T {
result := make([]T, count)

var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(count)

for i := 0; i < count; i++ {
go func(_i int) {
item := iteratee(_i)

mu.Lock()
result[_i] = item
mu.Unlock()

wg.Done()
}(i)
}

wg.Wait()

return result
}

// GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee.
// `iteratee` is call in parallel.
func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T {
Expand Down
15 changes: 13 additions & 2 deletions parallel/slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,26 @@ func TestMap(t *testing.T) {
is.Equal(result2, []string{"1", "2", "3", "4"})
}

func TestTimes(t *testing.T) {
is := assert.New(t)

result1 := Times[string](3, func(i int) string {
return strconv.FormatInt(int64(i), 10)
})

is.Equal(len(result1), 3)
is.Equal(result1, []string{"0", "1", "2"})
}

func TestGroupBy(t *testing.T) {
is := assert.New(t)

result1 := GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
return i % 3
})

is.Equal(len(result1), 3)
is.Equal(result1, map[int][]int{
is.EqualValues(len(result1), 3)
is.EqualValues(result1, map[int][]int{
0: []int{0, 3},
1: []int{1, 4},
2: []int{2, 5},
Expand Down
16 changes: 16 additions & 0 deletions retry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lo

// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned.
func Attempt(maxIteration int, f func(int) error) (int, error) {
var err error

for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
// for retries >= 0 {
err = f(i)
if err == nil {
return i + 1, nil
}
}

return maxIteration, err
}
48 changes: 48 additions & 0 deletions retry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package lo

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAttempt(t *testing.T) {
is := assert.New(t)

err := fmt.Errorf("failed")

iter1, err1 := Attempt(42, func(i int) error {
return nil
})
iter2, err2 := Attempt(42, func(i int) error {
if i == 5 {
return nil
}

return err
})
iter3, err3 := Attempt(2, func(i int) error {
if i == 5 {
return nil
}

return err
})
iter4, err4 := Attempt(0, func(i int) error {
if i < 42 {
return fmt.Errorf("failed")
}

return nil
})

is.Equal(iter1, 1)
is.Equal(err1, nil)
is.Equal(iter2, 6)
is.Equal(err2, nil)
is.Equal(iter3, 2)
is.Equal(err3, err)
is.Equal(iter4, 43)
is.Equal(err4, nil)
}
23 changes: 23 additions & 0 deletions slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ func ForEach[T any](collection []T, iteratee func(T, int)) {
}
}

// Times invokes the iteratee n times, returning an array of the results of each invocation.
// The iteratee is invoked with index as argument.
func Times[T any](count int, iteratee func(int) T) []T {
result := make([]T, count)

for i := 0; i < count; i++ {
result[i] = iteratee(i)
}

return result
}

// Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept.
// The order of result values is determined by the order they occur in the array.
func Uniq[T comparable](collection []T) []T {
Expand Down Expand Up @@ -193,6 +205,17 @@ func Fill[T Clonable[T]](collection []T, initial T) []T {
return result
}

// Repeat builds a slice with N copies of initial value.
func Repeat[T Clonable[T]](count int, initial T) []T {
result := make([]T, 0, count)

for i := 0; i < count; i++ {
result = append(result, initial.Clone())
}

return result
}

// ToMap transforms a slice or an array of structs to a map based on a pivot callback.
func ToMap[K comparable, V any](collection []V, iteratee func(V) K) map[K]V {
result := make(map[K]V, len(collection))
Expand Down
37 changes: 29 additions & 8 deletions slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import (
"github.com/stretchr/testify/assert"
)

type foo struct {
bar string
}

func (f foo) Clone() foo {
return foo{f.bar}
}

func TestFilter(t *testing.T) {
is := assert.New(t)

Expand Down Expand Up @@ -39,6 +47,17 @@ func TestMap(t *testing.T) {
is.Equal(result2, []string{"1", "2", "3", "4"})
}

func TestTimes(t *testing.T) {
is := assert.New(t)

result1 := Times[string](3, func(i int) string {
return strconv.FormatInt(int64(i), 10)
})

is.Equal(len(result1), 3)
is.Equal(result1, []string{"0", "1", "2"})
}

func TestReduce(t *testing.T) {
is := assert.New(t)

Expand Down Expand Up @@ -151,14 +170,6 @@ func TestReverse(t *testing.T) {
is.Equal(result3, []int{})
}

type foo struct {
bar string
}

func (f foo) Clone() foo {
return foo{f.bar}
}

func TestFill(t *testing.T) {
is := assert.New(t)

Expand All @@ -169,6 +180,16 @@ func TestFill(t *testing.T) {
is.Equal(result2, []foo{})
}

func TestRepeat(t *testing.T) {
is := assert.New(t)

result1 := Repeat[foo](2, foo{"a"})
result2 := Repeat[foo](0, foo{"a"})

is.Equal(result1, []foo{foo{"a"}, foo{"a"}})
is.Equal(result2, []foo{})
}

func TestToMap(t *testing.T) {
is := assert.New(t)

Expand Down

0 comments on commit 2d3ea84

Please sign in to comment.