Skip to content

on-the-ground/effect_ive_go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

80 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation


โ€œThe Zen of the Effect-ive Gopherโ€ โ€“ calm, centered, and side-effect free.

What is Effect-ive Go?

Effect-ive Go is the first attempt to implement Effect-ive Programming idiomatically in Go. This project provides a systematic way to isolate and handle side effects on top of Goโ€™s core principlesโ€”goroutines, channels, context, and duck typing.


What is Effect-ive Programming?

Effect-ive Programming is an approach to building predictable, testable, and reusable code.

Its first-class citizen is the Effect Pattern: the idea is to identify impure parts of your logic (=side effects), and isolate and delegate them externally.

This is a pattern-oriented approach, not tied to any specific language or paradigm, making it applicable in any environment.

Effect-ive Programming generally follows three steps:

  • ๐Ÿ” Side effect recognition: Identify non-pure logic
  • ๐Ÿงญ Side effect isolation: Isolate it into an external handler
  • ๐Ÿ“ฌ Side effect delegation: Delegate the effect to the handler

Why does Effect-ive Programming matter?

In real-world applications, side effects are everywhereโ€”logging, reading config, managing shared state, spawning goroutines, or interacting with the network. These effects make our code:

  • harder to test

  • difficult to reason about

  • fragile to reuse or compose

Effect-ive Programming offers a systematic way to isolate and delegate those side effects, so your business logic remains:

  • predictable โ€” no hidden interactions or surprises

  • testable โ€” you can mock or replace effects freely

  • reusable โ€” pure logic is portable across contexts

By explicitly recognizing effects and pushing them to the boundary of your application, you enforce separation of concerns and avoid coupling your core logic to runtime behavior.

Even in languages like Goโ€”without native effect systemsโ€”you can regain this structure through simple conventions and disciplined handler design.
This is what Effect-ive Go makes possible.


โœ… Before / After examplesโœ… Practical issues in Go: DI, error handling, config, context propagation, etc.(TBD)


What is Effect anyway?

๐Ÿ’ก Pure Functions

  • Executed immediately upon call
  • Deterministic: Output depends only on input
  • Do nothing besides returning the output (no side effects)

โš ๏ธ Side Effects

  • Everything that is not a pure function
  • A helpful guide: the 4W Checklist โ€” if you violate even one, itโ€™s a side effect:

๐Ÿงญ Categorizing Effects by the 4W Criteria

โœ… An effect is anything that violates Who, When, Where, or What.

An effect arises when a function violates one or more of the following 4W guarantees. Each type of violation leads to a specific class of side effect.

4W Criteria Effect Type Description Examples
Who: Control Flow Ownershipโ€œIs someone else executing this?โ€ Concurrency Effect Delegates execution to another unit of control flow (thread, goroutine, etc.) go func() { ... }
When: Execution Timing Guaranteeโ€œIs execution guaranteed immediately?โ€ Task Effect Execution may happen later, depending on the runtime or external trigger http.Get(...)
Where: Context Awarenessโ€œWhich context is this running in?โ€ Binding / State Effect Behavior depends on the current scoped context or environment context.WithValue()
request.Context()
ThreadLocal
this(JavaScript)
What: Predictable Resultโ€œDoes the same input always produce the same output?โ€ Time / Random / IO Effect Depends on or mutates internal/external state rand.Intn(n)
time.Now()
globalCounter++
db.Save()

๐Ÿ”ง Effect Pattern

The purpose of the effect pattern is simple:

๐Ÿ’ฌ Delegate side effects to external handlers, so your logic remains predictable, testable, and reusable.

๐Ÿ” 1. Side Effect Recognition

  • If even one of the 4Ws is violated, it qualifies as a side effect.
  • But not every side effect should be turned into a new effect.
๐ŸŽฏ Effect Atomicity Principle

If a side effect can be composed using existing effects + pure functions, do not define a new effect.


