duct

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2025 License: MIT Imports: 1 Imported by: 1

README ยถ

Metaprogramming of computations (duct)

syntax represented by the free monad for a functor that provides a signature.

The duct module provides a composition pattern for declaring monadic computation using abstract morphisms. It offers a category-theory-inspired algebra that enables structured composition of transformations (ฦ’: A โŸผ B), forming composable computation pipelines (๐‘š: A โŸผ B). Instead of executing computations directly, duct constructs an abstract syntax tree (AST) that represents the computation declaratively.

%%{init: {'theme':'neutral'}}%%
graph LR
    subgraph "๐‘š: A โŸผ Z"
    direction LR
    A([๐‘“: A โŸผ B])
    B([๐‘“: B โŸผ C])
    C([๐‘“: C โŸผ ...])
    Z([๐‘“: ... โŸผ Z])
    A --> B
    B --> C
    C --"..."--> Z
    end
    subgraph "๐‘š: B โŸผ C"
    direction LR
    B0([๐‘“: B โŸผ C])
    B1([๐‘“: ... โŸผ ...])
    B2([๐‘“: ... โŸผ C])
    B --> B0
    B0 --"..."--> B1
    B1 --"..."--> B2
    B2 --> B
    end
    subgraph "๐‘š: ... โŸผ ..."
    direction LR
    B10([๐‘“: ... โŸผ ...])
    B11([๐‘“: ... โŸผ ...])
    B12([๐‘“: ... โŸผ ...])
    B1 --> B10
    B10 --"..."--> B11
    B11 --"..."--> B12
    B12 --> B1
    end

This approach is particularly useful for metaprogramming, where the separation of concerns is crucial. By modeling computation as a structured transformation pipeline, duct allows introspection, optimization, and transformation before execution. This makes it especially powerful in domains like query builders, code generation and infrastructure as a code, where the representation of computation is as important as the computation itself.

With duct, computations are first-class entities, enabling dynamic modification, analysis, and execution strategies that extend beyond traditional function composition.

Quick Example

Assuming initial and terminal objects โŸจ๐‘ฐ,๐‘ปโŸฉ. The initial object ๐‘ฐ has exactly one identity morphism ๐‘ฐ โŸผ A, can be thought of as the starting point in the system. Consequntly, the terminal object ๐‘ป has morphism B โŸผ ๐‘ป, can be thought of as the end point in the system.

๐‘š: ๐‘ฐ โŸผ ๐‘ป
  ๐‘“: ๐‘ฐ โŸผ A
  ๐‘“: A โŸผ B
  ๐‘“: B โŸผ ๐‘ป

Therefor, the morphism ๐‘š: ๐‘ฐ โŸผ ๐‘ป is defined using duct's algebra.

f := duct.Yield(
  duct.L1[string](/* target */),
  duct.Join(
    duct.L2[int, string](/* computation */),
    duct.From(
      duct.L1[int](/* source */),
    ),
  ),
)

Given notation has defined a system capable of sourcing int, transforming them to strings and emitting results. By itself this naive example defines clear separation of concerns approach--defining the data flow but abstracting away the specifics of each individual operation. For example, resulting abstract syntax tree can be materialized into queueing system: the initial object represents a source of incoming data from a producer; the terminal object represents the destination where data ends up; and inner morphism if a pure int โŸผ string transformer. The instantiation of AST in the concern of the application:

var visitor duct.AstVisitor
if err := f.Apply(visitor); err != nil {
  // handle error
}

Algebra

duct module offers a category theory-inspired algebra that allows the composition of transformers into high-level morphism.

๐‘š: A โŸผ B

A morphism Morphism[A, B] is key abstract transformer that maps from category A to category B. It represents an abstract syntax tree of computation. It is composable with combinators defined by this algebra. The objective of the algebra is to collapse the morphism ๐‘š: ๐‘ฐ โŸผ ๐‘ป, marking the completion of the side effect computation.

From(A) = idแดฌ : ๐‘ฐ โŸผ A

From[A] initializes a computation by binding a source from category A (e.g. it could correspond to the process of putting data into the system), creating a initial object morphism that does not perform any transformation initially (identity). This is only the valid way to start declaration of morphisms--each morphism is started by this construct.

Join(๐‘“, ๐‘š) = ๐‘“ โˆ˜ ๐‘š : A โŸผ C

Join[A, B, C] represents standard function composition, lifting an ordinary function ๐‘“: B โŸผ C into the morphism ๐‘š: A โŸผ B, producing a new morphism ๐‘š: A โŸผ C.

LiftF(๐‘“, ๐‘š) = ๐“•(๐‘“) โˆ˜ ๐‘š : A โŸผ ๐“•โบ(C)

LiftF[A, B, C] enables transformation within a functorial context while preserving the computational structure -- allows composition between a morphism ๐‘š: A โŸผ ๐“•(B), where ๐“• is is some functor and a function ๐‘“: B โŸผ C, which transforms elements inside the functor. LiftF builds a free monad that deferred computation representation without immediate execution or collapsing. Unlike standard Kleisli composition, which typically involves monadic binding ๐‘š โ‰ซ ๐‘“, LiftF does not perform joining of nested functorial structures. Instead, it maintains a nested form, allowing further transformations within the computational pipeline. The composition Unit(LiftF(๐‘“, ๐‘š) โˆ˜ g)) is equivalent to Unit(๐‘š โ‰ซ (๐‘“ โˆ˜ ๐‘”)) in monads, where ๐‘” is applied after ๐‘“, and the results are flattened into a single structure. It is a responsibility of creator of such a free monadic value to do something with those nested contexts either yielding individual elements or uniting into the monad.

WrapF(๐‘š) = LiftF(idแดฌ, ๐‘š) : A โŸผ ๐“•โบ(B)

WrapF[A, B] is equivalent to LiftF but does not apply any transformationโ€”it simply makes a free monad from existing morphism. It enables further composition (e.g. ๐‘“: B โŸผ C ) inside the nested context ๐“•โบ(B). This allows you to yield elements of ๐“•(B) without additional transformations, enabling further composition with terminal object.

Unit(๐‘š) = ๐œ‚ โˆ˜ ๐‘šโบ : ๐“•โบ(A) โŸผ ๐“•(B)

Unit[A, B] finalizes a transformation context by lifting a morphism into a functorial structure--unit is the monadic return (๐œ‚ : B โŸผ ๐“•(B)). It acts as the closure operand for free monad, ensuring that all compositions, such as those created by LiftF and WrapF, are fully resolved into a functorial form.

Yield(๐‘š) = โŸ˜แดฎ โˆ˜ ๐‘š : B โŸผ ๐‘ป

Yield[A, B] binds a morphism ๐‘š: A โŸผ B to a target in category B, effectively consuming the computation. This means yield represents a terminal operation, finalizing the morphism pipeline (e.g. a side effect, storage, output). As the final step, it does not return anything, indicating an end of computation.

Examples

Let's consider this algebra from trivial example: "Giving the recommendation on relevant products in category for user". There is an external system that signals account id. For each account, recommendation system obtains user's profile, recommends N most relevant categories and discovers K relevant product per category, resulting in N ร— K product recommendations:

// id 
// โˆ˜ (Account โŸผ User)
// โˆ˜ (User โŸผ ๐“•(Category)) 
// โˆ˜ ๐“•(Category โŸผ ๐“•(Product)) 
// โˆ˜ โŸ˜ 
a := duct.From(duct.L1(/* source */))
b := duct.Join(duct.L2[Account, User](/* ... */), a)
c := duct.Join(duct.L2[User, []Category](/* ... */), b)
d := duct.Unit(duct.LiftF(duct.L2[Category, []Product](/* ... */), c))
e := duct.Yield(duct.L1(/* target */), d)

The definition above yield [][]Product at once when the morphism completes. Alternatively, you can yield each Product recommendation.

// id
// โˆ˜ (Account โŸผ User)
// โˆ˜ (User โŸผ ๐“•โบ(Category))
// โˆ˜ (Category โŸผ ๐“•โบ(Product))
// โˆ˜ โŸ˜ 
a := duct.From(duct.L1(/* source */))
b := duct.Join(duct.L2[Account, User](/* ... */), a)
c := duct.Join(duct.L2[User, []Category](/* ... */), b)
d := duct.WrapF(duct.LiftF(duct.L2[Category, []Product](/* ... */), c))
e := duct.Yield(duct.L1(/* target */), d)

The declaration result abstract syntax tree. The application defines own principles of its materialization (e.g it can use infrastructure as a code to deploy the computation pipeline).

Why This Abstraction Implements a Free Monad Structure

This module provides an AST-based Free Monad, enabling composable and deferred computations while preserving functorial transformations. The structure adheres to category theory principles and satisfies the definition of a Free Monad as follows:

A Free Monad over a functor ๐“• is defined as:

Free ๐“• A = A + ๐“•(Free ๐“• A)

