Skip to main content
Version: v2.2.82 (latest)
Reference · Core Type

Reader

Functional dependency injection. Reader[C, A] represents a computation that depends on a context C and produces a value A.

01

Overview

Reader is simply a function from context to value:

type_definition.go
package reader

// Reader is a function from context to value
type Reader[C, A any] = func(C) A

Why Reader?

Before
traditional_di.go
// ❌ Traditional DI with structs
type Service struct {
  db     *sql.DB
  logger *log.Logger
  config Config
}

func (s *Service) GetUser(id string) User {
  // Hard to compose
  // Difficult to test
}
After
reader_di.go
// ✅ Reader-based DI
type Dependencies struct {
  DB     *sql.DB
  Logger *log.Logger
  Config Config
}

func GetUser(id string) reader.Reader[Dependencies, User] {
  return func(deps Dependencies) User {
      // Easy to compose
      // Simple to test
  }
}
02

Core API

Constructors

FunctionSignatureDescription
Offunc Of[C, A any](value A) Reader[C, A]Wrap pure value (ignores context)
Askfunc Ask[C, A any](f func(C) A) Reader[C, A]Access and transform context
Asksfunc Asks[C, A any](f func(C) A) Reader[C, A]Alias for Ask

Transformations

FunctionSignatureDescription
Mapfunc Map[C, A, B any](f func(A) B) func(Reader[C, A]) Reader[C, B]Transform result
Chainfunc Chain[C, A, B any](f func(A) Reader[C, B]) func(Reader[C, A]) Reader[C, B]Sequence operations
Flattenfunc Flatten[C, A any](Reader[C, Reader[C, A]]) Reader[C, A]Unwrap nested Reader

Combining

FunctionSignatureDescription
Apfunc Ap[C, A, B any](fa Reader[C, A]) func(Reader[C, func(A) B]) Reader[C, B]Apply wrapped function
SequenceArrayfunc SequenceArray[C, A any]([]Reader[C, A]) Reader[C, []A]All-or-nothing
TraverseArrayfunc TraverseArray[C, A, B any](f func(A) Reader[C, B]) func([]A) Reader[C, []B]Map and sequence

Context Manipulation

FunctionSignatureDescription
Localfunc Local[C1, C2, A any](f func(C1) C2) func(Reader[C2, A]) Reader[C1, A]Transform context before use
03

Usage Examples

Basic Operations

basic.go
package main

import (
  R "github.com/IBM/fp-go/v2/reader"
)

type Config struct {
  DBUrl string
  Port  int
}

func main() {
  // Wrap pure value
  answer := R.Of[Config, int](42)
  result := answer(Config{})  // 42
  
  // Access context
  getDBUrl := R.Ask(func(c Config) string {
      return c.DBUrl
  })
  
  config := Config{DBUrl: "localhost:5432", Port: 8080}
  url := getDBUrl(config)  // "localhost:5432"
  
  // Access and transform
  getPort := R.Asks(func(c Config) int {
      return c.Port
  })
  port := getPort(config)  // 8080
}

Dependency Injection

dependency_injection.go
package main

import (
  "database/sql"
  "log"
  R "github.com/IBM/fp-go/v2/reader"
  F "github.com/IBM/fp-go/v2/function"
)

type Dependencies struct {
  DB     *sql.DB
  Logger *log.Logger
  Config Config
}

func getDB() R.Reader[Dependencies, *sql.DB] {
  return R.Ask(func(deps Dependencies) *sql.DB {
      return deps.DB
  })
}

func getLogger() R.Reader[Dependencies, *log.Logger] {
  return R.Ask(func(deps Dependencies) *log.Logger {
      return deps.Logger
  })
}

func fetchUser(id string) R.Reader[Dependencies, User] {
  return F.Pipe1(
      getDB(),
      R.Chain(func(db *sql.DB) R.Reader[Dependencies, User] {
          return R.Of[Dependencies](queryUser(db, id))
      }),
  )
}

