Skip to content

Commit

Permalink
Merge pull request #16 from life4/maps-inplace
Browse files Browse the repository at this point in the history
More functions for maps: IMerge, IMergeBy, IMapValues, HasValue
  • Loading branch information
orsinium committed Jul 21, 2023
2 parents 1e8c205 + 7353dd8 commit 5c9c40a
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 42 deletions.
61 changes: 30 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@

Generic functions for Go. Bringing the beauty of functional programming in Go 1.18+.

**Features:**
**😎 Features:**

+ Over 130 generic functions for channels, maps, and slices.
+ Uses the power of Go 1.18+ generics.
+ No code generation.
+ No dependencies (except [is](https://github.com/matryer/is) for testing).
+ Pure Go.
+ Sync and async versions of all the main functions.
+ For slices and channels.
+ 🛠️ Over 130 generic functions for channels, maps, and slices.
+ 💪 Uses the power of Go 1.18+ generics.
+ 🧐 No code generation.
+ 🪶 No dependencies (except [is](https://github.com/matryer/is) for testing).
+ 🏃 Pure Go.
+ 🪩 Sync and async versions of all the main functions.

**When to use:**
**🔨 When to use:**

+ In a big project. More the project grows, more you find yourself writing boring generic code like "Min". Break the cycle.
+ In a team project. Each line of code you write means higher maintenance cost that in turn means loosing time and money.
+ In a pet project. Leave the boring stuff to us, focus on the fun parts.
+ When readability matters. `slices.Shrink` is a function with a human-friendly name and documentation. `s[:len(s):len(s)]` is a jibberish and black magic. Prefer the former.
+ When you miss some conveniences that come in other languages out-of-the-box.
+ When you write a highly concurrent code and don't want to manually implement code for cancellation, results collection and ordering, worker groups, context, etc.
+ 🐘 **In a big project**. More the project grows, more you find yourself writing boring generic code like "Min". Break the cycle.
+ 🤝 **In a team project**. Each line of code you write means higher maintenance cost that in turn means loosing time and money.
+ 🐶 **In a pet project**. Leave the boring stuff to us, focus on the fun parts.
+ 📚 **When readability matters**. `slices.Shrink` is a function with a human-friendly name and documentation. `s[:len(s):len(s)]` is a jibberish and black magic. Prefer the former.
+ 💔 **When you miss some conveniences** that come in other languages out-of-the-box.
+ 🐇 **When you write a highly concurrent code** and don't want to manually implement code for cancellation, results collection and ordering, worker groups, context, etc.

**What's inside**:
**📦 What's inside**:

+ `Filter`, `Map`, and `Reduce` for data processing on steroids.
+ `FilterAsync`, `MapAsync`, and `ReduceAsync` for making your code fast and concurrent with a single line of code.
Expand All @@ -33,13 +32,13 @@ Generic functions for Go. Bringing the beauty of functional programming in Go 1.

And much more.

## Installation
## 💾 Installation

```bash
go get github.com/life4/genesis
```

## Examples
## 👀 Examples

Find the minimal value in a slice of ints:

Expand All @@ -57,25 +56,25 @@ Concurrently check status codes for multiple URLs:

```go
urls := []string{
"https://go.dev/",
"https://golang.org/",
"https://google.com/",
"https://go.dev/",
"https://golang.org/",
"https://google.com/",
}
codes := slices.MapAsync(
urls, 0,
func(url string) int {
return lambdas.Must(http.Get(url)).StatusCode
},
urls, 0,
func(url string) int {
return lambdas.Must(http.Get(url)).StatusCode
},
)
```

## Usage
## 🔨 Usage

Genesis contains the following packages:

+ [slices](https://pkg.go.dev/github.com/life4/genesis/slices): generic functions for slices.
+ [maps](https://pkg.go.dev/github.com/life4/genesis/maps): generic functions for maps.
+ [channels](https://pkg.go.dev/github.com/life4/genesis/channels): generic function for channels.
+ [lambdas](https://pkg.go.dev/github.com/life4/genesis/lambdas): helper generic functions to work with `slices.Map` and similar.
+ [🍞 slices](https://pkg.go.dev/github.com/life4/genesis/slices): generic functions for slices.
+ [🗺 maps](https://pkg.go.dev/github.com/life4/genesis/maps): generic functions for maps.
+ [📺 channels](https://pkg.go.dev/github.com/life4/genesis/channels): generic function for channels.
+ [🛟 lambdas](https://pkg.go.dev/github.com/life4/genesis/lambdas): helper generic functions to work with `slices.Map` and similar.

See [DOCUMENTATION](https://pkg.go.dev/github.com/life4/genesis) for more info.
See [📄 DOCUMENTATION](https://pkg.go.dev/github.com/life4/genesis) for more info.
42 changes: 36 additions & 6 deletions maps/maps.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package maps

// Copy returns a copy of the given map
// Copy returns a copy of the given map.
func Copy[M ~map[K]V, K comparable, V any](items M) M {
if items == nil {
return nil
}
result := make(M)
for key, value := range items {
result[key] = value
Expand All @@ -28,13 +31,32 @@ func Equal[M1, M2 ~map[K]V, K, V comparable](items1 M1, items2 M2) bool {
return true
}

// FromKeys makes a new map with keys taken from `keys` array and all values equal to `value`.
func FromKeys[K comparable, V any](keys []K, value V) map[K]V {
result := make(map[K]V)
for _, key := range keys {
result[key] = value
}
return result
}

// HasKey returns true if the given map contains the given key.
func HasKey[M ~map[K]V, K comparable, V any](items M, key K) bool {
_, ok := items[key]
return ok
}

// Map calls the given function f with each key and value and makes a new map
// HasValue returns true if the given map contains the given value.
func HasValue[M ~map[K]V, K, V comparable](items M, val V) bool {
for _, v := range items {
if v == val {
return true
}
}
return false
}

// Map calls the given function `f` with each key and value and makes a new map
// out of key-value pairs returned by the function.
func Map[M ~map[K]V, K, RK comparable, V, RV any](items M, f func(K, V) (RK, RV)) map[RK]RV {
result := make(map[RK]RV)
Expand All @@ -45,7 +67,7 @@ func Map[M ~map[K]V, K, RK comparable, V, RV any](items M, f func(K, V) (RK, RV)
return result
}

// MapKeys is like Map but the function f accepts only a key and returns only the new key.
// MapKeys is like [Map] but the function f accepts only a key and returns only the new key.
func MapKeys[M ~map[K]V, K, RK comparable, V any](items M, f func(K) RK) map[RK]V {
result := make(map[RK]V)
for key, value := range items {
Expand All @@ -54,7 +76,7 @@ func MapKeys[M ~map[K]V, K, RK comparable, V any](items M, f func(K) RK) map[RK]
return result
}

// MapValues is like Map but the function f accepts only a value and returns only the new value.
// MapValues is like [Map] but the function `f` accepts only a value and returns only the new value.
func MapValues[M ~map[K]V, K comparable, V, RV any](items M, f func(V) RV) map[K]RV {
result := make(map[K]RV)
for key, value := range items {
Expand All @@ -66,6 +88,8 @@ func MapValues[M ~map[K]V, K comparable, V, RV any](items M, f func(V) RV) map[K
// Merge returns a new map containing items from both given maps.
//
// In case of duplicate values, the value from items2 has precedence over items1.
//
// If you want to modify the map in-place, use [IMerge] instead.
func Merge[M1, M2 ~map[K]V, K, V comparable](items1 M1, items2 M2) M1 {
result := make(M1)
for key, value := range items1 {
Expand All @@ -77,7 +101,9 @@ func Merge[M1, M2 ~map[K]V, K, V comparable](items1 M1, items2 M2) M1 {
return result
}

// MergeBy is like Merge but conflicts are resolved by the function f.
// MergeBy is like [Merge] but conflicts are resolved by the function `f`.
//
// If you want to modify the map in-place, use [IMergeBy] instead.
func MergeBy[M1, M2 ~map[K]V, K, V comparable](items1 M1, items2 M2, f func(K, V, V) V) M1 {
result := make(M1)
for key, value := range items1 {
Expand Down Expand Up @@ -109,7 +135,8 @@ func Keys[M ~map[K]V, K comparable, V any](items M) []K {
//
// Keys that aren't in the map are simply ignored.
//
// This function is a counterpart for `Without`.
// This function is a counterpart for [Without].
// If you want to modify the map in-place, use [LeaveOnly] instead.
func Take[M ~map[K]V, K comparable, V any](items M, keys ...K) M {
result := make(M)
for _, key := range keys {
Expand All @@ -136,6 +163,9 @@ func Values[M ~map[K]V, K comparable, V any](items M) []V {
// Without returns a copy of the given map without the given keys.
//
// If a key is not found in the given map, it is simply ignored.
//
// This function is a counterpart for [Take].
// If you want to modify the map in-place, use [Drop] instead.
func Without[M ~map[K]V, K comparable, V any](items M, keys ...K) M {
result := make(M)
skip := make(map[K]struct{})
Expand Down
42 changes: 40 additions & 2 deletions maps/maps_inplace.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package maps

// Clear removes all items from the given map
// Clear removes all items from the given map.
//
// This is an in-place operation, meaning that it modifies the original map.
func Clear[M ~map[K]V, K comparable, V any](items M) {
Expand All @@ -14,13 +14,17 @@ func Clear[M ~map[K]V, K comparable, V any](items M) {
// If a key is not found in the given map, it is simply ignored.
//
// It is in-place operation, it modifies the original map.
// If you want to create a new map, use [Without] instead.
func Drop[M ~map[K]V, K comparable, V any](items M, keys ...K) {
for _, key := range keys {
delete(items, key)
}
}

// LeaveOnly leaves in the given map only the given keys.
//
// It is in-place operation, it modifies the original map.
// If you want to create a new map, use [Take] instead.
func LeaveOnly[M ~map[K]V, K comparable, V any](items M, keys ...K) {
skip := make(map[K]struct{})
for _, key := range keys {
Expand All @@ -36,7 +40,7 @@ func LeaveOnly[M ~map[K]V, K comparable, V any](items M, keys ...K) {

// Pop removes the given key from the map and returns the associated value.
//
// If the key not found, the ErrNotFound error is returned.
// If the key not found, the [ErrNotFound] error is returned.
//
// The function modifies the original map in-place.
func Pop[M ~map[K]V, K comparable, V any](items M, key K) (V, error) {
Expand All @@ -49,6 +53,8 @@ func Pop[M ~map[K]V, K comparable, V any](items M, key K) (V, error) {
}

// Replace replaces the `key` by the `value` but only if the key is already in the map.
//
// If the key is already in the map, the function does nothing.
func Replace[M ~map[K]V, K comparable, V any](items M, key K, value V) {
_, exists := items[key]
if exists {
Expand All @@ -64,3 +70,35 @@ func Update[M1, M2 ~map[K]V, K, V comparable](items M1, with M2) {
items[key] = value
}
}

// IMerge adds items from the second map to the first one.
//
// This is an in-place operation. It modifies `target` map in-place.
func IMerge[M1, M2 ~map[K]V, K, V comparable](target M1, items M2) {
for key, value := range items {
target[key] = value
}
}

// IMergeBy is like [IMerge] but conflicts are resolved by the function `f`.
//
// This is an in-place operation. It modifies `target` map in-place.
func IMergeBy[M1, M2 ~map[K]V, K, V comparable](target M1, items M2, f func(K, V, V) V) {
for key, value2 := range items {
value1, contains := target[key]
if contains {
value2 = f(key, value1, value2)
}
target[key] = value2
}
}

// IMapValues applies the function to the map values.
//
// This is an in-place operation. It modifies `items` map in-place.
// If you want to create a new map, use [MapValues] instead.
func IMapValues[M ~map[K]V, K comparable, V any](items M, f func(V) V) {
for key, value := range items {
items[key] = f(value)
}
}
39 changes: 38 additions & 1 deletion maps/maps_inplace_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package maps_test

import (
"fmt"
"testing"

"github.com/life4/genesis/maps"
Expand Down Expand Up @@ -32,9 +33,13 @@ func TestPop(t *testing.T) {
is := is.New(t)
m := map[int]int{1: 2, 3: 4, 5: 6}
val, err := maps.Pop(m, 1)
is.Equal(err, nil)
is.NoErr(err)
is.Equal(val, 2)
is.Equal(m, map[int]int{3: 4, 5: 6})

val, err = maps.Pop(m, 13)
is.Equal(val, 0)
is.Equal(err, maps.ErrNotFound)
}

func TestReplace(t *testing.T) {
Expand All @@ -52,3 +57,35 @@ func TestUpdate(t *testing.T) {
maps.Update(m1, m2)
is.Equal(m1, map[int]int{1: 2, 3: 5, 6: 7})
}

func TestIMerge(t *testing.T) {
is := is.New(t)
m1 := map[int]string{1: "one", 2: "two"}
m2 := map[int]string{2: "new two", 3: "three"}
exp := map[int]string{1: "one", 2: "new two", 3: "three"}
maps.IMerge(m1, m2)
is.Equal(m1, exp)
}

func TestIMergeBy(t *testing.T) {
is := is.New(t)
m1 := map[int]string{1: "one", 2: "two"}
m2 := map[int]string{2: "new two", 3: "three"}
f := func(k int, a, b string) string {
is.Equal(k, 2)
return fmt.Sprintf("%s|%s", a, b)
}
exp := map[int]string{1: "one", 2: "two|new two", 3: "three"}
maps.IMergeBy(m1, m2, f)
is.Equal(m1, exp)
}

func TestIMapValues(t *testing.T) {
is := is.New(t)
m := map[int]int32{1: 2, 3: 4, 5: 6}
f := func(v int32) int32 {
return v * 2
}
maps.IMapValues(m, f)
is.Equal(m, map[int]int32{1: 4, 3: 8, 5: 12})
}
Loading

0 comments on commit 5c9c40a

Please sign in to comment.