This definition implies:

  1. It consists of either A pure value (Pure) representing an already computed result or inner computation, capturing a functorial transformation without immediate execution.
  2. It enables chained computations (flatMap) without collapsing intermediate steps prematurely.
  3. It maintains compositional structure until explicitly resolved.

This API implements a Free Monad using an AST-based execution model, where transformations are staged and composed before execution. The core functions that establish this structure are:

  1. LiftF(f, m): Morphism[A, C] Lifts a functorial transformation ๐‘“: B โŸผ C into a deferred computation. This is analogous to ๐“•(Free ๐“• A), capturing computations without execution.

  2. Join(f, m): Morphism[A, C] composes a transformation ๐‘“: B โŸผ C with an existing morphism ๐‘š: A โŸผ B, producing a new staged computation. This behaves like flatMap, applying a function inside the monadic context.

  3. Unit(m): Morphism[A, []B] resolves and finalizes all staged transformations. This collapses the monadic structure, much like evaluating a Free monad via an interpreter.

The abstraction satisfies the monad laws: Left Identity (return x >>= f behaves the same as f(x)). Unit(Join(๐‘“, WrapF(๐‘š))) ensures that lifting and joining a function preserves its behavior.

Right Identity (m >>= return behaves the same as m.). Since Join(id, ๐‘š) == ๐‘š, composing with an identity function preserves structure.

Associativity ((m >>= f) >>= g behaves the same as m >>= (fun x -> f x >>= g)). Unit(Join(g, LiftF(f))) == flatMap(f โˆ˜ g), ensuring that function composition follows monadic associativity.

Read How the Free Monad and Functors Represent Syntax if your are looking deep-diving into Free Monands for a powerful way to build expressions in a domain-specific language.

How To Contribute

The library is MIT licensed and accepts contributions via GitHub pull requests:

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

The build and testing process requires Go version 1.18 or later.

Build and run in your development console.

git clone https://github.com/fogfish/golem
cd golem/duct
go test

License

See LICENSE

Documentation ยถ

Index ยถ

Constants ยถ

View Source
const Version = "duct/v0.0.1"

Variables ยถ

This section is empty.

Functions ยถ

func TypeOf ยถ

func TypeOf[T any]() string

TypeOf returns normalized name of the type T.

Types ยถ

type Ast ยถ

type Ast interface {
	Apply(depth int, v Visitor) error
}

Abstract syntax tree (AST) of the computation defined by morphism

type AstFrom ยถ

type AstFrom struct {
	Type   string
	Source any
}

AST element for the input to morphism ฦ’: รธ โŸผ A

func (AstFrom) Apply ยถ

func (node AstFrom) Apply(depth int, v Visitor) error

type AstMap ยถ

type AstMap struct {
	TypeA, TypeB string
	F            any
}

AST element representing transformation ฦ’: A โŸผ B

func (AstMap) Apply ยถ

func (node AstMap) Apply(depth int, v Visitor) error

type AstSeq ยถ

type AstSeq struct {
	Root     bool
	Deferred bool
	Seq      []Ast
}

AST element representing morphism, sequence of transformations ๐‘š: A โŸผ B โŸผ ... โŸผ F

func (AstSeq) Apply ยถ

func (n AstSeq) Apply(depth int, v Visitor) error

type AstVisitor ยถ

type AstVisitor struct{}

Empty abstract syntax tree visitor

func (AstVisitor) OnEnterFrom ยถ

func (AstVisitor) OnEnterFrom(depth int, node AstFrom) error

func (AstVisitor) OnEnterMap ยถ

func (AstVisitor) OnEnterMap(depth int, node AstMap) error

func (AstVisitor) OnEnterMorphism ยถ

func (AstVisitor) OnEnterMorphism(depth int, node AstSeq) error

func (AstVisitor) OnEnterSeq ยถ

func (AstVisitor) OnEnterSeq(depth int, node AstSeq) error

func (AstVisitor) OnEnterYield ยถ

func (AstVisitor) OnEnterYield(depth int, node AstYield) error

func (AstVisitor) OnLeaveFrom ยถ

func (AstVisitor) OnLeaveFrom(depth int, node AstFrom) error

func (AstVisitor) OnLeaveMap ยถ

func (AstVisitor) OnLeaveMap(depth int, node AstMap) error

func (AstVisitor) OnLeaveMorphism ยถ

func (AstVisitor) OnLeaveMorphism(depth int, node AstSeq) error

func (AstVisitor) OnLeaveSeq ยถ

func (AstVisitor) OnLeaveSeq(depth int, node AstSeq) error