func main() {
  deps := Dependencies{
      DB:     connectDB(),
      Logger: log.New(os.Stdout, "", 0),
      Config: loadConfig(),
  }
  
  user := fetchUser("user-123")(deps)
}

Composing Operations

composing.go
package main

import (
  R "github.com/IBM/fp-go/v2/reader"
  F "github.com/IBM/fp-go/v2/function"
)

func getUserEmail(id string) R.Reader[Dependencies, string] {
  return F.Pipe1(
      fetchUser(id),
      R.Map(func(u User) string {
          return u.Email
      }),
  )
}

func sendEmail(to, subject, body string) R.Reader[Dependencies, bool] {
  return R.Ask(func(deps Dependencies) bool {
      // Use deps.Logger, deps.Config, etc.
      return sendEmailViaService(to, subject, body)
  })
}

func notifyUser(id string) R.Reader[Dependencies, bool] {
  return F.Pipe1(
      getUserEmail(id),
      R.Chain(func(email string) R.Reader[Dependencies, bool] {
          return sendEmail(email, "Notification", "Hello!")
      }),
  )
}

func main() {
  deps := Dependencies{...}
  success := notifyUser("user-123")(deps)
}

Local Context Transformation

local_context.go
package main

import (
  R "github.com/IBM/fp-go/v2/reader"
)

type AppConfig struct {
  Database DatabaseConfig
  Server   ServerConfig
}

type DatabaseConfig struct {
  Host string
  Port int
}

// Reader that needs DatabaseConfig
func connectDB() R.Reader[DatabaseConfig, *sql.DB] {
  return R.Ask(func(cfg DatabaseConfig) *sql.DB {
      return sql.Open("postgres", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
  })
}

// Transform AppConfig to DatabaseConfig
func withDBConfig() R.Reader[AppConfig, *sql.DB] {
  return R.Local(func(app AppConfig) DatabaseConfig {
      return app.Database
  })(connectDB())
}

func main() {
  appConfig := AppConfig{
      Database: DatabaseConfig{Host: "localhost", Port: 5432},
      Server:   ServerConfig{Port: 8080},
  }
  
  db := withDBConfig()(appConfig)
}
04

Common Patterns

Pattern 1: Testing with Mocks

testing.go
package main

import (
  "testing"
  R "github.com/IBM/fp-go/v2/reader"
)

func TestFetchUser(t *testing.T) {
  // Mock dependencies
  mockDeps := Dependencies{
      DB:     mockDB(),
      Logger: mockLogger(),
      Config: testConfig(),
  }
  
  // Test the Reader
  user := fetchUser("test-id")(mockDeps)
  
  assert.Equal(t, "test-id", user.ID)
}

Pattern 2: Configuration Management

config_management.go
package main

import (
  R "github.com/IBM/fp-go/v2/reader"
)

type Config struct {
  APIKey    string
  Timeout   time.Duration
  MaxRetries int
}

func getAPIKey() R.Reader[Config, string] {
  return R.Asks(func(c Config) string {
      return c.APIKey
  })
}

func getTimeout() R.Reader[Config, time.Duration] {
  return R.Asks(func(c Config) time.Duration {
      return c.Timeout
  })
}

func makeAPICall(endpoint string) R.Reader[Config, Response] {
  return F.Pipe2(
      getAPIKey(),
      R.Chain(func(key string) R.Reader[Config, time.Duration] {
          return getTimeout()
      }),
      R.Chain(func(timeout time.Duration) R.Reader[Config, Response] {
          return R.Of[Config](callAPI(endpoint, key, timeout))
      }),
  )
}

When to Use Reader

Use Reader WhenConsider Alternative
Need dependency injectionSimple function parameters sufficient
Multiple operations share contextSingle operation doesn't need DI
Want testable, composable codeStruct-based DI is adequate
Context is read-onlyNeed mutable state (use State)