๐Ÿงญ 2. Side Effect Isolation

  • Each effect is handled by a dedicated effect handler
  • Every handler has a clear scope:
    • Only the closest handler is applied โ€” no implicit propagation
    • Leaving the scope automatically shuts down the handler
    • Handlers must not leak state outside their scope
  • Effect scope should be close to where effects are performed
    • Avoid placing all handlers at the composition root

๐Ÿ“ฌ 3. Side Effect Delegation

Delegation is more than passing data โ€” ๐Ÿ‘‰ itโ€™s about transferring control flow ownership to the handler.

โœณ๏ธ There are three types of delegation:
Type Description
Resumable Effect The handler processes the effect and returns the result to the initiator, who waits for the handling to complete before resuming.
Fire-and-Forget The initiator triggers the effect and resumes immediately, without waiting for the handler to finish processing.
Abortive Effect The handler processes the effect and terminates the initiator's flow, e.g., by raising a panic or error that escapes the current scope.

๐Ÿ“Œ Summary

  • An effect is any logic that depends on context, has external interaction, or violates pure function guarantees
  • The Effect Pattern provides a structured way to recognize, isolate, and delegate them
  • It forms a core foundation for testable, modular, reusable architecture

How does Effect-ive Go work?

Effect-ive Go is designed around the idea of delegating side effects to dedicated handlers. Its core structure can be broken down into three main components:


1. ๐Ÿงฐ Effect Handler

Each handler is responsible for handling a specific type of side effect. All handlers satisfy the following conditions:

  • Scoped: Handlers are only active within a specific context
  • Channel-based message handling: Handlers run in goroutines and communicate through channels
  • Explicit lifecycle management: Use WithXxxEffectHandler(ctx) to create and defer cancel() to release
  • ๐Ÿ”’ Not thread-safe by design: Handlers are meant to be used only within a single goroutine to ensure proper scoping

2. ๐Ÿงญ Effect Scope

Each handler is bound to a specific context.

  • Within that context, the effect is enabled
  • Once the context is exited, the handler is automatically shut down, and no effects can be handled

This guarantees:

  • โŒ No handler leaks
  • โŒ No upward propagation of effects (only explicit delegation is allowed)
  • โœ… Handler lifecycle is tracked via context

3. ๐Ÿ” Effect Delegation

Effect-ive Go supports two main delegation models: resumable and fire-and-forget.

Type Description Examples
Resumable Effect Processes the effect and returns the result Reading config, state access
Fire-and-Forget Executes asynchronously without waiting for a result Logging, sending events

โ†’ Both models are implemented using goroutines + channels, and delegation always involves handing off control flow ownership at the point of perform.


๐Ÿ” Example Flow

1. A handler scope is created with `WithStateEffectHandler(ctx)`
2. Internally, a handler goroutine is spawned and waits on a `chan`
3. Domain logic calls `PerformResumableEff(ctx, EffectState, payload)`
4. Handler processes the effect and sends result back on `resumeCh`
5. Caller resumes execution with the result

4. ๐Ÿง  Declaring Effects Explicitly

Another important role of Effect-ive Programming is to explicitly surface all effects that occur within a given function. In statically-typed languages with strong type inference, not only the kind of effects, but even their payload types can be reflected directly in function signatures.

In monadic systems, effects are expressed in the return type. In handler-based systems, effects are reflected by the presence of handler types in the context or surrounding scope.

This means that every function, from the one that performs the effect to the one that handles it, must reveal the effect type in its signature. However, Go lacks certain language features that make this kind of typing feasible:

  • โŒ No sum types (tagged unions)
  • โŒ No annotations or decorators to mark effect usage
  • โŒ Function signatures canโ€™t express anything beyond parameters and return values

โœ… Effect-ive Go introduces two complementary strategies:

1๏ธโƒฃ Effect-ive Lint (Static Analysis + Comment Injection)

A proposed tool will statically analyze the call stack from effect site to handler, and inject comments like this:

// @effect: Log, State, Config
func MyServiceFunc(...) { ... }

At the composition root, if these comments remain, it means the handler for that effect is missing.

2๏ธโƒฃ Die Loudly (Runtime Safety Net)

If an effect is performed without a handler in scope, the ideal behavior would be a compile-time failure. Since Go doesnโ€™t support this, Effect-ive Go panics at runtime instead.

This โ€œdie loudlyโ€ approach ensures that missing handlers are detected early during development and testing.

But caution: In rarely executed branches, an unhandled effect might sneak into production. Thatโ€™s why careful code review and testing is essential when using dynamic effect systems in Go.


๐Ÿ“Œ Additional Design Notes

  • Handlers are registered into context using EffectEnum as a key
  • Hash partitioning is supported via the Partitionable interface
  • Designed to be panic-safe, explicitly terminated, and single-threaded for maximum predictability

How can I make custom effects? (TBD)

โ†’ Guide for users to define their own effect handlersโ†’ How to apply scope and implement custom logic

โœ๏ธ Custom Effect Design Tips

While building your own custom effects, especially ResumableEffect, keep the following principle in mind:

โš ๏ธ Avoid invoking resumable effects inside other effect handlers.

Why?

Resumable effects involve not just side-effect execution but also waiting for a response. If a handler itself triggers another resumable effect, you introduce:

  • โ— Coupling between effects

  • โ— Non-determinism in handler behavior

  • โ— Hidden control flow delegation, which breaks testability and predictability

๐Ÿ”„ Analogy

This is similar to avoiding "nested monads" in functional programming. Instead of:

// โŒ Don't do this inside an effect handler 
msg.ResumeCh <- PerformResumableEff(ctx, EffectConfig, payload)

Do this instead:

// โœ… Do it outside and pass as explicit input 
configValue := PerformResumableEff(ctx, EffectConfig, payload)  PerformResumableEff(ctx, MyEffect, configValue)

๐Ÿง˜โ€โ™‚๏ธ Principle

Keep your effects flat.
Handlers should not rely on other handlers to work correctly.

Let the caller take control of effect composition:

  • Scope the config effect handler first

  • Query the value

  • Then pass it to the other effect handler as an input

๐Ÿ’ก Exception?

Fire-and-Forget effects (e.g., logging) are safe to call inside other handlers.

// โœ… This is fine inside any handler 
LogEff(ctx, LogInfo, "processing payload", map[string]any{"key": payload.Key})

Because they do not block and do not depend on return values, theyโ€™re harmless and often useful for internal visibility.


Built-in Effects

Effect Type Handler Responsibility
๐Ÿง  State Resumable Manage key-value state across goroutines
Sharded with Partitionable
๐Ÿ”— Binding Resumable Lookup config/flags/envs, with upper-scope fallback
๐Ÿงต Concurrency Fire-and-Forget Spawn goroutines, gracefully terminate on cancel
๐Ÿ“ Log Fire-and-Forget Async log emission via zap logger; ordering via numWorkers = 1

๐Ÿ›  Real-World Refactoring Examples

1. Configuration Lookup (Binding Effect)

๐Ÿ”ด Before

apiKey := os.Getenv("API_KEY")
if apiKey == "" {
    log.Fatal("missing API_KEY")
}

โœ… After (Effect-ive)

apiKey, err := GetFromBindingEffect[string](ctx, "API_KEY")
if err != nil {
    LogEff(ctx, LogError, "missing binding", map[string]any{"key": "API_KEY"})
    return err
}

2. Logging (Log Effect)

๐Ÿ”ด Before

log.Printf("Processing user %s", userID)

โœ… After (Effect-ive)

LogEff(ctx, LogInfo, "processing user", map[string]any{"userID": userID})

3. State Management (State Effect)

๐Ÿ”ด Before

cache := make(map[string]any)
cache["session"] = sessionData

โœ… After (Effect-ive)

StateEff(ctx, SetStatePayload{Key: "session", Value: sessionData})

4. Spawning Goroutines (Concurrency Effect)

๐Ÿ”ด Before

go func() {
    process(userID)
}()

โœ… After (Effect-ive)

ConcurrencyEff(ctx, []func(context.Context){
    func(ctx context.Context) { process(userID) },
})

๐Ÿค” Why is there no RaiseEffect?

Some languages treat exceptions or errors as effectsโ€”an unexpected break from the normal execution flow. But Go's approach is fundamentally different, which is why Effect-ive Go does not provide a built-in RaiseEffect.

Here's why:

  • โœ… In Go, errors are values.

    Errors are part of the regular return values (val, err := ...). They're not magical, and they donโ€™t break flow like exceptions.

  • โœ… Go supports multiple return values.

    Unlike most languages (Java, Python, etc.) that return only a single value, Go can return a result and an error side-by-side. This removes the need for an external RaiseEffect.

  • โœ… Error handling is not an effect in Go's model.

    Effects typically represent external interactions or non-determinism (e.g., time, I/O, logging). In contrast, Go treats errors as part of deterministic flow: they propagate up as values.

  • โœ… Errors in Go are not "events".

    Theyโ€™re not thrown and caught like in other languages. They're wrapped, enriched with context, and passed up the call chain explicitly.

So, while effect systems in other languages (like Kotlin or OCaml 5) must model error propagation as an effect, Go's idioms make this unnecessary.

๐Ÿ“Œ If your app is raising exceptions as control flow, reconsider. In Go, explicit error returns remain idiomaticโ€”and Effect-ive Go encourages this clarity.


Effect-ive Programming vs Traditional Patterns

๐Ÿง  vs Functional Programming

  • Shares the same goal as FP: isolating impurity for predictable, testable code
  • Does not enforce everything to be pure functions chained by composition
  • Focuses on recognition and delegation of effects through Effect Pattern, not the implementation style

โš™๏ธ vs Monad-based Systems

  • Monads chain effectful computations but introduce complexity
  • Composing multiple effects (e.g., Eff1[Eff2[T]]) breaks intuition
  • Algebraic Effect Handlers emerged to fix this
  • Effect-ive Programming offers a pragmatic and idiomatic alternative

๐Ÿงญ vs Effect Handler-Oriented Programming

Aspect Effect-ive Programming EH-Oriented Programming
Goal Practical side effect isolation (SoC, testability, reuse) Algebraic Effect abstraction at compiler level
Handler Role Scope-bound isolation using goroutine/context/channel Control flow rewiring via CPS
Implementation Stays idiomatic (Go-specific) Requires new features (CPS, algebraic effect)
Design Limit Stays within language boundaries Needs runtime/compiler support
Recognition Based on 4W: Who, What, When, Where Transformed effect contexts
Use Cases Real-world concerns (log, state, cache, config, concurrency) Language effects (exceptions, yield, fibers)

๐Ÿ”Œ vs Dependency Injection Frameworks

Using effect systems like a DI container is a common anti-pattern.

๐Ÿ›‘ You should not return a service โ€” just request an action and receive the result.

โŒ Anti-pattern

svc := PerformEff(ctx, EffectDependency)
return svc.DoSomething()

โœ… Proper Usage

result := PerformEff(ctx, EffectDoSomething, input)
return result

Effect systems are not about injecting helpers โ€” theyโ€™re about requesting side effects and receiving results.

๐Ÿง  Don't โ€œget something to do the jobโ€โ€”instead, โ€œask for the job to be done.โ€


๐Ÿง˜โ€โ™‚๏ธ Effect-ive Go follows โ€œThe Zen of Goโ€ Designed with simplicity, clarity, and maintainability at heart. From scope lifecycle to panic safety, every decision is deliberate. Because Go deserves idiomatic effect handling โ€” with Zen.