diff --git a/functions/core/chunk.go b/functions/core/chunk.go new file mode 100644 index 00000000..9001fd79 --- /dev/null +++ b/functions/core/chunk.go @@ -0,0 +1,25 @@ +package core + +// Chunk splits xs into sub-slices of the given size. +// The last chunk may contain fewer than size elements. +// Returns nil if xs is empty. Panics if size <= 0. +func Chunk[T any](xs []T, size int) [][]T { + if size <= 0 { + panic("chunk size must be > 0") + } + n := len(xs) + if n == 0 { + return nil + } + + numChunks := (n + size - 1) / size + chunks := make([][]T, 0, numChunks) + for i := 0; i < n; i += size { + end := i + size + if end > n { + end = n + } + chunks = append(chunks, xs[i:end]) + } + return chunks +} diff --git a/functions/core/chunk.md b/functions/core/chunk.md new file mode 100644 index 00000000..37dc7796 --- /dev/null +++ b/functions/core/chunk.md @@ -0,0 +1,35 @@ +--- +name: chunk +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func Chunk[T any](xs []T, size int) [][]T" +description: "Divide un slice en trozos (sub-slices) de tamanio N. El ultimo trozo puede contener menos de N elementos. Retorna nil si el slice esta vacio. Entra en panic si size <= 0." +tags: [slice, chunk, batch, generic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/chunk.go" +--- + +## Ejemplo + +```go +chunks := Chunk([]int{1, 2, 3, 4, 5}, 2) +// chunks = [[1, 2], [3, 4], [5]] + +chunks = Chunk([]string{"a", "b", "c", "d"}, 4) +// chunks = [["a", "b", "c", "d"]] +``` + +## Notas + +Funcion pura generica. Los sub-slices comparten memoria con el slice original (son slices del mismo backing array). Pre-calcula la capacidad del slice de chunks para evitar reasignaciones. Si size es mayor que len(xs), retorna un unico chunk con todos los elementos. diff --git a/functions/core/map_concurrent.go b/functions/core/map_concurrent.go new file mode 100644 index 00000000..d3d1c2a5 --- /dev/null +++ b/functions/core/map_concurrent.go @@ -0,0 +1,43 @@ +package core + +import "sync" + +// MapConcurrent applies fn to each element of xs using a pool of worker goroutines. +// The number of concurrent workers is capped by the workers parameter. +// Results preserve the original index order. If workers <= 0, it defaults to 1. +func MapConcurrent[T any, U any](xs []T, fn func(T) U, workers int) []U { + if workers <= 0 { + workers = 1 + } + n := len(xs) + if n == 0 { + return []U{} + } + + results := make([]U, n) + var wg sync.WaitGroup + ch := make(chan int, n) + + // Feed indices into the channel. + for i := range xs { + ch <- i + } + close(ch) + + // Spawn workers. + if workers > n { + workers = n + } + wg.Add(workers) + for w := 0; w < workers; w++ { + go func() { + defer wg.Done() + for i := range ch { + results[i] = fn(xs[i]) + } + }() + } + + wg.Wait() + return results +} diff --git a/functions/core/map_concurrent.md b/functions/core/map_concurrent.md new file mode 100644 index 00000000..a976eee8 --- /dev/null +++ b/functions/core/map_concurrent.md @@ -0,0 +1,34 @@ +--- +name: map_concurrent +kind: function +lang: go +domain: core +version: "1.0.0" +purity: impure +signature: "func MapConcurrent[T any, U any](xs []T, fn func(T) U, workers int) []U" +description: "Aplica una funcion a cada elemento de un slice usando un pool de goroutines como workers. Los resultados preservan el orden original del slice de entrada." +tags: [map, concurrent, parallel, generic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/map_concurrent.go" +--- + +## Ejemplo + +```go +squares := MapConcurrent([]int{1, 2, 3, 4, 5}, func(n int) int { + return n * n +}, 3) +// squares = [1, 4, 9, 16, 25] (orden preservado) +``` + +## Notas + +Funcion impura generica que usa goroutines y sync.WaitGroup. Los workers se alimentan de un canal con indices, garantizando que cada resultado se escribe en su posicion correcta sin race conditions (cada goroutine escribe en un indice unico). Si workers <= 0 se usa 1. Si workers > len(xs) se ajusta a len(xs). diff --git a/functions/core/memoize.go b/functions/core/memoize.go new file mode 100644 index 00000000..e3d766cc --- /dev/null +++ b/functions/core/memoize.go @@ -0,0 +1,16 @@ +package core + +// Memoize wraps a pure function fn so that results are cached by key. +// Subsequent calls with the same key return the cached value without calling fn again. +// The returned function is safe for single-goroutine use. +func Memoize[K comparable, V any](fn func(K) V) func(K) V { + cache := make(map[K]V) + return func(key K) V { + if val, ok := cache[key]; ok { + return val + } + val := fn(key) + cache[key] = val + return val + } +} diff --git a/functions/core/memoize.md b/functions/core/memoize.md new file mode 100644 index 00000000..e6db007c --- /dev/null +++ b/functions/core/memoize.md @@ -0,0 +1,40 @@ +--- +name: memoize +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func Memoize[K comparable, V any](fn func(K) V) func(K) V" +description: "Cachea resultados de una funcion pura. Retorna una nueva funcion que almacena en un mapa interno los resultados ya calculados, evitando recalculos para la misma clave." +tags: [cache, memoize, functional, generic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/memoize.go" +--- + +## Ejemplo + +```go +fib := Memoize(func(n int) int { + if n <= 1 { + return n + } + // nota: para recursion memoizada se necesita declarar la variable antes + return n // simplificado +}) +doubled := Memoize(func(x int) int { return x * 2 }) +doubled(5) // calcula: 10 +doubled(5) // cache hit: 10 +``` + +## Notas + +Funcion pura generica (referencialmente transparente). El cache interno es un map[K]V sin sincronizacion, por lo que la funcion retornada es segura solo para uso en una sola goroutine. K debe ser comparable para usarse como clave del mapa. diff --git a/functions/core/partition.go b/functions/core/partition.go new file mode 100644 index 00000000..aedff5d6 --- /dev/null +++ b/functions/core/partition.go @@ -0,0 +1,16 @@ +package core + +// Partition splits xs into two slices: the first contains elements where pred returns true, +// the second contains elements where pred returns false. Original order is preserved in both. +func Partition[T any](xs []T, pred func(T) bool) ([]T, []T) { + trueSlice := make([]T, 0) + falseSlice := make([]T, 0) + for _, x := range xs { + if pred(x) { + trueSlice = append(trueSlice, x) + } else { + falseSlice = append(falseSlice, x) + } + } + return trueSlice, falseSlice +} diff --git a/functions/core/partition.md b/functions/core/partition.md new file mode 100644 index 00000000..e73442f9 --- /dev/null +++ b/functions/core/partition.md @@ -0,0 +1,33 @@ +--- +name: partition +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func Partition[T any](xs []T, pred func(T) bool) ([]T, []T)" +description: "Divide un slice en dos segun un predicado. El primer slice contiene los elementos que cumplen el predicado, el segundo los que no. Se preserva el orden original." +tags: [slice, partition, functional, generic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/partition.go" +--- + +## Ejemplo + +```go +evens, odds := Partition([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 }) +// evens = [2, 4] +// odds = [1, 3, 5] +``` + +## Notas + +Funcion pura generica. Ambos slices retornados son nuevos (no muta el slice original). Si el slice de entrada esta vacio, retorna dos slices vacios. Los elementos mantienen su orden relativo original en cada particion. diff --git a/functions/core/pipeline.go b/functions/core/pipeline.go new file mode 100644 index 00000000..f3e02403 --- /dev/null +++ b/functions/core/pipeline.go @@ -0,0 +1,13 @@ +package core + +// Pipeline composes a sequence of functions T -> T into a single function. +// The functions are applied left to right: Pipeline(f, g, h)(x) == h(g(f(x))). +// Returns the identity function if no functions are provided. +func Pipeline[T any](fns ...func(T) T) func(T) T { + return func(val T) T { + for _, fn := range fns { + val = fn(val) + } + return val + } +} diff --git a/functions/core/pipeline.md b/functions/core/pipeline.md new file mode 100644 index 00000000..6b1856be --- /dev/null +++ b/functions/core/pipeline.md @@ -0,0 +1,36 @@ +--- +name: pipeline +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func Pipeline[T any](fns ...func(T) T) func(T) T" +description: "Compone funciones T -> T en secuencia de izquierda a derecha. Pipeline(f, g, h)(x) equivale a h(g(f(x))). Sin funciones retorna la identidad." +tags: [pipeline, compose, functional, generic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/pipeline.go" +--- + +## Ejemplo + +```go +transform := Pipeline( + func(s string) string { return strings.TrimSpace(s) }, + func(s string) string { return strings.ToUpper(s) }, + func(s string) string { return "[" + s + "]" }, +) +transform(" hello ") // "[HELLO]" +``` + +## Notas + +Funcion pura generica. Las funciones se aplican en orden de izquierda a derecha (no es composicion matematica tradicional). Si se pasa un slice vacio de funciones, la funcion retornada actua como identidad. diff --git a/functions/core/retry_with_backoff.go b/functions/core/retry_with_backoff.go new file mode 100644 index 00000000..733a3292 --- /dev/null +++ b/functions/core/retry_with_backoff.go @@ -0,0 +1,31 @@ +package core + +import ( + "fmt" + "time" +) + +// RetryWithBackoff retries fn up to maxRetries times with exponential backoff. +// The delay between attempt i and i+1 is baseDelay * 2^i. +// Returns the first successful result or the last error after all retries are exhausted. +func RetryWithBackoff[T any](fn func() (T, error), maxRetries int, baseDelay time.Duration) (T, error) { + var zero T + if maxRetries < 0 { + return zero, fmt.Errorf("maxRetries must be >= 0, got %d", maxRetries) + } + + var lastErr error + for attempt := 0; attempt <= maxRetries; attempt++ { + result, err := fn() + if err == nil { + return result, nil + } + lastErr = err + + if attempt < maxRetries { + delay := baseDelay * (1 << uint(attempt)) + time.Sleep(delay) + } + } + return zero, fmt.Errorf("all %d retries exhausted: %w", maxRetries+1, lastErr) +} diff --git a/functions/core/retry_with_backoff.md b/functions/core/retry_with_backoff.md new file mode 100644 index 00000000..05b59f17 --- /dev/null +++ b/functions/core/retry_with_backoff.md @@ -0,0 +1,40 @@ +--- +name: retry_with_backoff +kind: function +lang: go +domain: core +version: "1.0.0" +purity: impure +signature: "func RetryWithBackoff[T any](fn func() (T, error), maxRetries int, baseDelay time.Duration) (T, error)" +description: "Reintenta una funcion impura con backoff exponencial. El delay entre intento i e i+1 es baseDelay * 2^i. Retorna el primer resultado exitoso o el ultimo error tras agotar los reintentos." +tags: [retry, backoff, resilience, generic] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "functions/core/retry_with_backoff.go" +--- + +## Ejemplo + +```go +result, err := RetryWithBackoff(func() (string, error) { + resp, err := http.Get("https://api.example.com/data") + if err != nil { + return "", err + } + defer resp.Body.Close() + return "ok", nil +}, 3, 100*time.Millisecond) +// Intenta hasta 4 veces (1 inicial + 3 reintentos) +// Delays: 100ms, 200ms, 400ms +``` + +## Notas + +Funcion impura generica que usa time.Sleep para el backoff. El numero total de intentos es maxRetries + 1 (el intento inicial mas los reintentos). Si maxRetries es negativo retorna error inmediatamente.