How curryhoward Handles Case Classes and Sealed Traits for Code Generation
CurryHoward uses Scala reflection to detect case classes and sealed traits, translates them into algebraic types (NamedConjunctT for products and DisjunctT for sums), and generates idiomatic Scala code through a theorem prover and macro-based emitter.
The curryhoward library automates Scala code generation by treating types as logical propositions and programs as proofs. When working with curryhoward case classes sealed traits code generation, the macro system must accurately reflect on Scala's algebraic data structures to produce correct, type-safe implementations.
Detecting and Representing Case Classes
The macro entry point Macros.buildTypeExpr in src/main/scala/io/chymyst/ch/Macros.scala inspects type symbols to distinguish between case classes, case objects, and other types.
Case Classes as NamedConjunctT
When the macro encounters a case class, it collects the accessor methods to extract field names and types:
if (finalTypeSymbol.asClass.isCaseClass) {
// collect accessor methods (case fields)
val (accessors, typeExprs) = finalType.decls
.collect { case s: MethodSymbol if s.isCaseAccessor ⇒
val accessorType = buildTypeExpr(s.typeSignature.resultType, Seq(), typesSeenNow)
val substitutedType = TypeExpr.substNames(accessorType, typeMap.toMap)
(s.name.decodedName.toString, substitutedType) }
.toList.unzip
val wrapped = if (typeExprs.isEmpty) List(UnitT(typeName)) else typeExprs
NamedConjunctT(typeName, matchedTypeArgs, accessors, wrapped)
}
Source: [Macros.scala – lines 95‑108](https://github.com/chymyst/curryhoward/blob/master/src/main/scala/io/chymyst/ch/Macros.scala#L95-L108)
The NamedConjunctT type expression stores the constructor name, type parameters, field names (accessors), and field types (wrapped). This representation allows the theorem prover to treat case classes as product types while preserving the specific constructor information needed for code generation.
Case Objects and Empty Argument Lists
For case objects, the macro detects isModuleClass and creates a NamedConjunctT with empty accessor and wrapped lists. Empty-argument case classes contain a single UnitT in their wrapped list. These cases are handled in Macros.scala and verified in the test suite.
Source: [MacrosSpec.scala – lines 93‑100](https://github.com/chymyst/curryhoward/blob/master/src/test/scala/io/chymyst/ch/unit/MacrosSpec.scala#L93-L100)
Handling Sealed Traits as Sum Types
Sealed traits and abstract classes with case class implementations represent sum types (disjunctions) in the curryhoward system.
DisjunctT for Sealed Traits
When buildTypeExpr encounters a sealed trait, it validates that all direct subclasses are case classes or case objects, then constructs a DisjunctT:
if ((typeSymbol.asClass.isTrait || typeSymbol.asClass.isAbstract) &&
subclasses.nonEmpty &&
subclasses.forall { s ⇒
val resultClass = s.typeSignature.resultType.typeSymbol.asClass
resultClass.isCaseClass || resultClass.isModuleClass
}) {
// Build a DisjunctT from each subclass
subclasses.map { s ⇒
val subclassType = buildTypeExpr(s.asType.toType, Seq(), typesSeenNow)
// handle type‑parameter substitution for GADTs …
if (subclassType.typeParams.nonEmpty) { … }
else subclassType
} match {
case part :: Nil => part
case parts => DisjunctT(typeName, matchedTypeArgs, parts.asInstanceOf[List[NamedConjunctT]])
}
}
Source: [Macros.scala – lines 126‑166](https://github.com/chymyst/curryhoward/blob/master/src/main/scala/io/chymyst/ch/Macros.scala#L126-L166)
The DisjunctT type expression captures the sealed trait name and a list of NamedConjunctT representing each case class alternative.
GADT Support and Type Parameter Substitution
The macro handles Generalized Algebraic Data Types (GADTs) by substituting type parameters when building the DisjunctT. If a subclass has different type parameters than the parent trait, the macro maps them correctly to ensure type safety in the generated code.
From TypeExpr to Generated Scala Code
The transformation from algebraic type representation to executable Scala involves two phases: theorem proving and code emission.
Theorem Prover and Term Synthesis
Once buildTypeExpr constructs the TypeExpr (whether NamedConjunctT or DisjunctT), the system passes it to the theorem prover in TheoremProver.scala. The prover searches for a TermExpr that inhabits the type, treating the algebraic representation as a logical formula to be proved.
emitTermCode and Code Emission
The emitTermCode method in Macros.scala converts the synthesized TermExpr back into Scala AST nodes:
NamedConjunctEgenerates constructor calls likePerson(name, age)DisjunctEgenerates pattern matches with case class extraction
Source: [Macros.scala – emitTermCode](https://github.com/chymyst/curryhoward/blob/master/src/main/scala/io/chymyst/ch/Macros.scala)
Practical Examples
Simple Case Class Extraction
case class Wrap1[A, B](x: Int, a: A, b: B)
def f[A, B]: Wrap1[A, B] ⇒ B = implement
The macro generates:
def f[A, B](w: Wrap1[A, B]): B = w.b
Source: [LJTSpec3.scala – lines 40‑44](https://github.com/chymyst/curryhoward/blob/master/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala#L40-L44)
Sealed Trait Pattern Matching
sealed trait GadtChoice[A, B]
case class GadtChoice1[A, B](x: A, a: A, b: B) extends GadtChoice[A, B]
case class GadtChoice2[B](name: String, bb: B) extends GadtChoice[Boolean, B]
def f[A, B]: GadtChoice[A, B] ⇒ B = implement
Generated code:
def f[A, B](g: GadtChoice[A, B]): B = g match {
case GadtChoice1(_, _, b) => b
case GadtChoice2(_, bb) => bb
}
Source: [LJTSpec3.scala – lines 46‑52](https://github.com/chymyst/curryhoward/blob/master/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala#L46-L52)
Summary
- Type Reflection: The macro in
Macros.scalauses Scala reflection to detect case classes (isCaseClass), case objects (isModuleClass), and sealed traits with case class subclasses. - Algebraic Mapping: Case classes become
NamedConjunctT(product types) while sealed traits becomeDisjunctT(sum types) in the internalTypeExprhierarchy. - GADT Support: The system handles Generalized Algebraic Data Types by substituting type parameters when building
DisjunctTrepresentations. - Code Generation: The theorem prover synthesizes a
TermExprthat inhabits the type, whichemitTermCodeconverts to idiomatic Scala constructor calls or pattern matches. - Edge Cases: Empty case classes and case objects are handled via
UnitTand empty accessor lists, ensuring complete coverage of Scala's algebraic data type syntax.
Frequently Asked Questions
How does curryhoward distinguish between case classes and regular classes?
The macro checks finalTypeSymbol.asClass.isCaseClass to identify case classes specifically. Regular classes without the case modifier are not supported for automatic code generation because they lack the guaranteed accessor methods and stable construction patterns that the theorem prover relies on to build NamedConjunctT representations.
Can curryhoward handle nested case classes or recursive algebraic data types?
Yes, the macro handles recursion through the typesSeenNow parameter in buildTypeExpr, which tracks types currently being processed to avoid infinite loops. Nested case classes are supported because each level is independently translated into TypeExpr structures, allowing the theorem prover to synthesize functions that construct or deconstruct arbitrarily nested products and sums.
What happens if a sealed trait has non-case-class implementations?
The macro explicitly validates that all direct subclasses of a sealed trait are case classes or case objects using subclasses.forall { s => resultClass.isCaseClass || resultClass.isModuleClass }. If this check fails, the macro does not construct a DisjunctT and falls back to treating the type as opaque or raises an error, ensuring that only well-formed algebraic data types participate in automatic code generation.
Does curryhoward support Scala 3's enum types?
The analysis focuses on Scala 2.x macro mechanisms using scala.reflect APIs. While Scala 3 enums compile to similar sealed trait hierarchies, the specific reflection logic in Macros.scala targets Scala 2's isCaseClass and isModuleClass checks. Support for Scala 3 would require adapting the reflection layer to handle the new enum encoding while maintaining the same TypeExpr mapping strategy.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →