How to Inspect or Access Lambda‑Terms from Automatically Generated Code in curryhoward
TLDR: Use the implicit .lambdaTerm extension method on any value produced by ofType, implement, or allOfType, or call TermExpr.lambdaTerm(value) to receive an Option[TermExpr] containing the symbolic representation.
The curryhoward library for Scala synthesizes executable code from type signatures using compile‑time macros. While the output is a standard Scala function, the macro simultaneously constructs a lambda‑term—a syntactic tree representing the logical proof or algorithm. You can extract these terms from the generated code to inspect the underlying structure, perform symbolic substitution, or verify equivalence.
How curryhoward Attaches Lambda‑Terms to Generated Functions
When you invoke a macro such as ofType[T] or implement[T], the expansion logic in src/main/scala/io/chymyst/ch/Macros.scala builds both the executable function and its symbolic TermExpr. The method Macros.returnTerm wraps the compiled code alongside the term using arity‑specific classes.
For functions with up to three arguments, the macro instantiates one of the wrapper classes defined in src/main/scala/io/chymyst/ch/TermExpr.scala:
Function0Lambda[Res]Function1Lambda[Ar, Res]Function2Lambda[Arg1, Arg2, Res]Function3Lambda[Arg1, Arg2, Arg3, Res]
Each wrapper constructor accepts two parameters: the compiled Scala function and the TermExpr representing the lambda‑term. The term is stored in the public lambdaTerm field. For arities greater than three, the macro returns the plain function without a wrapper, and the term is discarded to reduce allocation overhead.
Retrieving Lambda‑Terms from Macro‑Generated Values
The library provides two idiomatic ways to extract the attached term. Both rely on pattern‑matching against the wrapper classes described above.
Using the Implicit .lambdaTerm Extension
The file src/main/scala/io/chymyst/ch/package.scala defines an implicit class WithLambdaTerm. When you import the io.chymyst.ch._ package, any value generated by a curryhoward macro gains the .lambdaTerm method. This method pattern‑matches the underlying wrapper and returns the TermExpr directly.
import io.chymyst.ch._
// Generates a function and its symbolic term
val f: Int => Int = ofType[Int => Int]
// Access the lambda-term
val term: TermExpr = f.lambdaTerm
println(term.prettyPrint) // Output: a ⇒ a
This approach works seamlessly for nullary, unary, binary, and ternary functions. If the value was not produced by a curryhoward macro, the implicit conversion will not apply, resulting in a compile‑time error.
Using TermExpr.lambdaTerm for Optional Extraction
For scenarios where you need a defensive check—such as when receiving a value from Java code or when you are uncertain of the function’s origin—use the static helper in TermExpr. The method TermExpr.lambdaTerm accepts any AnyRef and returns an Option[TermExpr] implemented in src/main/scala/io/chymyst/ch/TermExpr.scala.
import io.chymyst.ch._
val g = implement[Int => Int]
val maybeTerm: Option[TermExpr] = TermExpr.lambdaTerm(g)
maybeTerm.foreach(t => println(t.prettyRenamePrint)) // a ⇒ a
If the input is an ordinary Scala function or a curryhoward function with arity greater than three, the method returns None.
Working with Multi‑Argument Functions
The wrapper mechanism supports functions of up to three arguments. When you generate such functions using allOfType or implement, you can inspect their curried structure.
val f2 = allOfType[(Int, Int) => Int].head // Arity 2
val term2 = f2.lambdaTerm
println(term2.prettyRenamePrint) // e.g., a ⇒ b ⇒ a._1
However, for higher arities, the library intentionally omits the wrapper. Attempting to extract a term from a ten‑argument function yields None:
val f10 = implement[(Int, String, String, String, String, String, String, String, String, String) => Int]
println(TermExpr.lambdaTerm(f10)) // None
Using Lambda‑Terms for Symbolic Reasoning
Once extracted, the TermExpr supports algebraic manipulation. You can substitute type variables, apply the term to arguments, and check equivalence between expressions.
def mapReader[X, A, B] = ofType[(X => A) => (A => B) => (X => B)]
val mapReaderTerm = mapReader.lambdaTerm
val id = ofType[A => A].lambdaTerm
val readerVar = freshVar[X => A]
val substituted = TermExpr.substTypeVar(TP("B"), TP("A"), mapReaderTerm)
assert(substituted(readerVar)(id).equiv(readerVar))
This capability is demonstrated in the test suite LambdaTermsSpec, which verifies that the generated terms behave correctly under substitution and application.
Summary
- Lambda‑terms are automatically constructed by
curryhowardmacros (ofType,implement,allOfType,anyOfType) and attached to the generated function via wrapper classes. - Access the term using the implicit
.lambdaTermextension provided insrc/main/scala/io/chymyst/ch/package.scalafor a direct return, or use TermExpr.lambdaTerm for anOption[TermExpr]. - Wrappers exist only for arities zero through three; higher‑arity functions do not carry extractable terms.
- Extracted terms support pretty‑printing, renaming, substitution, and equivalence checking via the
TermExprAPI.
Frequently Asked Questions
Can I extract a lambda‑term from any Scala function?
No. Only functions produced by the curryhoward macros contain the hidden wrapper class that stores the term. Standard Scala functions or manually written code lack this metadata, so TermExpr.lambdaTerm returns None and the implicit .lambdaTerm method will not compile.
Why does TermExpr.lambdaTerm return None for my generated function?
The macro only attaches lambda‑terms to functions with three or fewer arguments. If you use implement or ofType with a function type that has four or more parameters, the library returns the bare compiled function without a wrapper, making the term inaccessible at runtime.
What can I do with a TermExpr once I have it?
You can inspect its structure using prettyPrint or prettyRenamePrint, perform symbolic substitution with substTypeVar, apply it to other terms using function application syntax, and verify logical equivalence with the equiv method. This is useful for testing that the synthesized code matches a specific proof structure.
Is the .lambdaTerm extension method safe to use in production code?
Yes, provided the value is guaranteed to originate from a curryhoward macro. The pattern match in WithLambdaTerm is exhaustive for the wrapper classes. However, if you pass a non‑wrapped value, you will receive a compile‑time error stating that lambdaTerm is not a member of the type. For defensive programming, prefer the TermExpr.lambdaTerm utility method.
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 →