context-fp-go

June 18, 2024 ยท View on GitHub

logo

Functional programming context for Golang

Motivation

if you want Dependency injection for Golang using plain functions, you are in the right place.

Features

  • ๐Ÿค Tiny size and most of it is Golang generics
  • ๐Ÿ’‰ Dependency injection without magic, only functions
  • ๐ŸคŒ Functions cached during workflow, no excess cost of CPU
  • ๐Ÿ’ก Smart type inference, only think about Context type and rest will be checked
  • โ™ป๏ธ Unit tests friendly, feel free to pass mocked function in the Context
  • ๐Ÿ“ฆ Tiny Redux like state manager also attached

How to

Basic example

import (
  "github.com/samber/lo"
  "github.com/darky/context-fp-go"
)

type Context struct {
  numbers []int
}
positiveNumbers := func(ctx *Context) []int {
  return lo.Filter(ctx.numbers, func(n int, i int) bool { return n > 0 })
}
numbersPrefix := func(ctx *Context) string {
  return "Here is numbers:"
}
positiveNumbersAsString := Cfp2(
  &positiveNumbers,
  &numbersPrefix,
  func(ns []int, prefix string) string {
    ns2str := lo.Map(ns, func(n int, i int) string { return fmt.Sprintf("%v", n) })
    return prefix + " " + strings.Join(ns2str, ",")
  },
)
assert.Equal(t,
  positiveNumbersAsString(&Context{
    numbers: []int{-1, -5, 7, 0, 4},
  }),
  "Here is numbers: 7,4",
)

Calculations cached example

import (
  "github.com/samber/lo"
  "github.com/darky/context-fp-go"
)

type Context struct {
  numbers []int
}
called := 0
positiveNumbers := func(ctx *Context) []int {
  called++
  return lo.Filter(ctx.numbers, func(n int, i int) bool { return n > 0 })
}
positiveNumbersLength := Cfp1(
  &positiveNumbers,
  func(ns []int) int {
    return len(ns)
  },
)
positiveNumbersAsString := Cfp2(
  &positiveNumbers,
  &positiveNumbersLength,
  func(ns []int, length int) string {
    ns2str := lo.Map(ns, func(n int, i int) string { return fmt.Sprintf("%v", n) })
    return strings.Join(ns2str, ",") + "; length - " + fmt.Sprintf("%v", length)
  },
)
assert.Equal(t,
  positiveNumbersAsString(&Context{
    numbers: []int{-1, -5, 7, 0, 4},
  }),
  "7,4; length - 2",
)
assert.Equal(t, called, 1)

Dependency injection example

import (
  "github.com/darky/context-fp-go"
)

type User struct {
  name string
}
type Context struct {
  fetchUser func() User
}
fetchUserFromDB := func() User {
  // some production implementation
}
fetchUser := func(ctx *Context) User {
  if ctx.fetchUser == nil {
    return fetchUserFromDB()
  }
  return ctx.fetchUser()
}
helloWorldUser := Cfp1(&fetchUser, func(user User) string {
  return "Hello world, " + user.name
})
assert.Equal(
  t,
  helloWorldUser(&Context{fetchUser: func() User { return User{name: "Vasya"} }}),
  "Hello world, Vasya",
)

State manager example

import (
  "github.com/darky/context-fp-go"
)

type N struct {
  n int
}
type Context struct {
  incNumber int
}
numbers := func(ctx *Context) func(payload *N) []int {
  return Sfp(
    func(ns []int, payload *N) []int {
      return append(ns, payload.n+ctx.incNumber)
    },
    []int{},
  )
}
addNumber1 := Cfp1(&numbers, func(ns func(payload *N) []int) []int {
  return ns(&N{1})
})
addNumber2 := Cfp1(&numbers, func(ns func(payload *N) []int) []int {
  return ns(&N{2})
})
addNumber3 := Cfp1(&numbers, func(ns func(payload *N) []int) []int {
  return ns(&N{3})
})
numbersToString := Cfp4(
  &numbers,
  &addNumber1,
  &addNumber2,
  &addNumber3,
  func(ns func(payload *N) []int, n1 []int, n2 []int, n3 []int) string {
    return strings.Join(
      lo.Map(ns(nil), func(n int, i int) string { return fmt.Sprintf("%v", n) }),
      ",",
    )
  },
)
assert.Equal(t,
  numbersToString(&Context{incNumber: 1}),
  "2,3,4",
)

See also

  • context-fp - Functional programming context for TypeScript
  • functx - Functional programming context for Gleam