refactor: Builder and ConfigBuilder are structurally identical monads — unify via StateT #28

Closed
opened 2026-06-10 14:48:48 +02:00 by yorunikakeru · 0 comments
Owner

Problem

DSL.Builder contains two nearly identical monad implementations:

newtype Builder scope a = Builder
    { runBuilder :: SectionState -> Either DomainError (a, SectionState) }

newtype ConfigBuilder a = ConfigBuilder
    { runConfigBuilder :: ConfigState -> Either DomainError (a, ConfigState) }

Both have hand-written Functor, Applicative, and Monad instances that are structurally identical. The only difference is the state type (SectionState vs ConfigState).

Consequence

Any new combinator (rollback, local scope, inspection) must be written twice. The instances are copy-paste boilerplate that can silently drift if one is modified.

Options

Option A — Use StateT s (Either DomainError)

type Builder scope = StateT SectionState (Either DomainError)
type ConfigBuilder  = StateT ConfigState  (Either DomainError)

Eliminates both hand-written monad implementations. runBuilder becomes runStateT. Requires transformers or mtl dependency.

Option B — Single polymorphic newtype

newtype GenBuilder s a = GenBuilder
    { runGenBuilder :: s -> Either DomainError (a, s) }

Derive all instances once. Builder scope and ConfigBuilder become type aliases. No new dependency.

Option C — Keep as-is but add a HasField-based abstraction

Low-risk, no new dependency, but does not remove the duplication.

## Problem `DSL.Builder` contains two nearly identical monad implementations: ```haskell newtype Builder scope a = Builder { runBuilder :: SectionState -> Either DomainError (a, SectionState) } newtype ConfigBuilder a = ConfigBuilder { runConfigBuilder :: ConfigState -> Either DomainError (a, ConfigState) } ``` Both have hand-written `Functor`, `Applicative`, and `Monad` instances that are structurally identical. The only difference is the state type (`SectionState` vs `ConfigState`). ## Consequence Any new combinator (rollback, local scope, inspection) must be written twice. The instances are copy-paste boilerplate that can silently drift if one is modified. ## Options **Option A — Use `StateT s (Either DomainError)`** ```haskell type Builder scope = StateT SectionState (Either DomainError) type ConfigBuilder = StateT ConfigState (Either DomainError) ``` Eliminates both hand-written monad implementations. `runBuilder` becomes `runStateT`. Requires `transformers` or `mtl` dependency. **Option B — Single polymorphic newtype** ```haskell newtype GenBuilder s a = GenBuilder { runGenBuilder :: s -> Either DomainError (a, s) } ``` Derive all instances once. `Builder scope` and `ConfigBuilder` become type aliases. No new dependency. **Option C — Keep as-is but add a `HasField`-based abstraction** Low-risk, no new dependency, but does not remove the duplication.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
FrogOS/DSL#28
No description provided.