How to Automatically Implement Functions for the Reader Monad Using CurryHoward

CurryHoward’s implement macro synthesizes fully working Scala functions from type signatures alone, allowing you to obtain Reader monad operations like point, map, and flatMap without writing any implementation code.

To automatically implement functions for the Reader monad using CurryHoward, you define the Reader type as a simple function E => A and use the implement macro in place of method bodies. The macro, located in src/main/scala/io/chymyst/ch/package.scala, inspects the left-hand side of your definition, constructs a type expression, and invokes an automated theorem prover to generate the corresponding lambda term.

Understanding the Reader Monad Structure

The Reader monad represents computations that depend on a shared environment. In Scala, it is naturally expressed as a function type from some environment E to a result A.

In the CurryHoward test suite, the Reader is defined as type Reader[A] = Int ⇒ A in src/test/scala/io/chymyst/ch/unit/LawsSpec.scala at line 26. This type alias serves as the foundation for generating the three core monadic operations: point (also called pure or return), map (functor), and flatMap (monad bind).

How the implement Macro Synthesizes Code

The automatic implementation process follows a pipeline from type signature to executable Scala AST. Understanding these internals helps debug compilation errors and verify that generated code satisfies the expected laws.

Macro Entry Point and Type Inspection

The user-facing API is the implement macro, defined at lines 27-29 of src/main/scala/io/chymyst/ch/package.scala. This zero-argument macro immediately forwards to Macros.inhabitImpl[U], where U represents the return type inferred from the left-hand side of the definition.

The macro captures the lexical scope, including local vals and method parameters, making them available as premises to the theorem prover.

Building Type Expressions from Scala Types

Before proving, the macro must translate Scala's c.Type into CurryHoward's internal TypeExpr representation. This occurs in Macros.buildTypeExpr at lines 85-90 of src/main/scala/io/chymyst/ch/Macros.scala.

For the Reader monad, the function arrow => becomes a ConjunctT … #-> … node, representing implication in the logic. This transformation allows the prover to treat function arguments as hypotheses and the return type as the goal.

Theorem Proving and Term Generation

The search for an implementation happens in Macros.inhabitImpl (lines 57-74 of src/main/scala/io/chymyst/ch/Macros.scala). This method:

  1. Collects all symbols in scope (including class members and local definitions).
  2. Constructs a "type structure" representing the entire function signature.
  3. Invokes inhabitOneInternal, which delegates to TheoremProver.inhabitInternal (lines 35-44) to search for a proof term with minimal information loss.

The theorem prover performs a constructive proof search, ensuring that the generated term is the most direct implementation possible given the available premises.

Emitting Scala ASTs

Once a proof term (TermExpr) is found, Macros.returnTerm converts it back into a Scala AST (c.Tree) using emitTermCode. For ordinary functions, this produces a lambda expression; for higher-arity functions, it may wrap the result in a FunctionNLambda to preserve the original lambda structure (lines 58-70 of src/main/scala/io/chymyst/ch/Macros.scala).

The resulting code is then inlined at the call site, giving you a zero-overhead abstraction with no runtime reflection.

Automatically Implementing Reader Monad Functions

With the macro machinery understood, you can define the complete Reader monad instance using only type signatures. The following example mirrors the implementation verified in src/test/scala/io/chymyst/ch/unit/LawsSpec.scala at lines 14-33.

import io.chymyst.ch._

// Define the Reader type as a function from environment E to value A
type Reader[E, A] = E => A

// 1. Point (pure) - lifts a value into the Reader context
def pointReader[E, A]: A => Reader[E, A] = implement

// 2. Map (functor) - transforms the result value while keeping the environment
def mapReader[E, A, B]: Reader[E, A] => (A => B) => Reader[E, B] = implement

// 3. FlatMap (monad) - sequences readers, passing the environment through both
def flatMapReader[E, A, B]: Reader[E, A] => (A => Reader[E, B]) => Reader[E, B] = implement

When compiled, the macro expands these definitions into the standard Reader monad implementations:

  • pointReader returns a function that ignores the environment and returns the original value.
  • mapReader composes the reader with a transformation function.
  • flatMapReader passes the environment to the first reader, uses its result to determine the second reader, and applies the environment to that second reader.

Verifying Monad Laws with Generated Code

The CurryHoward test suite confirms that these macro-generated functions satisfy the monad laws. In src/test/scala/io/chymyst/ch/unit/LawsSpec.scala, the generated pointReader, mapReader, and flatMapReader are tested against the functor and monad laws using ScalaCheck-style property tests.

Because the theorem prover constructs terms with minimal information loss, the generated implementations are guaranteed to be the canonical ones that satisfy the type signatures. For the Reader monad, this means the macro produces exactly the lawful implementations you would write by hand, ensuring associativity, left identity, and right identity hold for flatMapReader with pointReader.

Summary

  • CurryHoward’s implement macro synthesizes Scala code from types alone by treating function signatures as logical propositions and constructing proof terms via TheoremProver.inhabitInternal.
  • Reader monad functionspoint, map, and flatMap—can be fully implemented using only type signatures and the implement macro, as demonstrated in src/test/scala/io/chymyst/ch/unit/LawsSpec.scala.
  • The implementation pipeline involves type expression building in Macros.buildTypeExpr, proof search in Macros.inhabitImpl, and code emission via emitTermCode in Macros.returnTerm.
  • Generated code is lawful: The theorem prover’s minimization strategy ensures the resulting Reader monad instances satisfy functor and monad laws without manual verification.

Frequently Asked Questions

Can CurryHoward generate Reader monad instances for any environment type?

Yes, the macro is generic over the environment type E. In src/test/scala/io/chymyst/ch/unit/LawsSpec.scala, the tests use Int as the environment, but you can define type Reader[E, A] = E => A for any E. The theorem prover treats the environment as an implicit hypothesis available to the proof term, regardless of its specific type.

How does the macro handle higher-kinded types like Reader?

CurryHoward operates on the fully resolved type representation after Scala’s type checker has applied all higher-kinded type constructors. When you write Reader[E, A] => (A => B) => Reader[E, B], the macro sees the underlying function type (E => A) => (A => B) => (E => B). It then builds the corresponding TypeExpr with implication nodes (#->) and searches for a proof, effectively handling the higher-kinded structure by working with its concrete representation.

Is the generated Reader monad code efficient?

Yes, the generated code is a zero-overhead abstraction. The macro expands into standard Scala lambda expressions at compile time, as seen in Macros.returnTerm and emitTermCode. There is no runtime reflection or intermediate data structure overhead. The resulting bytecode is identical to what you would write manually: simple function composition for map and environment threading for flatMap.

What happens if the type signature is ambiguous or uninhabited?

If the theorem prover cannot construct a proof term for the given type signature, compilation fails with an error indicating that no implementation could be found. This occurs in TheoremProver.inhabitInternal when the search space is exhausted without finding a valid term. For the Reader monad, this protects you from defining nonsensical operations like Reader[E, A] => (B => A) => Reader[E, B] where the types don't align for a lawful transformation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →