Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
fc7c19c
lotup
Jul 19, 2025
5e24c1c
loset
Jul 19, 2025
5c5633c
fix lotup
Jul 19, 2025
d7fffbd
fix lotup
Jul 19, 2025
19b3082
fix lotup
Jul 19, 2025
d0a9d1d
lofn not modificator
Jul 19, 2025
d15486e
loslice
Jul 19, 2025
7b73d69
mutslice
Jul 19, 2025
e345d26
upgrade go ver
Jul 19, 2025
9514ea4
lofn partial
Jul 20, 2025
48dc7d8
upgrade deps
Jul 20, 2025
78cf6b3
loany
Jul 20, 2025
9c0d9c2
take address of slice values
Jul 20, 2025
745e95c
lotup access shortcuts
Jul 20, 2025
49f6d5d
loslice find functions
Jul 20, 2025
46fffa1
loset functions
Jul 20, 2025
a1d0946
loslice minmax functions
Jul 20, 2025
410fede
lofn getters
Jul 20, 2025
4e070c1
lomap basics
Jul 20, 2025
d66eafc
wip
Jul 21, 2025
08783e0
loany map
Jul 22, 2025
bfd50a7
lofn partial reorder args
Jul 22, 2025
521cb81
lomap wip
Jul 22, 2025
c644fc6
loset fix types
Jul 22, 2025
469fab4
lomap logic
Jul 22, 2025
619f3bd
loany fix small bug
Jul 22, 2025
bb71e89
lomap find
Jul 22, 2025
2e33627
lomap min max
Jul 24, 2025
eeb05bd
lotype cast primitives
Jul 24, 2025
59e7374
loslice rename args
Jul 24, 2025
a269e28
lomap grouping
Jul 24, 2025
a93532b
mutmap filter
Jul 24, 2025
6bb5a7a
loslice preserve nils
Jul 24, 2025
36d4537
lomap monad operations
Jul 25, 2025
7ebad86
loslice special methods
Jul 25, 2025
565548b
mutmap wrap nil
Jul 25, 2025
551c0f7
mutslice force nillability
Jul 25, 2025
aebabfa
loset multiargs
Jul 25, 2025
1c5864b
lomap type parameters order
Jul 25, 2025
178fed4
mutmap mutations
Jul 25, 2025
b4093a4
loany fix bug
Jul 25, 2025
4e07796
loslice nested
Jul 25, 2025
33dbb01
lomap fix bug
Jul 25, 2025
cc6625d
lomap nested
Jul 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/samber/lo

go 1.18
go 1.24

//
// Dev dependencies are excluded from releases. Please check CI.
Expand All @@ -9,8 +9,8 @@ go 1.18
require (
github.com/stretchr/testify v1.10.0
github.com/thoas/go-funk v0.9.3
go.uber.org/goleak v1.2.1
golang.org/x/text v0.22.0
go.uber.org/goleak v1.3.0
golang.org/x/text v0.27.0
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
97 changes: 97 additions & 0 deletions loany/func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package loany

import (
"fmt"
"github.com/samber/lo/loslice"
"reflect"
)

type argSource string

const (
fromArgs argSource = "args"
fromKwargs argSource = "kwargs"
)

func errCall(index int, value any, source argSource, fnType reflect.Type, kwType reflect.Type) error {
typeName := fnType.In(index).Name()
if kwType == nil {
return fmt.Errorf("expected argument #%d to be of type `%s`, got `%#v`", index+1, typeName, value)
}

argName := kwType.Field(index).Name
return fmt.Errorf("expected argument #%d (%s) to be of type `%s`, got from %s `%#v`", index+1, argName, typeName, source, value)
}

// Call calls a function with the provided args and kwargs.
// Does not support variadic functions.
// Does not panic on invalid input, but returns an error instead.
func Call(fn any, args []any, kwargs any) ([]any, error) {
rvfn := reflect.ValueOf(fn)
rvkw := reflect.ValueOf(kwargs)

tfn := rvfn.Type()
if tfn.Kind() != reflect.Func {
return nil, fmt.Errorf("expected function, got `%#v`", fn)
}

if tfn.IsVariadic() {
return nil, fmt.Errorf("variadic functions are not supported, got `%#v`", fn)
}

numIn := tfn.NumIn()

if kwargs != nil {
if rvkw.Kind() == reflect.Pointer {
if rvkw.IsNil() {
return nil, fmt.Errorf("expected kwargs to be non-nil pointer, got %#v", kwargs)
}
rvkw = rvkw.Elem()
}

if rvkw.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected kwargs to be struct, got `%#v`", kwargs)
}

if rvkw.NumField() != numIn {
return nil, fmt.Errorf("expected kwargs (`%T`) to have %d fields, got %d: %#v",
kwargs, numIn, rvkw.NumField(), kwargs)
}
} else if len(args) != numIn {
return nil, fmt.Errorf("no kwargs were provided, expected exactly %d args, got %d: %#v", numIn, len(args), args)
}

var (
val any
source argSource
)

rvargs := make([]reflect.Value, numIn)

for i := range numIn {
if len(args) > 0 {
source = fromArgs
val = args[i]
} else if kwargs != nil {
source = fromKwargs
val = rvkw.Field(i).Interface()
} else {
panic("impossible: neither args nor kwargs were provided")
}

rv := reflect.ValueOf(val)
tin := tfn.In(i)

if !rv.CanConvert(tin) {
if kwargs == nil {
return nil, errCall(i, val, source, tfn, nil)
} else {
return nil, errCall(i, val, source, tfn, rvkw.Type())
}
}

rvargs[i] = rv.Convert(tin)
}

return loslice.Map(rvfn.Call(rvargs), reflect.Value.Interface), nil
}
23 changes: 23 additions & 0 deletions loany/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package loany