func (AstVisitor) OnLeaveYield ยถ

func (AstVisitor) OnLeaveYield(depth int, node AstYield) error

type AstYield ยถ

type AstYield struct {
	Type   string
	Target any
}

AST element for the output of morphism ฦ’: A โŸผ รธ

func (AstYield) Apply ยถ

func (node AstYield) Apply(depth int, v Visitor) error

type F ยถ

type F[A, B any] struct {
	// contains filtered or unexported fields
}

Binary type A, B

func L2 ยถ

func L2[A, B any](f any) F[A, B]

Lifts a value into a binary type A, B

type Morphism ยถ

type Morphism[A, B any] struct {
	// contains filtered or unexported fields
}

Morphism ๐‘š: A โŸผ B is an abstract transformer of category `A` to `B`.

func From ยถ

func From[A any](source T[A]) Morphism[A, A]

From create new morphism ๐‘“: รธ โŸผ A, binding it with source of category `A`.

func Join ยถ

func Join[A, B, C any](f F[B, C], m Morphism[A, B]) Morphism[A, C]

Compose transformer ๐‘“: B โŸผ C with morphism ๐‘š: A โŸผ B producing a new morphism ๐‘š: A โŸผ C.

func LiftF ยถ

func LiftF[A, B, C any](f F[B, C], m Morphism[A, []B]) Morphism[A, C]

Compose the transformer ๐‘“: B โŸผ C with the morphism ๐‘š: A โŸผ []B to produce a free monad ๐‘šโบ: A โŸผ []C that enables transformation within a functorial context while preserving the computational structure without immediate collapsing. Unlike traditional flatMap, LiftF preserves the transformer ๐‘“'s context, enabling further composition inside the transformation. Specifically, Join(g, LiftF(f)) is equivalent to flatMap(f โˆ˜ g), where the function g is applied after f, and the results are flattened into a single structure.

It is a responsibility of creator of such a free monadic value to do something with those nested contexts either yielding individual elements or uniting into the monad (e.g. use Unit(Join(g, LiftF(f))) to leave nested context into the morphism ๐‘š: A โŸผ []C).

func Unit ยถ

func Unit[A, B any](m Morphism[A, B]) Morphism[A, []B]

Unit finalizes a transformation context by collapsing the free monadic value of the morphism ๐‘š: A โŸผ []B. It acts as the terminal operation, ensuring that all staged compositions, such as those built with LiftF and WrapF, are fully resolved into a single, consumable form.

func WrapF ยถ

func WrapF[A, B any](m Morphism[A, []B]) Morphism[A, B]

WrapF is equivalent to LiftF but operates directly on the inner structure of the morphism ๐‘š: A โŸผ []B, extracting individual elements of B while preserving the transformation context, enabling further composition.

Usable to Yield elements of []B without transformation

func Yield ยถ

func Yield[A, B any](target T[B], m Morphism[A, B]) Morphism[A, Void]

Yield results of ๐‘š: A โŸผ B binding it with target of category `B`.

func (Morphism[A, B]) Apply ยถ

func (seq Morphism[A, B]) Apply(v Visitor) error

Applies visitor over computation elements within morphism ๐‘š: A โŸผ B.

type OnFrom ยถ

type OnFrom interface {
	OnEnterFrom(depth int, node AstFrom) error
	OnLeaveFrom(depth int, node AstFrom) error
}

type OnMap ยถ

type OnMap interface {
	OnEnterMap(depth int, node AstMap) error
	OnLeaveMap(depth int, node AstMap) error
}

type OnMorphism ยถ

type OnMorphism interface {
	OnEnterMorphism(depth int, node AstSeq) error
	OnLeaveMorphism(depth int, node AstSeq) error
}

Visits root morphism

type OnSeq ยถ

type OnSeq interface {
	OnEnterSeq(depth int, node AstSeq) error
	OnLeaveSeq(depth int, node AstSeq) error
}

Visist nested transformers

type OnYield ยถ

type OnYield interface {
	OnEnterYield(depth int, node AstYield) error
	OnLeaveYield(depth int, node AstYield) error
}

type T ยถ

type T[A any] struct {
	// contains filtered or unexported fields
}

Unary type A

func L1 ยถ

func L1[A any](f any) T[A]

Lifts a value into a unary type A

type Visitor ยถ

type Visitor interface {
	OnMorphism
	OnSeq
	OnMap
	OnFrom
	OnYield
}

Visitor over abstract syntax tree

type Void ยถ

type Void any

Void type to emulate empty type รธ

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL