From eb8c9313694e56912780f907399cf56e7d5bc75c Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Tue, 3 Apr 2018 22:38:38 -0700 Subject: [PATCH 01/11] implement MatchE simplification of nested match --- src/main/scala/io/chymyst/ch/TermExpr.scala | 32 +++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main/scala/io/chymyst/ch/TermExpr.scala b/src/main/scala/io/chymyst/ch/TermExpr.scala index 6829ca4..2e92fab 100644 --- a/src/main/scala/io/chymyst/ch/TermExpr.scala +++ b/src/main/scala/io/chymyst/ch/TermExpr.scala @@ -803,36 +803,52 @@ final case class MatchE(term: TermExpr, cases: List[TermExpr]) extends TermExpr } private[ch] override def simplifyOnceInternal(withEta: Boolean): TermExpr = { - lazy val casesSimplified = cases.map(_.simplifyOnce(withEta)) + val ncases = cases.length term.simplifyOnce(withEta) match { // Match a fixed part of the disjunction; can be simplified to just one clause. // Example: Left(a) match { case Left(x) => f(x); case Right(y) => ... } can be simplified to just f(a). case DisjunctE(index, total, termInjected, _) ⇒ - if (total === cases.length) { + if (total === ncases) { AppE(cases(index).simplifyOnce(withEta), termInjected).simplifyOnce(withEta) - } else throw new Exception(s"Internal error: MatchE with ${cases.length} cases applied to DisjunctE with $total parts, but must be of equal size") + } else throw new Exception(s"Internal error: MatchE with $ncases cases applied to DisjunctE with $total parts, but must be of equal size") // Match of an inner match, can be simplified to a single match. - // Example: (Left(a) match { case Left(x) ⇒ ...; case Right(y) ⇒ ... }) match { case ... ⇒ ... } - // can be simplified to Left(a) match { case Left(x) ⇒ ... match { case ... ⇒ ...}; case Right(y) ⇒ ... match { case ... ⇒ ... } } + // Example: (q match { case Left(x) ⇒ ...; case Right(y) ⇒ ... }) match { case ... ⇒ ... } + // can be simplified to q match { case Left(x) ⇒ ... match { case ... ⇒ ...}; case Right(y) ⇒ ... match { case ... ⇒ ... } } case MatchE(innerTerm, innerCases) ⇒ MatchE(innerTerm, innerCases map { case CurriedE(List(head), body) ⇒ CurriedE(List(head), MatchE(body, cases)) }) + .simplifyOnce(withEta) // Detect the identity patterns: // MatchE(_, List(a ⇒ DisjunctE(0, total, a, _), a ⇒ DisjunctE(1, total, a, _), ...)) // MatchE(_, a: T1 ⇒ DisjunctE(i, total, NamedConjunctE(List(ProjectE(0, a), Project(1, a), ...), T1), ...), _) case termSimplified ⇒ - if (cases.nonEmpty && { + + // Replace redundant matches on the same term, can be simplified by eliminating one match subexpresssion. + // Example: q match { case x ⇒ q match { case y ⇒ b; case other ⇒ ... } ... } + // We already know that q was matched as Left(x). Therefore, we can replace y by x in b and remove the `case other` clause altogether. + + val casesSimplified = cases.zipWithIndex.map { case (c@CurriedE(List(headVar), _), i) ⇒ + TermExpr.substMap(c.simplifyOnce(withEta)) { + case MatchE(otherTerm, otherCases) if otherTerm === termSimplified ⇒ + // We already matched `otherTerm`, and we are now in case `c`, which is `case x ⇒ ...`. + // Therefore we can discard any of the `otherCases` except the one corresponding to `c`. + // We can replace the `q match { case y ⇒ b; ...}` by `b` after replacing `x` by `y` in `b`. + val remainingCase = otherCases(i) + AppE(remainingCase, headVar).simplifyOnce(withEta) + } + } + if (casesSimplified.nonEmpty && { casesSimplified.zipWithIndex.forall { // Detect a ⇒ a pattern case (CurriedE(List(head@VarE(_, _)), body@VarE(_, _)), _) if head.name === body.name ⇒ true case (CurriedE(List(head@VarE(_, _)), DisjunctE(i, len, x, _)), ind) - if x === head && len === cases.length && ind === i + if x === head && len === ncases && ind === i ⇒ true case (CurriedE(List(head@VarE(_, headT)), DisjunctE(i, len, NamedConjunctE(projectionTerms, conjT), _)), ind) ⇒ - len === cases.length && ind === i && headT === conjT && + len === ncases && ind === i && headT === conjT && projectionTerms.zipWithIndex.forall { case (ProjectE(k, head1), j) if k === j && head1 === head ⇒ true case _ ⇒ false From a43f1531642936538e2291465d3b1ea69e3c22ad Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Tue, 3 Apr 2018 23:08:58 -0700 Subject: [PATCH 02/11] reintroduce simplify/distinct into theorem prover --- src/main/scala/io/chymyst/ch/TermExpr.scala | 4 +++- src/main/scala/io/chymyst/ch/TheoremProver.scala | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/scala/io/chymyst/ch/TermExpr.scala b/src/main/scala/io/chymyst/ch/TermExpr.scala index 2e92fab..8e59530 100644 --- a/src/main/scala/io/chymyst/ch/TermExpr.scala +++ b/src/main/scala/io/chymyst/ch/TermExpr.scala @@ -835,7 +835,9 @@ final case class MatchE(term: TermExpr, cases: List[TermExpr]) extends TermExpr // Therefore we can discard any of the `otherCases` except the one corresponding to `c`. // We can replace the `q match { case y ⇒ b; ...}` by `b` after replacing `x` by `y` in `b`. val remainingCase = otherCases(i) - AppE(remainingCase, headVar).simplifyOnce(withEta) + val result = AppE(remainingCase, headVar).simplifyOnce(withEta) + println(s"DEBUG: replacing ${MatchE(otherTerm, otherCases)} by $result in ${c.simplifyOnce(withEta)}") + result } } if (casesSimplified.nonEmpty && { diff --git a/src/main/scala/io/chymyst/ch/TheoremProver.scala b/src/main/scala/io/chymyst/ch/TheoremProver.scala index 6d7e53a..5317f20 100644 --- a/src/main/scala/io/chymyst/ch/TheoremProver.scala +++ b/src/main/scala/io/chymyst/ch/TheoremProver.scala @@ -93,9 +93,10 @@ object TheoremProver { val transformedProofs = explodedNewProofs.map(ruleResult.backTransform) val t1 = System.currentTimeMillis() - val result = transformedProofs.sortBy(_.informationLossScore).take(maxTermsToSelect(sequent)) + val result = transformedProofs.map(_.simplifyOnce(withEta = false)).distinct.sortBy(_.informationLossScore).take(maxTermsToSelect(sequent)) // Note: at this point, it is a mistake to do prettyRename, because we are calling this function recursively. // We will call prettyRename() at the very end of the proof search. + // It is also a mistake to do a `.simplifyOnce(withEta = true)`. The eta-conversion produces incorrect code here. if (debug) { println(s"DEBUG: elapsed ${System.currentTimeMillis() - t0} ms, .map(_.simplify()).distinct took ${System.currentTimeMillis() - t1} ms, produced ${result.size} terms out of ${transformedProofs.size} back-transformed terms; after rule ${ruleResult.ruleName} for sequent $sequent") // println(s"DEBUG: for sequent $sequent, after rule ${ruleResult.ruleName}, transformed ${transformedProofs.length} proof terms:\n ${transformedProofs.mkString(" ;\n ")} ,\nafter simplifying:\n ${result.mkString(" ;\n ")} .") From fa2cd8c89711b71358913e8375f1c4705c730e81 Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Tue, 3 Apr 2018 23:12:19 -0700 Subject: [PATCH 03/11] bump version --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 424c652..4dd65ac 100644 --- a/build.sbt +++ b/build.sbt @@ -103,7 +103,7 @@ lazy val curryhoward: Project = (project in file(".")) .settings(common) .settings( organization := "io.chymyst", - version := "0.3.7", + version := "0.3.8", licenses := Seq("Apache License, Version 2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")), homepage := Some(url("https://github.com/Chymyst/curryhoward")), From 820a4b97dc76e2b9ddd67c2b0c75f5af9721fdfa Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Wed, 4 Apr 2018 00:33:28 -0700 Subject: [PATCH 04/11] generate variables for disjunctions --- docs/Tutorial.md | 4 +-- src/main/scala/io/chymyst/ch/TermExpr.scala | 12 +++++++ src/main/tut/Tutorial.md | 4 +-- .../io/chymyst/ch/unit/TermExprSpec.scala | 31 +++++++++++++++---- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 34706e2..4d28e93 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -888,8 +888,8 @@ res26: Boolean = true | `a.substTypeVar(b, c)` | `TermExpr ⇒ (TermExpr, TermExpr) ⇒ TermExpr` | replace a type variable in `a`; the type variable is specified as the type of `b`, and the replacement type is specified as the type of `c` | | `a.substTypeVars(s)` | `TermExpr ⇒ Map[TP, TypeExpr] ⇒ TermExpr` | replace all type variables in `a` according to the given substitution map `s` -- all type variables are substituted at once | | `u()` | `TermExpr ⇒ () ⇒ TermExpr` and `TypeExpr ⇒ () ⇒ TermExpr` | create a "named Unit" term of type `u.t` -- the type of `u` must be a named unit type, e.g. `None.type` or a case class with no constructors | -| `c(x...)` | `TermExpr ⇒ TermExpr* ⇒ TermExpr` and `TypeExpr ⇒ TermExpr* ⇒ TermExpr` | create a named conjunction term of type `c.t` -- the type of `c` must be a conjunction whose parts match the types of the arguments `x...` | -| `d(x)` | `TermExpr ⇒ TermExpr ⇒ TermExpr` and `TypeExpr ⇒ TermExpr ⇒ TermExpr` | create a disjunction term of type `d.t` using term `x` -- the type of `x` must match one of the disjunction parts in the type `d`, which must be a disjunction type | +| `c(x...)` | `TypeExpr ⇒ TermExpr* ⇒ TermExpr` and `TypeExpr ⇒ TermExpr* ⇒ TermExpr` | create a named conjunction term of type `c` -- the type `c` must be a conjunction whose parts match the types of the arguments `x...` | +| `d(x)` | `TypeExpr ⇒ TermExpr ⇒ TermExpr` and `TypeExpr ⇒ TermExpr ⇒ TermExpr` | create a disjunction term of type `d` using term `x` -- the type of `x` must match one of the disjunction parts in the type `d`, which must be a disjunction type | | `c(i)` | `TermExpr ⇒ Int ⇒ TermExpr` | project a conjunction term onto part with given zero-based index -- the type of `c` must be a conjunction with sufficiently many parts | | `c("id")` | `TermExpr ⇒ String ⇒ TermExpr` | project a conjunction term onto part with given accessor name -- the type of `c` must be a named conjunction that supports this accessor | | `d.cases(x =>: ..., y =>: ..., ...)` | `TermExpr ⇒ TermExpr* ⇒ TermExpr` | create a term that pattern-matches on the given disjunction term -- the type of `d` must be a disjunction whose arguments match the arguments `x`, `y`, ... of the given case clauses | diff --git a/src/main/scala/io/chymyst/ch/TermExpr.scala b/src/main/scala/io/chymyst/ch/TermExpr.scala index 8e59530..85f024e 100644 --- a/src/main/scala/io/chymyst/ch/TermExpr.scala +++ b/src/main/scala/io/chymyst/ch/TermExpr.scala @@ -342,6 +342,18 @@ object TermExpr { } private[ch] def roundFactor(x: Double): Int = math.round(x * 10000).toInt + + /** Generate all necessary free variables for equality checking of functions that consume disjunction types. + * + * @param typeExpr The type of the argument expression. + * @return A sequence of [[TermExpr]] values containing the necessary free variables. + */ + def subtypeVars(typeExpr: TypeExpr): Seq[TermExpr] = typeExpr match { + case dt@DisjunctT(constructor, typeParams, terms) ⇒ terms.zipWithIndex.flatMap { case (t, i) ⇒ subtypeVars(t).map(v ⇒ DisjunctE(i, terms.length, v, dt)) } + case nct@NamedConjunctT(constructor, typeParams, accessors, wrapped) ⇒ + TheoremProver.explode(wrapped.map(subtypeVars)).map(NamedConjunctE(_, nct)) + case _ ⇒ Seq(VarE(freshIdents(), typeExpr)) + } } sealed trait TermExpr { diff --git a/src/main/tut/Tutorial.md b/src/main/tut/Tutorial.md index 9185776..1a37837 100644 --- a/src/main/tut/Tutorial.md +++ b/src/main/tut/Tutorial.md @@ -725,8 +725,8 @@ getIdAutoTerm equiv getId.prettyRename | `a.substTypeVar(b, c)` | `TermExpr ⇒ (TermExpr, TermExpr) ⇒ TermExpr` | replace a type variable in `a`; the type variable is specified as the type of `b`, and the replacement type is specified as the type of `c` | | `a.substTypeVars(s)` | `TermExpr ⇒ Map[TP, TypeExpr] ⇒ TermExpr` | replace all type variables in `a` according to the given substitution map `s` -- all type variables are substituted at once | | `u()` | `TermExpr ⇒ () ⇒ TermExpr` and `TypeExpr ⇒ () ⇒ TermExpr` | create a "named Unit" term of type `u.t` -- the type of `u` must be a named unit type, e.g. `None.type` or a case class with no constructors | -| `c(x...)` | `TermExpr ⇒ TermExpr* ⇒ TermExpr` and `TypeExpr ⇒ TermExpr* ⇒ TermExpr` | create a named conjunction term of type `c.t` -- the type of `c` must be a conjunction whose parts match the types of the arguments `x...` | -| `d(x)` | `TermExpr ⇒ TermExpr ⇒ TermExpr` and `TypeExpr ⇒ TermExpr ⇒ TermExpr` | create a disjunction term of type `d.t` using term `x` -- the type of `x` must match one of the disjunction parts in the type `d`, which must be a disjunction type | +| `c(x...)` | `TypeExpr ⇒ TermExpr* ⇒ TermExpr` and `TypeExpr ⇒ TermExpr* ⇒ TermExpr` | create a named conjunction term of type `c` -- the type `c` must be a conjunction whose parts match the types of the arguments `x...` | +| `d(x)` | `TypeExpr ⇒ TermExpr ⇒ TermExpr` and `TypeExpr ⇒ TermExpr ⇒ TermExpr` | create a disjunction term of type `d` using term `x` -- the type of `x` must match one of the disjunction parts in the type `d`, which must be a disjunction type | | `c(i)` | `TermExpr ⇒ Int ⇒ TermExpr` | project a conjunction term onto part with given zero-based index -- the type of `c` must be a conjunction with sufficiently many parts | | `c("id")` | `TermExpr ⇒ String ⇒ TermExpr` | project a conjunction term onto part with given accessor name -- the type of `c` must be a named conjunction that supports this accessor | | `d.cases(x =>: ..., y =>: ..., ...)` | `TermExpr ⇒ TermExpr* ⇒ TermExpr` | create a term that pattern-matches on the given disjunction term -- the type of `d` must be a disjunction whose arguments match the arguments `x`, `y`, ... of the given case clauses | diff --git a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala index f3239a1..33fe584 100644 --- a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala +++ b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala @@ -15,8 +15,29 @@ class TermExprSpec extends FlatSpec with Matchers { behavior of "TermExpr miscellaneous methods" + it should "generate variables for disjunction subtypes" in { + val p = freshVar[Either[Option[Option[(Int, Int)]], Option[(Option[Int], Option[Int])]]] + + val subtypeVars = TermExpr.subtypeVars(p.t).map(_.prettyPrint) + + val indices = "z([0-9]+)".r.findAllMatchIn(subtypeVars.mkString("")).map(_.group(1).toInt).toList + indices.map(_ - indices.min) shouldEqual Seq(0, 1, 3, 2, 2, 3) + + subtypeVars.map(_.replaceAll("z[0-9]+", "z")) shouldEqual List( + "(Left((None() + 0)) + 0)", + "(Left((0 + Some((None() + 0)))) + 0)", + "(Left((0 + Some((0 + Some(Tuple2(z, z)))))) + 0)", + "(0 + Right((None() + 0)))", "(0 + Right((0 + Some(Tuple2((None() + 0), (None() + 0))))))", + "(0 + Right((0 + Some(Tuple2((None() + 0), (0 + Some(z)))))))", + "(0 + Right((0 + Some(Tuple2((0 + Some(z)), (None() + 0))))))", + "(0 + Right((0 + Some(Tuple2((0 + Some(z)), (0 + Some(z)))))))" + ) + + } + it should "compute identity function" in { def idAB[A, B] = TermExpr.id(typeExpr[A ⇒ B]) + idAB.prettyPrint shouldEqual "x ⇒ x" idAB.toString shouldEqual "\\((x:A ⇒ B) ⇒ x)" idAB.t.prettyPrint shouldEqual "(A ⇒ B) ⇒ A ⇒ B" @@ -274,9 +295,9 @@ a ⇒ Tuple2(a._2._2, a._2._2) // Choose second element of second inner tuple. // println(flattens.size) - def f[A] = allOfType[Option[(A, A, A)] ⇒ Option[(A, A, A)]]() + def f[A] = anyOfType[Option[(A, A, A)] ⇒ Option[(A, A, A)]]() - println(f.size) + f.size shouldEqual 28 // f[Int].map(_.lambdaTerm.prettyPrint).sorted.foreach(println) // f.size shouldEqual factorial(4) } @@ -284,9 +305,7 @@ a ⇒ Tuple2(a._2._2, a._2._2) // Choose second element of second inner tuple. it should "generate match clauses" in { def f[A] = anyOfType[Option[Option[A]] ⇒ Option[Option[A]]]().map(_.lambdaTerm) - println(f.size) - f.map(_.prettyPrint).foreach(println) - + f.size shouldEqual 13 + // f.map(_.prettyPrint).foreach(println) } - } From 72670af9806de8ae329bf21bc7a2c304aa3ad3be Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Wed, 4 Apr 2018 01:02:56 -0700 Subject: [PATCH 05/11] implement subtype fresh vars checking --- src/main/scala/io/chymyst/ch/TermExpr.scala | 20 +++++++++++++++++-- ...ecking.scala => SymbolicLawChecking.scala} | 6 +++--- .../scala/io/chymyst/ch/unit/LJTSpec4.scala | 2 +- .../io/chymyst/ch/unit/LambdaTermsSpec.scala | 8 ++++---- .../io/chymyst/ch/unit/TermExprSpec.scala | 3 +++ 5 files changed, 29 insertions(+), 10 deletions(-) rename src/main/scala/io/chymyst/ch/data/{LawChecking.scala => SymbolicLawChecking.scala} (89%) diff --git a/src/main/scala/io/chymyst/ch/TermExpr.scala b/src/main/scala/io/chymyst/ch/TermExpr.scala index 85f024e..72adfe2 100644 --- a/src/main/scala/io/chymyst/ch/TermExpr.scala +++ b/src/main/scala/io/chymyst/ch/TermExpr.scala @@ -343,10 +343,10 @@ object TermExpr { private[ch] def roundFactor(x: Double): Int = math.round(x * 10000).toInt - /** Generate all necessary free variables for equality checking of functions that consume disjunction types. + /** Generate all necessary fresh variables for equality checking of functions that consume disjunction types. * * @param typeExpr The type of the argument expression. - * @return A sequence of [[TermExpr]] values containing the necessary free variables. + * @return A sequence of [[TermExpr]] values containing the necessary fresh variables. */ def subtypeVars(typeExpr: TypeExpr): Seq[TermExpr] = typeExpr match { case dt@DisjunctT(constructor, typeParams, terms) ⇒ terms.zipWithIndex.flatMap { case (t, i) ⇒ subtypeVars(t).map(v ⇒ DisjunctE(i, terms.length, v, dt)) } @@ -354,6 +354,22 @@ object TermExpr { TheoremProver.explode(wrapped.map(subtypeVars)).map(NamedConjunctE(_, nct)) case _ ⇒ Seq(VarE(freshIdents(), typeExpr)) } + + /** Extensional equality check. If the term expressions are functions, fresh variables are substituted as arguments and the results are compared with `equiv`. + * + * @param termExpr1 The first term. + * @param termExpr2 The second term. + * @return `true` if the terms are extensionally equal. + */ + def extEquals(termExpr1: TermExpr, termExpr2: TermExpr): Boolean = { + val t1 = termExpr1.simplify + val t2 = termExpr2.simplify + (t1.t === t2.t) && ((t1 equiv t2) || ((t1, t2) match { + case (CurriedE(h1 :: _, _), CurriedE(_ :: _, _)) ⇒ + subtypeVars(h1.t).forall(term ⇒ extEquals(t1(term), t2(term))) + case _ ⇒ false + })) + } } sealed trait TermExpr { diff --git a/src/main/scala/io/chymyst/ch/data/LawChecking.scala b/src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala similarity index 89% rename from src/main/scala/io/chymyst/ch/data/LawChecking.scala rename to src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala index 18fc221..f1a655b 100644 --- a/src/main/scala/io/chymyst/ch/data/LawChecking.scala +++ b/src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala @@ -2,14 +2,14 @@ package io.chymyst.ch.data import io.chymyst.ch._ -object LawChecking { +object SymbolicLawChecking { def checkFlattenAssociativity(fmap: TermExpr, flatten: TermExpr): Boolean = { // fmap ftn . ftn = ftn . ftn val lhs = flatten :@@ flatten val rhs = (fmap :@ flatten) :@@ flatten // println(s"check associativity laws for flatten = ${flatten.prettyPrint}:\n\tlhs = ${lhs.simplify.prettyRenamePrint}\n\trhs = ${rhs.simplify.prettyRenamePrint}") - lhs equiv rhs + TermExpr.extEquals(lhs, rhs) } def checkPureFlattenLaws(fmap: TermExpr, pure: TermExpr, flatten: TermExpr): Boolean = { @@ -23,7 +23,7 @@ object LawChecking { val fpf = (fmap :@ pure) :@@ flatten // println(s"check identity laws for pure = ${pure.prettyPrint} and flatten = ${flatten.prettyPrint}:\n\tlhs1 = ${pf.simplify.prettyPrint}\n\trhs1 = ${idFA.simplify.prettyPrint}\n\tlhs2 = ${fpf.simplify.prettyPrint}\n\trhs2 = ${idFA.simplify.prettyPrint}") - (pf equiv idFA) && (fpf equiv idFA) + TermExpr.extEquals(pf, idFA) && TermExpr.extEquals(fpf, idFA) } } diff --git a/src/test/scala/io/chymyst/ch/unit/LJTSpec4.scala b/src/test/scala/io/chymyst/ch/unit/LJTSpec4.scala index 0e2edc5..6fa78c2 100644 --- a/src/test/scala/io/chymyst/ch/unit/LJTSpec4.scala +++ b/src/test/scala/io/chymyst/ch/unit/LJTSpec4.scala @@ -1,7 +1,7 @@ package io.chymyst.ch.unit import io.chymyst.ch._ -import io.chymyst.ch.data.{LawChecking => LC} +import io.chymyst.ch.data.{SymbolicLawChecking => LC} import org.scalatest.{FlatSpec, Matchers} class LJTSpec4 extends FlatSpec with Matchers { diff --git a/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala b/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala index e5aa48e..1e9953d 100644 --- a/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala +++ b/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala @@ -1,7 +1,7 @@ package io.chymyst.ch.unit import io.chymyst.ch._ -import io.chymyst.ch.data.{LawChecking => LC} +import io.chymyst.ch.data.{SymbolicLawChecking => LC} import org.scalatest.{Assertion, FlatSpec, Matchers} class LambdaTermsSpec extends FlatSpec with Matchers { @@ -658,7 +658,7 @@ class LambdaTermsSpec extends FlatSpec with Matchers { println("Good monads:") println(goodMonads.map { case (pure, ftn) ⇒ s"pure = ${pure.prettyPrint}, flatten = ${ftn.prettyPrint}" }.mkString("\n")) - goodSemimonads.size shouldEqual 2 + goodSemimonads.size shouldEqual 3 goodMonads.size shouldEqual 1 } @@ -718,7 +718,7 @@ class LambdaTermsSpec extends FlatSpec with Matchers { println("Good monads:") println(goodMonads.map { case (pure, ftn) ⇒ s"pure = ${pure.prettyPrint}, flatten = ${ftn.prettyPrint}" }.mkString("\n")) - goodSemimonads.size shouldEqual 13 + goodSemimonads.size shouldEqual 19 goodMonads.size shouldEqual 6 } @@ -758,7 +758,7 @@ class LambdaTermsSpec extends FlatSpec with Matchers { println("Good monads:") println(goodMonads.map { case (pure, ftn) ⇒ s"pure = ${pure.prettyPrint}, flatten = ${ftn.prettyPrint}" }.mkString("\n")) - goodSemimonads.size shouldEqual 2 + goodSemimonads.size shouldEqual 3 goodMonads.size shouldEqual 1 } } diff --git a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala index 33fe584..5fc9888 100644 --- a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala +++ b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala @@ -32,7 +32,10 @@ class TermExprSpec extends FlatSpec with Matchers { "(0 + Right((0 + Some(Tuple2((0 + Some(z)), (None() + 0))))))", "(0 + Right((0 + Some(Tuple2((0 + Some(z)), (0 + Some(z)))))))" ) + } + it should "compute extensional equality of functions" in { + TermExpr.extEquals(TermExpr.id(typeExpr[Int]), TermExpr.id(typeExpr[Int])) shouldEqual true } it should "compute identity function" in { From b22f039f35c4d467499ea7ae901a0ab87f63a13b Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Wed, 4 Apr 2018 14:38:32 -0700 Subject: [PATCH 06/11] WIP fixing the bugs with subst and extensional equality --- src/main/scala/io/chymyst/ch/TermExpr.scala | 40 +++++--- .../chymyst/ch/data/SymbolicLawChecking.scala | 4 +- .../io/chymyst/ch/unit/LambdaTermsSpec.scala | 93 ++++++++++++++++++- .../io/chymyst/ch/unit/TermExprSpec.scala | 4 +- 4 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/main/scala/io/chymyst/ch/TermExpr.scala b/src/main/scala/io/chymyst/ch/TermExpr.scala index 72adfe2..56f1cea 100644 --- a/src/main/scala/io/chymyst/ch/TermExpr.scala +++ b/src/main/scala/io/chymyst/ch/TermExpr.scala @@ -145,7 +145,7 @@ object TermExpr { thisVar.name === otherVar.name && (thisVar.t === otherVar.t || TypeExpr.isDisjunctionPart(thisVar.t, otherVar.t)) } - /** Replace all non-free occurrences of variable `replaceVar` by expression `byExpr` in `origExpr`. + /** Replace all free occurrences of variable `replaceVar` by expression `byExpr` in `origExpr`. * * @param replaceVar A variable that may occur freely in `origExpr`. * @param byExpr A new expression to replace all free occurrences of that variable. @@ -156,7 +156,7 @@ object TermExpr { // Check that all instances of replaceVar in origExpr have the correct type. val badVars = origExpr.freeVars.filter(_.name === replaceVar.name).filterNot(varMatchesType(_, replaceVar)) if (badVars.nonEmpty) { - throw new Exception(s"In subst($replaceVar, $byExpr, $origExpr), found variable(s) ${badVars.map(v ⇒ s"(${v.name}:${v.t.prettyPrint})").mkString(", ")} with incorrect type(s), expected variable type ${replaceVar.t.prettyPrint}") + throw new Exception(s"In subst($replaceVar:${replaceVar.t.prettyPrint}, $byExpr, $origExpr), found variable(s) ${badVars.map(v ⇒ s"(${v.name}:${v.t.prettyPrint})").mkString(", ")} with incorrect type(s), expected variable type ${replaceVar.t.prettyPrint}") } // Do we need an alpha-conversion? Better be safe than sorry. val (convertedReplaceVar, convertedOrigExpr) = if (byExpr.usedVarNames contains replaceVar.name) { @@ -167,7 +167,7 @@ object TermExpr { substMap(convertedOrigExpr) { case c@CurriedE(heads, _) if heads.exists(_.name === convertedReplaceVar.name) ⇒ c - // If a variable from `heads` collides with `convertedReplaceVar`, we do not replace anything in the body. + // If a variable from `heads` collides with `convertedReplaceVar`, we do not replace anything in the body because the variable occurs as non-free. case v@VarE(_, _) if varMatchesType(v, convertedReplaceVar) ⇒ byExpr } @@ -361,14 +361,23 @@ object TermExpr { * @param termExpr2 The second term. * @return `true` if the terms are extensionally equal. */ - def extEquals(termExpr1: TermExpr, termExpr2: TermExpr): Boolean = { + def extEqual(termExpr1: TermExpr, termExpr2: TermExpr): Boolean = { val t1 = termExpr1.simplify val t2 = termExpr2.simplify - (t1.t === t2.t) && ((t1 equiv t2) || ((t1, t2) match { - case (CurriedE(h1 :: _, _), CurriedE(_ :: _, _)) ⇒ - subtypeVars(h1.t).forall(term ⇒ extEquals(t1(term), t2(term))) - case _ ⇒ false - })) + (t1.t === t2.t) && ( + (t1 equiv t2) || { + println(s"DEBUG: checking extensional equality of ${t1.prettyPrint} and ${t2.prettyPrint}") + (t1, t2) match { + case (CurriedE(h1 :: _, _), CurriedE(_ :: _, _)) ⇒ + subtypeVars(h1.t).forall { term ⇒ + val result = extEqual(t1(term), t2(term)) + if (!result) println(s"DEBUG: found inequality after substituting term ${term.prettyPrint}") + result + } + case _ ⇒ false + } + } + ) } } @@ -540,7 +549,7 @@ sealed trait TermExpr { letter ← ('a' to 'z').toIterator } yield s"$letter$number" - private lazy val renameBoundVars: TermExpr = TermExpr.substMap(this) { + private[ch] lazy val renameBoundVars: TermExpr = TermExpr.substMap(this) { case CurriedE(heads, body) ⇒ val oldAndNewVars = heads.map { v ⇒ (v, VarE(TermExpr.freshIdents(), v.t)) } val renamedBody = oldAndNewVars.foldLeft(body.renameBoundVars) { case (prev, (oldVar, newVar)) ⇒ @@ -855,19 +864,22 @@ final case class MatchE(term: TermExpr, cases: List[TermExpr]) extends TermExpr // Replace redundant matches on the same term, can be simplified by eliminating one match subexpresssion. // Example: q match { case x ⇒ q match { case y ⇒ b; case other ⇒ ... } ... } // We already know that q was matched as Left(x). Therefore, we can replace y by x in b and remove the `case other` clause altogether. - - val casesSimplified = cases.zipWithIndex.map { case (c@CurriedE(List(headVar), _), i) ⇒ - TermExpr.substMap(c.simplifyOnce(withEta)) { + // Doing a .renameBoundVars on the cases leads to infinite loops somewhere due to incorrect alpha-conversion. + val casesSimplified = cases.map(_.simplifyOnce(withEta)) + /* + .zipWithIndex.map { case (c@CurriedE(List(headVar), _), i) ⇒ + TermExpr.substMap(c) { case MatchE(otherTerm, otherCases) if otherTerm === termSimplified ⇒ // We already matched `otherTerm`, and we are now in case `c`, which is `case x ⇒ ...`. // Therefore we can discard any of the `otherCases` except the one corresponding to `c`. // We can replace the `q match { case y ⇒ b; ...}` by `b` after replacing `x` by `y` in `b`. val remainingCase = otherCases(i) val result = AppE(remainingCase, headVar).simplifyOnce(withEta) - println(s"DEBUG: replacing ${MatchE(otherTerm, otherCases)} by $result in ${c.simplifyOnce(withEta)}") + // println(s"DEBUG: replacing ${MatchE(otherTerm, otherCases)} by $result in ${c.simplifyOnce(withEta)}") result } } + */ if (casesSimplified.nonEmpty && { casesSimplified.zipWithIndex.forall { // Detect a ⇒ a pattern diff --git a/src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala b/src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala index f1a655b..6949d33 100644 --- a/src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala +++ b/src/main/scala/io/chymyst/ch/data/SymbolicLawChecking.scala @@ -9,7 +9,7 @@ object SymbolicLawChecking { val lhs = flatten :@@ flatten val rhs = (fmap :@ flatten) :@@ flatten // println(s"check associativity laws for flatten = ${flatten.prettyPrint}:\n\tlhs = ${lhs.simplify.prettyRenamePrint}\n\trhs = ${rhs.simplify.prettyRenamePrint}") - TermExpr.extEquals(lhs, rhs) + TermExpr.extEqual(lhs, rhs) } def checkPureFlattenLaws(fmap: TermExpr, pure: TermExpr, flatten: TermExpr): Boolean = { @@ -23,7 +23,7 @@ object SymbolicLawChecking { val fpf = (fmap :@ pure) :@@ flatten // println(s"check identity laws for pure = ${pure.prettyPrint} and flatten = ${flatten.prettyPrint}:\n\tlhs1 = ${pf.simplify.prettyPrint}\n\trhs1 = ${idFA.simplify.prettyPrint}\n\tlhs2 = ${fpf.simplify.prettyPrint}\n\trhs2 = ${idFA.simplify.prettyPrint}") - TermExpr.extEquals(pf, idFA) && TermExpr.extEquals(fpf, idFA) + TermExpr.extEqual(pf, idFA) && TermExpr.extEqual(fpf, idFA) } } diff --git a/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala b/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala index 1e9953d..ebef4ac 100644 --- a/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala +++ b/src/test/scala/io/chymyst/ch/unit/LambdaTermsSpec.scala @@ -602,7 +602,7 @@ class LambdaTermsSpec extends FlatSpec with Matchers { // Compute flatten terms from flm terms val ftnTerms = flmTerms.map(flm ⇒ (flm :@ (px =>: px)).simplify) - if (debug) println(s"flatten terms: ${ftnTerms.map(_.prettyPrint)}") + if (debug) println(s"flatten terms (type ${ftnTerms.head.t.prettyPrint}):\n\t${ftnTerms.map(_.prettyPrint).mkString("\n\t")}") val pureTerms = TheoremProver.findProofs(pureVar.t)._2 if (debug) println(s"pure terms: ${pureTerms.map(_.prettyPrint)}") @@ -671,7 +671,7 @@ class LambdaTermsSpec extends FlatSpec with Matchers { def pure[A] = freshVar[A ⇒ P[A]] - val (goodSemimonads, goodMonads) = semimonadsAndMonads(fmapTerm, pure, flm) + val (goodSemimonads, goodMonads) = semimonadsAndMonads(fmapTerm, pure, flm, debug = true) println(s"Good semimonads (flatten):\n${goodSemimonads.map(_.prettyPrint).mkString("\n")}") @@ -742,6 +742,26 @@ class LambdaTermsSpec extends FlatSpec with Matchers { goodMonads.size shouldEqual 2 } + it should "check A x A + A x A x A monad" in { + type P[A] = Either[(A, A), (A, A, A)] + + def fmapTerm[A, B] = ofType[(A ⇒ B) ⇒ P[A] ⇒ P[B]].lambdaTerm + + def flm[A, B] = freshVar[(A ⇒ P[B]) ⇒ P[A] ⇒ P[B]] + + def pure[A] = freshVar[A ⇒ P[A]] + + val (goodSemimonads, goodMonads) = semimonadsAndMonads(fmapTerm, pure, flm) + + println(s"Good semimonads (flatten):\n${goodSemimonads.map(_.prettyPrint).mkString("\n")}") + + println("Good monads:") + println(goodMonads.map { case (pure, ftn) ⇒ s"pure = ${pure.prettyPrint}, flatten = ${ftn.prettyPrint}" }.mkString("\n")) + + goodSemimonads.size shouldEqual 12 + goodMonads.size shouldEqual 2 + } + it should "check A + (1 ⇒ A) monad" in { type P[A] = Either[A, Unit ⇒ A] @@ -761,4 +781,73 @@ class LambdaTermsSpec extends FlatSpec with Matchers { goodSemimonads.size shouldEqual 3 goodMonads.size shouldEqual 1 } + + it should "check the 1 + A x A monad by hand" in { + type P[A] = Option[(A, A)] + + def fmap[A, B] = ofType[(A ⇒ B) ⇒ P[A] ⇒ P[B]].lambdaTerm + + fmap.prettyPrint shouldEqual "a ⇒ b ⇒ b match { c ⇒ (None() + 0); d ⇒ (0 + Some(Tuple2(a d.value._1, a d.value._2))) }" + + def pure[A] = ofType[A ⇒ P[A]].lambdaTerm + + pure.prettyPrint shouldEqual "a ⇒ (0 + Some(Tuple2(a, a)))" + + // Construct flatten by hand. + def ppa[A] = freshVar[P[P[A]]] + + def pa[A] = freshVar[P[A]] + + val none = freshVar[None.type] + val ppaNone = ppa(none) + val paNone = pa(none) + ppaNone.t.prettyPrint shouldEqual "Option[Tuple2[Option[Tuple2[A,A]],Option[Tuple2[A,A]]]]" + ppa.t.prettyPrint shouldEqual ppaNone.t.prettyPrint + + def tuplePa[A] = freshVar[Some[(P[A], P[A])]] + + def tupleA0[A] = freshVar[Some[(A, A)]] + + def tupleA1[A] = freshVar[Some[(A, A)]] + + def tupleA2[A] = freshVar[Some[(A, A)]] + + val ftn = ( + ppa =>: ppa.cases( + none =>: paNone, + tuplePa =>: tuplePa("value")(0).cases( + none =>: paNone, + tupleA0 =>: tuplePa("value")(1).cases( + none =>: paNone, + tupleA1 =>: + pa(tupleA0(tupleA0("value").t(tupleA0("value")(0), tupleA1("value")(1)))) + ) + ) + ) + ).prettyRename + ftn.t.prettyPrint shouldEqual "Option[Tuple2[Option[Tuple2[A,A]],Option[Tuple2[A,A]]]] ⇒ Option[Tuple2[A,A]]" + ftn.prettyPrint shouldEqual "a ⇒ a match { b ⇒ (b + 0); c ⇒ c.value._1 match { d ⇒ (d + 0); e ⇒ c.value._2 match { f ⇒ (f + 0); g ⇒ (0 + Some(Tuple2(e.value._1, g.value._2))) } } }" + // Fails due to violating laws. + LC.checkPureFlattenLaws(fmap, pure, ftn) shouldEqual true + LC.checkFlattenAssociativity(fmap, ftn) shouldEqual false + + val ftn2 = ( + ppa =>: ppa.cases( + none =>: paNone, + tuplePa =>: tuplePa("value")(0).cases( + none =>: tuplePa("value")(1), + tupleA0 =>: tuplePa("value")(1).cases( + none =>: tuplePa("value")(0), + tupleA1 =>: + pa(tupleA0(tupleA0("value").t(tupleA0("value")(0), tupleA1("value")(1)))) + ) + ) + ) + ).simplify.prettyRename + ftn2.t.prettyPrint shouldEqual "Option[Tuple2[Option[Tuple2[A,A]],Option[Tuple2[A,A]]]] ⇒ Option[Tuple2[A,A]]" + ftn2.prettyPrint shouldEqual "a ⇒ a match { b ⇒ (b + 0); c ⇒ c.value._1 match { d ⇒ c.value._2; e ⇒ c.value._2 match { f ⇒ c.value._1; g ⇒ (0 + Some(Tuple2(e.value._1, g.value._2))) } } }" + LC.checkPureFlattenLaws(fmap, pure, ftn2) shouldEqual true + LC.checkFlattenAssociativity(fmap, ftn2) shouldEqual false + } + } diff --git a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala index 5fc9888..548388a 100644 --- a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala +++ b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala @@ -35,7 +35,7 @@ class TermExprSpec extends FlatSpec with Matchers { } it should "compute extensional equality of functions" in { - TermExpr.extEquals(TermExpr.id(typeExpr[Int]), TermExpr.id(typeExpr[Int])) shouldEqual true + TermExpr.extEqual(TermExpr.id(typeExpr[Int]), TermExpr.id(typeExpr[Int])) shouldEqual true } it should "compute identity function" in { @@ -65,7 +65,7 @@ class TermExprSpec extends FlatSpec with Matchers { the[Exception] thrownBy { TermExpr.subst(var23, termExpr1, var32 =>: var23.copy(t = TP("1"))) shouldEqual (var32 =>: termExpr1) - } should have message "In subst(x2, \\((x2:3) ⇒ (x3:2) ⇒ (x4:1) ⇒ x3), \\((x3:2) ⇒ x2)), found variable(s) (x2:1) with incorrect type(s), expected variable type 3" + } should have message "In subst(x2:3, \\((x2:3) ⇒ (x3:2) ⇒ (x4:1) ⇒ x3), \\((x3:2) ⇒ x2)), found variable(s) (x2:1) with incorrect type(s), expected variable type 3" } it should "recover from incorrect substitution" in { From 3764a93fd1b21a28cfc4ad173efe04d4ede02040 Mon Sep 17 00:00:00 2001 From: Sergei Winitzki Date: Fri, 11 May 2018 21:55:22 -0700 Subject: [PATCH 07/11] start on printScala implementation --- README.md | 2 + src/main/scala/io/chymyst/ch/TermExpr.scala | 51 +++++++++---- .../io/chymyst/ch/data/CategoryTheory.scala | 75 +++++++++++++++++++ .../scala/io/chymyst/ch/data/Monoid.scala | 17 ----- .../io/chymyst/ch/unit/TermExprSpec.scala | 20 +++++ 5 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 src/main/scala/io/chymyst/ch/data/CategoryTheory.scala delete mode 100644 src/main/scala/io/chymyst/ch/data/Monoid.scala diff --git a/README.md b/README.md index 862ac6f..67392a1 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ See the [youtube presentation](https://youtu.be/cESdgot_ZxY) for more details ab [This lecture](https://youtu.be/v=KcfD3Iv--UM) is a pedagogical explanation of the Curry-Howard correspondence in the context of functional programming. +See also a [recent presentation at the Haskell User's Group meetup](https://youtu.be/OFBwrMo1ESk). + # Unit tests `sbt test` diff --git a/src/main/scala/io/chymyst/ch/TermExpr.scala b/src/main/scala/io/chymyst/ch/TermExpr.scala index 56f1cea..d9aba93 100644 --- a/src/main/scala/io/chymyst/ch/TermExpr.scala +++ b/src/main/scala/io/chymyst/ch/TermExpr.scala @@ -349,8 +349,8 @@ object TermExpr { * @return A sequence of [[TermExpr]] values containing the necessary fresh variables. */ def subtypeVars(typeExpr: TypeExpr): Seq[TermExpr] = typeExpr match { - case dt@DisjunctT(constructor, typeParams, terms) ⇒ terms.zipWithIndex.flatMap { case (t, i) ⇒ subtypeVars(t).map(v ⇒ DisjunctE(i, terms.length, v, dt)) } - case nct@NamedConjunctT(constructor, typeParams, accessors, wrapped) ⇒ + case dt@DisjunctT(_, _, terms) ⇒ terms.zipWithIndex.flatMap { case (t, i) ⇒ subtypeVars(t).map(v ⇒ DisjunctE(i, terms.length, v, dt)) } + case nct@NamedConjunctT(_, _, _, wrapped) ⇒ TheoremProver.explode(wrapped.map(subtypeVars)).map(NamedConjunctE(_, nct)) case _ ⇒ Seq(VarE(freshIdents(), typeExpr)) } @@ -544,6 +544,27 @@ sealed trait TermExpr { "(" + leftZeros.mkString(" + ") + leftZerosString + term.prettyPrintWithParentheses(0) + rightZerosString + rightZeros.mkString(" + ") + ")" } + lazy val printScala: String = printScalaWithTypes() + + private[ch] def printScalaWithTypes(withTypes: Boolean = false): String = this match { + case VarE(name, _) ⇒ name + (if (withTypes) ": " + t.prettyPrint else "") + case AppE(head, arg) ⇒ + val h = head.printScalaWithTypes(true) + val b = arg.printScalaWithTypes() + s"$h($b)" + case CurriedE(heads, body) ⇒ + s"${heads.map(_.printScalaWithTypes(true)).mkString(" ⇒ ")} ⇒ ${body.printScalaWithTypes()}" + case UnitE(_) ⇒ "()" + case ConjunctE(terms) ⇒ "(" + terms.map(_.printScalaWithTypes()).mkString(", ") + ")" + case NamedConjunctE(terms, tExpr) ⇒ if (tExpr.wrapped.isEmpty) tExpr.constructor.toString + else s"${tExpr.constructor.toString}(${terms.map(_.printScalaWithTypes()).mkString(", ")})" + case ProjectE(index, term) ⇒ term.printScalaWithTypes() + "." + term.accessor(index) + case MatchE(term, cases) ⇒ + term.printScalaWithTypes() + " match { case " + cases.map(_.printScalaWithTypes(true)).mkString("; case ") + " }" + case DisjunctE(index, total, term, _) ⇒ + term.printScalaWithTypes() + } + private def prettyVars: Iterator[String] = for { number ← Iterator.single("") ++ Iterator.from(1).map(_.toString) letter ← ('a' to 'z').toIterator @@ -866,20 +887,20 @@ final case class MatchE(term: TermExpr, cases: List[TermExpr]) extends TermExpr // We already know that q was matched as Left(x). Therefore, we can replace y by x in b and remove the `case other` clause altogether. // Doing a .renameBoundVars on the cases leads to infinite loops somewhere due to incorrect alpha-conversion. val casesSimplified = cases.map(_.simplifyOnce(withEta)) - /* - .zipWithIndex.map { case (c@CurriedE(List(headVar), _), i) ⇒ - TermExpr.substMap(c) { - case MatchE(otherTerm, otherCases) if otherTerm === termSimplified ⇒ - // We already matched `otherTerm`, and we are now in case `c`, which is `case x ⇒ ...`. - // Therefore we can discard any of the `otherCases` except the one corresponding to `c`. - // We can replace the `q match { case y ⇒ b; ...}` by `b` after replacing `x` by `y` in `b`. - val remainingCase = otherCases(i) - val result = AppE(remainingCase, headVar).simplifyOnce(withEta) - // println(s"DEBUG: replacing ${MatchE(otherTerm, otherCases)} by $result in ${c.simplifyOnce(withEta)}") - result - } + /* + .zipWithIndex.map { case (c@CurriedE(List(headVar), _), i) ⇒ + TermExpr.substMap(c) { + case MatchE(otherTerm, otherCases) if otherTerm === termSimplified ⇒ + // We already matched `otherTerm`, and we are now in case `c`, which is `case x ⇒ ...`. + // Therefore we can discard any of the `otherCases` except the one corresponding to `c`. + // We can replace the `q match { case y ⇒ b; ...}` by `b` after replacing `x` by `y` in `b`. + val remainingCase = otherCases(i) + val result = AppE(remainingCase, headVar).simplifyOnce(withEta) + // println(s"DEBUG: replacing ${MatchE(otherTerm, otherCases)} by $result in ${c.simplifyOnce(withEta)}") + result } - */ + } + */ if (casesSimplified.nonEmpty && { casesSimplified.zipWithIndex.forall { // Detect a ⇒ a pattern diff --git a/src/main/scala/io/chymyst/ch/data/CategoryTheory.scala b/src/main/scala/io/chymyst/ch/data/CategoryTheory.scala new file mode 100644 index 0000000..543d660 --- /dev/null +++ b/src/main/scala/io/chymyst/ch/data/CategoryTheory.scala @@ -0,0 +1,75 @@ +package io.chymyst.ch.data + +// Declarations of standard type classes, to be used in macros. + +trait Semigroup[T] { + def combine(x: T, y: T): T +} + +trait Monoid[T] extends Semigroup[T] { + def empty: T +} + +object Monoid { + def empty[T](implicit ev: Monoid[T]): T = ev.empty + + implicit class MonoidSyntax[T](t: T)(implicit ev: Monoid[T]) { + + def combine(y: T): T = ev.combine(t, y) + } + +} + +trait Functor[F[_]] { + def map[A, B](fa: F[A])(f: A ⇒ B): F[B] +} + +trait ContraFunctor[F[_]] { + def map[A, B](fa: F[A])(f: B ⇒ A): F[B] +} + +trait Filterable[F[_]] extends Functor[F] { + def deflate[A](fa: F[Option[A]]): F[A] +} + +trait ContraFilterable[F[_]] extends ContraFunctor[F] { + def inflate[A](fa: F[A]): F[Option[A]] +} + +trait Semimonad[F[_]] extends Functor[F] { + def join[A](ffa: F[F[A]]): F[A] +} + +trait Pointed[F[_]] extends Functor[F] { + def point[A]: F[A] +} + +trait Zippable[F[_]] extends Functor[F] { + def zip[A, B](fa: F[A], fb: F[B]): F[(A, B)] +} + +trait Foldable[F[_]] extends Functor[F] { + def foldMap[A, B: Monoid](fa: F[A])(f: A ⇒ B) +} + +trait Traversable[F[_]] extends Functor[F] { + def sequence[Z[_] : Zippable, A](fga: F[Z[A]]): Z[F[A]] +} + +trait Monad[F[_]] extends Pointed[F] with Semimonad[F] + +trait Applicative[F[_]] extends Pointed[F] with Zippable[F] + +trait Cosemimonad[F[_]] extends Functor[F] { + def cojoin[A](fa: F[A]): F[F[A]] +} + +trait Copointed[F[_]] extends Functor[F] { + def extract[A](fa: F[A]): A +} + +trait Comonad[F[_]] extends Copointed[F] with Cosemimonad[F] + +trait Cozippable[F[_]] extends Functor[F] { + def decide[A, B](fab: F[Either[A, B]]): Either[F[A], F[B]] +} diff --git a/src/main/scala/io/chymyst/ch/data/Monoid.scala b/src/main/scala/io/chymyst/ch/data/Monoid.scala deleted file mode 100644 index dba460d..0000000 --- a/src/main/scala/io/chymyst/ch/data/Monoid.scala +++ /dev/null @@ -1,17 +0,0 @@ -package io.chymyst.ch.data - -trait Monoid[T] { - def empty: T - - def combine(x: T, y: T): T -} - -object Monoid { - def empty[T](implicit ev: Monoid[T]): T = ev.empty - - implicit class MonoidSyntax[T](t: T)(implicit ev: Monoid[T]) { - - def combine(y: T): T = ev.combine(t, y) - } - -} diff --git a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala index 548388a..7510763 100644 --- a/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala +++ b/src/test/scala/io/chymyst/ch/unit/TermExprSpec.scala @@ -32,8 +32,19 @@ class TermExprSpec extends FlatSpec with Matchers { "(0 + Right((0 + Some(Tuple2((0 + Some(z)), (None() + 0))))))", "(0 + Right((0 + Some(Tuple2((0 + Some(z)), (0 + Some(z)))))))" ) + + val subtypeVarsScala = TermExpr.subtypeVars(p.t).map(_.printScala).map(_.replaceAll("z[0-9]+", "z")) shouldEqual + List("Left(None)", "Left(Some(None))", "Left(Some(Some(Tuple2(z, z))))", "Right(None)", "Right(Some(Tuple2(None, None)))", "Right(Some(Tuple2(None, Some(z))))", "Right(Some(Tuple2(Some(z), None)))", "Right(Some(Tuple2(Some(z), Some(z))))") } + it should "compute Scala code of flatten for Option" in { + def flattenOpt[A]: Option[Option[A]] ⇒ Option[A] = implement + + val flattenScala = flattenOpt.lambdaTerm.printScala + + flattenScala shouldEqual "a: Option[Option[A]] ⇒ a match { case b: None.type ⇒ None; case c: Some[Option[A]] ⇒ c.value }" + } + it should "compute extensional equality of functions" in { TermExpr.extEqual(TermExpr.id(typeExpr[Int]), TermExpr.id(typeExpr[Int])) shouldEqual true } @@ -82,6 +93,15 @@ class TermExprSpec extends FlatSpec with Matchers { have message "Incorrect substitution of bound variable x2 by non-variable Tuple2(x, x) in substMap(x2 ⇒ x1){...}" } + behavior of "printScala" + + it should "print functions in Scala syntax" in { + termExpr1.printScala shouldEqual "x2 ⇒ x3 ⇒ x4 ⇒ x3" + termExpr2.printScala shouldEqual "x2 ⇒ x3 ⇒ x4 ⇒ x1" + termExpr3.printScala shouldEqual "x1 ⇒ x2 ⇒ x3 ⇒ x4 ⇒ x1" + + } + behavior of "TermExpr#renameVar" it should "rename one variable" in { From d8d666a4f301f47324e2b34baafd6916a7235432 Mon Sep 17 00:00:00 2001 From: "sergei.winitzki" Date: Mon, 23 Aug 2021 15:38:10 +0200 Subject: [PATCH 08/11] add some build.sbt settings so they don't get lost --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index ecd1f19..2376f53 100644 --- a/build.sbt +++ b/build.sbt @@ -157,14 +157,14 @@ lazy val curryhoward: Project = (project in file(".")) ///////////////////////////////////////////////////////////////////////////////////////////////////// // Publishing to Sonatype Maven repository publishMavenStyle := true -publishTo := sonatypePublishToBundle.value -/*{ +publishTo := //sonatypePublishToBundle.value +{ val nexus = "https://oss.sonatype.org/" if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots") else Some("releases" at nexus + "service/local/staging/deploy/maven2") -}*/ +} // publishArtifact in Test := false // From 7d7c8a39ad1fe9728ec154a4c1b2baba0ae963f0 Mon Sep 17 00:00:00 2001 From: "sergei.winitzki" Date: Sun, 29 Aug 2021 21:05:30 +0200 Subject: [PATCH 09/11] update READMe and bump version --- README.md | 1 + build.sbt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac7ca6e..c8a87a0 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ Build the tutorial (thanks to the [tut plugin](https://github.com/tpolecat/tut)) # Revision history +- 0.3.8 Support Scala 2.13 (keep supporting Scala 2.11 and 2.12) - 0.3.7 Implement the `typeExpr` macro instead of the old test-only API. Detect and use `val`s from the immediately enclosing class. Minor performance improvements and bug fixes (alpha-conversion for STLC terms). Tests for automatic discovery of some monads. - 0.3.6 STLC terms are now emitted for `implement` as well; the JVM bytecode limit is obviated; fixed bug with recognizing `Function10`. - 0.3.5 Added `:@@` and `@@:` operations to the STLC interpreter. Fixed a bug whereby `Tuple2(x._1, x._2)` was not simplified to `x`. Fixed other bugs in alpha-conversion of type parameters. diff --git a/build.sbt b/build.sbt index 2376f53..5dd9f8a 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val curryhoward: Project = (project in file(".")) .settings(common) .settings( organization := "io.chymyst", - version := "0.3.8", + version := "0.3.9", licenses := Seq("Apache License, Version 2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")), homepage := Some(url("https://github.com/Chymyst/curryhoward")), From 4f036347aa256104550a85b22331400e0e4a816c Mon Sep 17 00:00:00 2001 From: "sergei.winitzki" Date: Sat, 6 Nov 2021 12:51:59 +0100 Subject: [PATCH 10/11] one more test that fails --- src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala b/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala index 157f402..48e0bf4 100644 --- a/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala +++ b/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala @@ -123,6 +123,12 @@ class LJTSpec3 extends FlatSpec with Matchers with BeforeAndAfterEach { f3.size shouldEqual 1 } + it should "generate methods for State monad with Int state" in { + final case class S_int[A](run: Int => (A, Int)) // State monad with internal state of type Int. + val wu1: S_int[Unit] = S_int { i => ((), i) } + val wu2: S_int[Unit] = implement // Expect the same + } + it should "generate methods for Continuation monad with no ambiguity" in { case class Cont[X, R](c: (X ⇒ R) ⇒ R) From 4769b4e1b4f6b5913b098b6005ffbe77c8d35c55 Mon Sep 17 00:00:00 2001 From: "sergei.winitzki" Date: Sat, 6 Nov 2021 12:53:28 +0100 Subject: [PATCH 11/11] add more tests --- src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala b/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala index 48e0bf4..d426ca2 100644 --- a/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala +++ b/src/test/scala/io/chymyst/ch/unit/LJTSpec3.scala @@ -126,7 +126,9 @@ class LJTSpec3 extends FlatSpec with Matchers with BeforeAndAfterEach { it should "generate methods for State monad with Int state" in { final case class S_int[A](run: Int => (A, Int)) // State monad with internal state of type Int. val wu1: S_int[Unit] = S_int { i => ((), i) } + wu1.run(123) shouldEqual (((), 123)) val wu2: S_int[Unit] = implement // Expect the same + wu2.run(123) shouldEqual (((), 123)) } it should "generate methods for Continuation monad with no ambiguity" in {