func Map[Map ~map[K]V, K comparable, V any](m Map) map[K]any {
result := make(map[K]any, len(m))
for k, v := range m {
result[k] = v
}

return result
}

// TypedMap converts a map of any type to a map of a specific type V.
// Values that cannot be converted to V will be omitted.
func TypedMap[Map ~map[K]any, V any, K comparable](m Map) map[K]V {
result := make(map[K]V, len(m))
for k, v := range m {
if val, ok := v.(V); ok {
result[k] = val
}
}

return result
}
23 changes: 23 additions & 0 deletions loany/slice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package loany

func Slice[Slice ~[]T, T any](xs Slice) []any {
result := make([]any, len(xs))
for i, x := range xs {
result[i] = x
}

return result
}

// TypedSlice converts a slice of any type to a slice of a specific type T.
// Values that cannot be converted to T will be omitted.
func TypedSlice[T any, Slice ~[]any](xs Slice) []T {
result := make([]T, 0, len(xs))
for _, x := range xs {
if v, ok := x.(T); ok {
result = append(result, v)
}
}

return result
}
13 changes: 13 additions & 0 deletions lofn/cmp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package lofn

func CmpFromLess[T any](less func(a, b T) bool) func(a, b T) int {
return func(a, b T) int {
if less(a, b) {
return -1
} else if less(b, a) {
return 1
} else {
return 0
}
}
}
72 changes: 72 additions & 0 deletions lofn/internal/generate/not/generate_not.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//go:build generate_monad

package main

import (
_ "embed"

"bytes"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"strings"
"text/template"
)

const (
monadGeneratorPath = "internal/generate/not/generate_not.go"
filename = "not.go"

ownerWritePermission = 0o644

argumentsLimit = 9
)

var (
//go:embed not.gotmpl
notRaw string
)

type Variant struct {
N int
}

func main() {
funcMap := template.FuncMap{
"add": func(x, y int) int { return x + y },
}
notTmpl := template.Must(template.New("not").Funcs(funcMap).Parse(notRaw))

pkg := detectPackageName()

var buf bytes.Buffer

buf.WriteString("// Code generated by generate_not.go; DO NOT EDIT.\n")
buf.WriteString(fmt.Sprintf("package %s\n\n", pkg))
buf.WriteString(fmt.Sprintf("//go:generate go run %s\n", monadGeneratorPath))

for n := 1; n <= argumentsLimit; n++ {
err := notTmpl.Execute(&buf, Variant{N: n})
if err != nil {
panic(err)
}
}

err := os.WriteFile(filename, buf.Bytes(), fs.FileMode(ownerWritePermission))
if err != nil {
panic(err)
}

slog.Info("done")
}

func detectPackageName() string {
out, err := exec.Command("go", "list", "-f", "{{.Name}}").Output()
if err != nil {
panic(err)
}

return strings.TrimSpace(string(out))
}
13 changes: 13 additions & 0 deletions lofn/internal/generate/not/not.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{- define "decls-types" -}}
{{ range $i := .N }}{{ if ne $i 0 }}, {{ end }}T{{ add $i 1 }}{{ end }}
{{- end }}
{{- define "decls-args" -}}
({{ range $i := .N }}{{ if ne $i 0 }}, {{ end }}v{{ add $i 1 }} T{{ add $i 1 }}{{ end }})
{{- end }}
func Not{{ if ne .N 1 }}{{ .N }}{{ end }}[{{ template "decls-types" . }} any](
pred func({{ template "decls-types" . }}) bool,
) func({{ template "decls-types" . }}) bool {
return func{{ template "decls-args" . }} bool {
return !pred({{ range $i := .N }}{{ if ne $i 0 }}, {{ end }}v{{ add $i 1 }}{{ end }})
}
}
72 changes: 72 additions & 0 deletions lofn/internal/generate/optional/generate_optional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//go:build generate_monad

package main

import (
_ "embed"

"bytes"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"strings"
"text/template"
)

const (
monadGeneratorPath = "internal/generate/optional/generate_optional.go"
filename = "optional.go"

ownerWritePermission = 0o644

argumentsLimit = 9
)

var (
//go:embed optional.gotmpl
optionalRaw string
)

type Variant struct {
N int
}

func main() {
funcMap := template.FuncMap{
"add": func(x, y int) int { return x + y },
}
optionalTmpl := template.Must(template.New("optional").Funcs(funcMap).Parse(optionalRaw))

pkg := detectPackageName()

var buf bytes.Buffer

buf.WriteString("// Code generated by generate_optional.go; DO NOT EDIT.\n")
buf.WriteString(fmt.Sprintf("package %s\n\n", pkg))
buf.WriteString(fmt.Sprintf("//go:generate go run %s\n", monadGeneratorPath))

for n := 1; n <= argumentsLimit; n++ {
err := optionalTmpl.Execute(&buf, Variant{N: n})
if err != nil {
panic(err)
}
}

err := os.WriteFile(filename, buf.Bytes(), fs.FileMode(ownerWritePermission))
if err != nil {
panic(err)
}

slog.Info("done")
}

func detectPackageName() string {
out, err := exec.Command("go", "list", "-f", "{{.Name}}").Output()
if err != nil {
panic(err)
}

return strings.TrimSpace(string(out))
}
Loading