From 7f7b57fb8a789e4e2d40c5986e29bfadc0799336 Mon Sep 17 00:00:00 2001 From: Jeff May Date: Thu, 10 Feb 2022 14:34:34 -0800 Subject: [PATCH] Add repeat operation and support use case of threading it into zipToShortest (#94) * Add repeat operation and support use case of threading it into zipToShortest - Allow IterableOnce for ExprHList.zipToShortest - Use Seq for ZipToShortest of IterableOnce * Add documentation and configuration to Expr.Repeat * Use DSL methods to distinguish between repeat methods * Support better DebugArgs for Repeat --- core-v1/src/main/scala/algebra/Expr.scala | 25 +++ core-v1/src/main/scala/debug/DebugArgs.scala | 7 + core-v1/src/main/scala/dsl/BuildExprDsl.scala | 30 ++++ .../DefaultUnwrappedExprHListImplicits.scala | 13 ++ .../scala/dsl/DslImplicitDefinitions.scala | 61 ++++++- core-v1/src/main/scala/dsl/DslTypes.scala | 2 +- .../scala/dsl/ExprHListDslImplicits.scala | 24 +++ .../scala/dsl/JustifiedBuildExprDsl.scala | 13 ++ .../src/main/scala/dsl/ZipToShortest.scala | 4 +- .../scala/engine/ImmutableCachingEngine.scala | 14 ++ .../src/main/scala/engine/SimpleEngine.scala | 14 ++ .../main/scala/engine/StandardEngine.scala | 6 + .../scala/SimpleJustifiedRepeatSpec.scala | 163 ++++++++++++++++++ core-v1/src/test/scala/SimpleRepeatSpec.scala | 34 ++++ .../src/test/scala/example/FactTypes.scala | 1 + 15 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 core-v1/src/test/scala/SimpleJustifiedRepeatSpec.scala create mode 100644 core-v1/src/test/scala/SimpleRepeatSpec.scala diff --git a/core-v1/src/main/scala/algebra/Expr.scala b/core-v1/src/main/scala/algebra/Expr.scala index 52b565b3d..a30a990cb 100644 --- a/core-v1/src/main/scala/algebra/Expr.scala +++ b/core-v1/src/main/scala/algebra/Expr.scala @@ -338,6 +338,8 @@ object Expr { opO: OP[W[B]], ): I ~:> W[B] + def visitRepeat[I, O](expr: Repeat[I, O, OP])(implicit opO: OP[IterableOnce[O]]): I ~:> IterableOnce[O] + def visitSelect[I, A, B, O : OP](expr: Select[I, A, B, O, OP]): I ~:> O def visitSequence[C[+_] : Applicative : SemigroupK : Traverse, I, O]( @@ -516,6 +518,9 @@ object Expr { opO: OP[W[B]], ): H[I, W[B]] = proxy(underlying.visitOr(expr)) + override def visitRepeat[I, O](expr: Repeat[I, O, OP])(implicit opO: OP[IterableOnce[O]]): H[I, IterableOnce[O]] = + proxy(underlying.visitRepeat(expr)) + override def visitSelect[I, A, B, O : OP](expr: Select[I, A, B, O, OP]): H[I, O] = proxy(underlying.visitSelect(expr)) @@ -1039,6 +1044,26 @@ object Expr { copy(debugging = debugging) } + /** + * Creates an [[IterableOnce]] that emits the result of the input expression forever, or up to a given limit. + * + * @param inputExpr the input expression to repeat + * @param recompute whether to recompute the expression on every iteration or just use the first result + * @param limit whether to limit the total number of elements produced by the iterable + */ + final case class Repeat[-I, +O, OP[_]]( + inputExpr: Expr[I, O, OP], + recompute: Boolean, + limit: Option[Int], + override private[v1] val debugging: Debugging[Nothing, Nothing] = NoDebugging, + )(implicit + opO: OP[IterableOnce[O]], + ) extends Expr[I, IterableOnce[O], OP]("repeat") { + override def visit[G[-_, +_]](v: Visitor[G, OP]): G[I, IterableOnce[O]] = v.visitRepeat(this) + override private[v1] def withDebugging(debugging: Debugging[Nothing, Nothing]): Repeat[I, O, OP] = + copy(debugging = debugging) + } + /** * Grabs all facts of the given [[FactTypeSet]]'s type and returns them as output. * diff --git a/core-v1/src/main/scala/debug/DebugArgs.scala b/core-v1/src/main/scala/debug/DebugArgs.scala index eebed07a4..ee96a4086 100644 --- a/core-v1/src/main/scala/debug/DebugArgs.scala +++ b/core-v1/src/main/scala/debug/DebugArgs.scala @@ -6,6 +6,7 @@ import algebra.{Expr, SizeComparison} import data._ import lens.VariantLens +import cats.Eval import cats.data.{NonEmptySeq, NonEmptyVector} import izumi.reflect.Tag import shapeless.HList @@ -256,6 +257,12 @@ object DebugArgs { override type Out = C[B] } + implicit def debugRepeat[I, O, OP[_]]: Aux[Expr.Repeat[I, O, OP], OP, (I, Eval[O], Option[Int]), IterableOnce[O]] = + new DebugArgs[Expr.Repeat[I, O, OP], OP] { + override type In = (I, Eval[O], Option[Int]) + override type Out = IterableOnce[O] + } + implicit def debugSelect[I, A, B, O, OP[_]]: Aux[Expr.Select[I, A, B, O, OP], OP, (I, A, VariantLens[A, B], B), O] = new DebugArgs[Expr.Select[I, A, B, O, OP], OP] { override type In = (I, A, VariantLens[A, B], B) diff --git a/core-v1/src/main/scala/dsl/BuildExprDsl.scala b/core-v1/src/main/scala/dsl/BuildExprDsl.scala index 9583f3dc9..d15b3b15c 100644 --- a/core-v1/src/main/scala/dsl/BuildExprDsl.scala +++ b/core-v1/src/main/scala/dsl/BuildExprDsl.scala @@ -235,6 +235,36 @@ You should prefer put your declaration of dependency on definitions close to whe constType: ConstOutputType[W, A], ): ConstExprBuilder[constType.Out, OP] + final def repeatConstForever[I, O]( + expr: I ~:> O, + )(implicit + opO: OP[IterableOnce[O]], + ): Expr.Repeat[I, O, OP] = + Expr.Repeat(expr, recompute = false, limit = None) + + final def repeatConst[I, O]( + n: Int, + expr: I ~:> O, + )(implicit + opO: OP[IterableOnce[O]], + ): Expr.Repeat[I, O, OP] = + Expr.Repeat(expr, recompute = false, limit = Some(n)) + + final def repeatForever[I, O]( + expr: I ~:> O, + )(implicit + opO: OP[IterableOnce[O]], + ): Expr.Repeat[I, O, OP] = + Expr.Repeat(expr, recompute = true, limit = None) + + final def repeat[I, O]( + n: Int, + expr: I ~:> O, + )(implicit + opO: OP[IterableOnce[O]], + ): Expr.Repeat[I, O, OP] = + Expr.Repeat(expr, recompute = true, limit = Some(n)) + // TODO: Is this redundant syntax worth keeping around? implicit def inSet[I, A](inputExpr: I ~:> W[A]): InSetExprBuilder[I, A] diff --git a/core-v1/src/main/scala/dsl/DefaultUnwrappedExprHListImplicits.scala b/core-v1/src/main/scala/dsl/DefaultUnwrappedExprHListImplicits.scala index 6edba8c36..336081040 100644 --- a/core-v1/src/main/scala/dsl/DefaultUnwrappedExprHListImplicits.scala +++ b/core-v1/src/main/scala/dsl/DefaultUnwrappedExprHListImplicits.scala @@ -8,6 +8,12 @@ trait DefaultUnwrappedExprHListImplicits with DefaultUnwrappedLowPriorityExprHListDslImplicits with DefaultUnwrappedDslImplicitDefinitions { + override implicit final def hlastAlignIterableOnceMapN[H]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[H] :: HNil, IterableOnce[H], HNil], + ): ZipToShortest.Aux[Seq, IterableOnce[H] :: HNil, OP, H :: HNil] = + defn.hlastAlignIterableOnceMapN + override implicit final def hlastAlignMapN[C[_] : Functor, H]( implicit isCons: IsExprHCons.Aux[C[H] :: HNil, C[H], HNil], @@ -18,6 +24,13 @@ trait DefaultUnwrappedExprHListImplicits zts } + override implicit final def hconsAlignIterableOnceMapN[H, WT <: HList]( + implicit + mt: ZipToShortest[Seq, WT, OP], + isCons: IsExprHCons.Aux[IterableOnce[H] :: WT, IterableOnce[H], WT], + ): ZipToShortest.Aux[Seq, IterableOnce[H] :: WT, OP, H :: mt.UL] = + defn.hconsAlignIterableOnceMapN(mt) + override implicit final def hconsAlignMapN[C[_] : Align : FunctorFilter, H, WT <: HList]( implicit isCons: IsExprHCons.Aux[C[H] :: WT, C[H], WT], diff --git a/core-v1/src/main/scala/dsl/DslImplicitDefinitions.scala b/core-v1/src/main/scala/dsl/DslImplicitDefinitions.scala index 3f103afe0..a8d0c6506 100644 --- a/core-v1/src/main/scala/dsl/DslImplicitDefinitions.scala +++ b/core-v1/src/main/scala/dsl/DslImplicitDefinitions.scala @@ -8,7 +8,7 @@ import com.rallyhealth.vapors.v1.algebra.Expr import com.rallyhealth.vapors.v1.lens.DataPath import shapeless.{::, HList, HNil} -import scala.collection.Factory +import scala.collection.{Factory, IterableOnce} final class DslImplicitDefinitions[W[+_] : Functor : Semigroupal, OP[_]]( implicit @@ -107,6 +107,31 @@ final class DslImplicitDefinitions[W[+_] : Functor : Semigroupal, OP[_]]( ): W[O] = wrapElementW.wrapSelected(wrapped, path, value) } + def hlastAlignIterableOnceMapN[C[a] <: IterableOnce[a], H]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[W[H]] :: HNil, IterableOnce[W[H]], HNil], + factory: Factory[W[H :: HNil], C[W[H :: HNil]]], + ): ZipToShortest.Aux[Lambda[a => C[W[a]]], IterableOnce[W[H]] :: HNil, OP, H :: HNil] = + new ZipToShortest[Lambda[a => C[W[a]]], IterableOnce[W[H]] :: HNil, OP] { + override type UL = H :: HNil + override def zipToShortestWith[G[-_, +_] : Arrow, I]( + xhl: ExprHList[I, IterableOnce[W[H]] :: HNil, OP], + v: Expr.Visitor[G, OP], + ): G[I, C[W[H :: HNil]]] = { + val G = Arrow[G] + val gcwh = xhl.head.visit(v) + gcwh >>> G.lift { cwh => + factory.fromSpecific { + cwh.iterator.map { wh => + wh.map { h => + h :: HNil + } + } + } + } + } + } + def hlastAlignMapN[C[_] : Functor, H]( implicit isCons: IsExprHCons.Aux[C[W[H]] :: HNil, C[W[H]], HNil], @@ -129,6 +154,40 @@ final class DslImplicitDefinitions[W[+_] : Functor : Semigroupal, OP[_]]( } } + def hconsAlignIterableOnceMapN[C[a] <: IterableOnce[a], H, WT <: HList]( + mt: ZipToShortest[Lambda[a => C[W[a]]], WT, OP], + )(implicit + isCons: IsExprHCons.Aux[IterableOnce[W[H]] :: WT, IterableOnce[W[H]], WT], + factory: Factory[W[H :: mt.UL], C[W[H :: mt.UL]]], + ): ZipToShortest.Aux[Lambda[a => C[W[a]]], IterableOnce[W[H]] :: WT, OP, H :: mt.UL] = + new ZipToShortest[Lambda[a => C[W[a]]], IterableOnce[W[H]] :: WT, OP] { + override type UL = H :: mt.UL + override def zipToShortestWith[G[-_, +_] : Arrow, I]( + xhl: ExprHList[I, IterableOnce[W[H]] :: WT, OP], + v: Expr.Visitor[G, OP], + ): G[I, C[W[H :: mt.UL]]] = { + val G = Arrow[G] + val gcwh = xhl.head.visit(v) + val gcwt: G[I, C[W[mt.UL]]] = mt.zipToShortestWith(xhl.tail, v) + (gcwh &&& gcwt) >>> G.lift { + case (cwh, cwt) => + val lefts = cwh.iterator.map(Some(_)) + val rights = cwt.iterator.map(Some(_)) + factory.fromSpecific { + lefts + .zip(rights) + .map { + case (Some(wh), Some(wt)) => Some((wh, wt).mapN { case (h, t) => h :: t }) + case _ => None + } + .collect { + case Some(hl) => hl + } + } + } + } + } + def hconsAlignMapN[C[_] : Align : FunctorFilter, H, WT <: HList]( mt: ZipToShortest[Lambda[a => C[W[a]]], WT, OP], )(implicit diff --git a/core-v1/src/main/scala/dsl/DslTypes.scala b/core-v1/src/main/scala/dsl/DslTypes.scala index c4a09e0f7..84afb23ac 100644 --- a/core-v1/src/main/scala/dsl/DslTypes.scala +++ b/core-v1/src/main/scala/dsl/DslTypes.scala @@ -105,7 +105,7 @@ trait DslTypes extends Any { /** * An [[ExprHList]] with a fixed [[OP]] type. */ - final type XHL[-I, L <: HList] = ExprHList[I, L, OP] + final type XHL[-I, +L <: HList] = ExprHList[I, L, OP] /** * An [[ExprHNil]] with a fixed [[OP]] type. diff --git a/core-v1/src/main/scala/dsl/ExprHListDslImplicits.scala b/core-v1/src/main/scala/dsl/ExprHListDslImplicits.scala index a4a3c43a0..a08d8d8c7 100644 --- a/core-v1/src/main/scala/dsl/ExprHListDslImplicits.scala +++ b/core-v1/src/main/scala/dsl/ExprHListDslImplicits.scala @@ -5,6 +5,8 @@ package dsl import cats.{Align, Functor, FunctorFilter} import shapeless.{::, HList, HNil} +import scala.collection.Factory + /** * A marker trait for determining which set of implicits to inherit. * @@ -27,11 +29,22 @@ sealed trait ExprHListDslImplicits trait WrappedExprHListDslImplicits extends ExprHListDslImplicits { self: DslTypes with WrappedLowPriorityExprHListDslImplicits => + implicit def hlastAlignIterableOnceMapN[H]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[W[H]] :: HNil, IterableOnce[W[H]], HNil], + ): ZipToShortest.Aux[Lambda[a => Seq[W[a]]], IterableOnce[W[H]] :: HNil, OP, H :: HNil] + implicit def hlastAlignMapN[C[_] : Functor, H]( implicit isCons: IsExprHCons.Aux[C[W[H]] :: HNil, C[W[H]], HNil], ): ZipToShortest.Aux[Lambda[a => C[W[a]]], C[W[H]] :: HNil, OP, H :: HNil] + implicit def hconsAlignIterableOnceMapN[H, WT <: HList]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[W[H]] :: WT, IterableOnce[W[H]], WT], + mt: ZipToShortest[Lambda[a => Seq[W[a]]], WT, OP], + ): ZipToShortest.Aux[Lambda[a => Seq[W[a]]], IterableOnce[W[H]] :: WT, OP, H :: mt.UL] + implicit def hconsAlignMapN[C[_] : Align : FunctorFilter, H, WT <: HList]( implicit isCons: IsExprHCons.Aux[C[W[H]] :: WT, C[W[H]], WT], @@ -58,11 +71,22 @@ trait WrappedLowPriorityExprHListDslImplicits { trait UnwrappedExprHListDslImplicits extends ExprHListDslImplicits { self: DslTypes with UnwrappedLowPriorityExprHListDslImplicits => + implicit def hlastAlignIterableOnceMapN[H]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[H] :: HNil, IterableOnce[H], HNil], + ): ZipToShortest.Aux[Seq, IterableOnce[H] :: HNil, OP, H :: HNil] + implicit def hlastAlignMapN[C[_] : Functor, H]( implicit isCons: IsExprHCons.Aux[C[H] :: HNil, C[H], HNil], ): ZipToShortest.Aux[C, C[H] :: HNil, OP, H :: HNil] + implicit def hconsAlignIterableOnceMapN[H, WT <: HList]( + implicit + mt: ZipToShortest[Seq, WT, OP], + isCons: IsExprHCons.Aux[IterableOnce[H] :: WT, IterableOnce[H], WT], + ): ZipToShortest.Aux[Seq, IterableOnce[H] :: WT, OP, H :: mt.UL] + implicit def hconsAlignMapN[C[_] : Align : FunctorFilter, H, WT <: HList]( implicit isCons: IsExprHCons.Aux[C[H] :: WT, C[H], WT], diff --git a/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala b/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala index 10a919e49..a2f493966 100644 --- a/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala +++ b/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala @@ -109,12 +109,25 @@ sealed trait JustifiedExprHListDslImplicits with JustifiedLowPriorityExprHListDslImplicits with DefinedJustifiedDslImplicitDefinitions { + override implicit def hlastAlignIterableOnceMapN[H]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[Justified[H]] :: HNil, IterableOnce[Justified[H]], HNil], + ): ZipToShortest.Aux[Lambda[a => Seq[Justified[a]]], IterableOnce[Justified[H]] :: HNil, OP, H :: HNil] = + defn.hlastAlignIterableOnceMapN + override implicit def hlastAlignMapN[C[_] : Functor, H]( implicit isCons: IsExprHCons.Aux[C[Justified[H]] :: HNil, C[Justified[H]], HNil], ): ZipToShortest.Aux[Lambda[a => C[Justified[a]]], C[Justified[H]] :: HNil, OP, H :: HNil] = defn.hlastAlignMapN + override implicit def hconsAlignIterableOnceMapN[H, WT <: HList]( + implicit + isCons: IsExprHCons.Aux[IterableOnce[Justified[H]] :: WT, IterableOnce[Justified[H]], WT], + mt: ZipToShortest[Lambda[a => Seq[Justified[a]]], WT, OP], + ): ZipToShortest.Aux[Lambda[a => Seq[Justified[a]]], IterableOnce[Justified[H]] :: WT, OP, H :: mt.UL] = + defn.hconsAlignIterableOnceMapN(mt) + override implicit def hconsAlignMapN[C[_] : Align : FunctorFilter, H, WT <: HList]( implicit isCons: IsExprHCons.Aux[C[Justified[H]] :: WT, C[Justified[H]], WT], diff --git a/core-v1/src/main/scala/dsl/ZipToShortest.scala b/core-v1/src/main/scala/dsl/ZipToShortest.scala index 1b8d2fd7b..942383dcb 100644 --- a/core-v1/src/main/scala/dsl/ZipToShortest.scala +++ b/core-v1/src/main/scala/dsl/ZipToShortest.scala @@ -17,7 +17,7 @@ import shapeless.HList * * (Any ~:> Seq[String]) :: (I ~:> Seq[Int]) => Any ~:> Seq[String :: Int :: HNil] */ -trait ZipToShortest[W[_], WL <: HList, OP[_]] { +trait ZipToShortest[+W[_], WL <: HList, OP[_]] { type UL <: HList def zipToShortestWith[G[-_, +_] : Arrow, I]( @@ -30,5 +30,5 @@ trait ZipToShortest[W[_], WL <: HList, OP[_]] { * Implementations live in the subclasses of [[ExprHListDslImplicits]]. */ object ZipToShortest { - type Aux[W[_], WL <: HList, OP[_], UL0] = ZipToShortest[W, WL, OP] { type UL = UL0 } + type Aux[+W[_], WL <: HList, OP[_], UL0] = ZipToShortest[W, WL, OP] { type UL = UL0 } } diff --git a/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala b/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala index 1d1bd41f4..34a97a768 100644 --- a/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala +++ b/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala @@ -405,6 +405,20 @@ object ImmutableCachingEngine { debugging(expr).invokeAndReturn(state((i, inputs), result)) } + override def visitRepeat[I, O]( + expr: Expr.Repeat[I, O, OP], + )(implicit + opO: OP[IterableOnce[O]], + ): I => CachedResult[IterableOnce[O]] = { i => + val always = Eval.always { + expr.inputExpr.visit(this)(i).value + } + val eval = if (expr.recompute) always else always.memoize + val unlimited = Iterator.continually(eval.value) + val iterable = expr.limit.fold(unlimited)(unlimited.take) + debugging(expr).invokeAndReturn(state((i, eval, expr.limit), cached(iterable))) + } + override def visitSelect[I, A, B, O : OP](expr: Expr.Select[I, A, B, O, OP]): I => CachedResult[O] = memoize(expr, _) { i => val inputResult = expr.inputExpr.visit(this)(i) diff --git a/core-v1/src/main/scala/engine/SimpleEngine.scala b/core-v1/src/main/scala/engine/SimpleEngine.scala index 292960836..98be45664 100644 --- a/core-v1/src/main/scala/engine/SimpleEngine.scala +++ b/core-v1/src/main/scala/engine/SimpleEngine.scala @@ -228,6 +228,20 @@ object SimpleEngine { debugging(expr).invokeAndReturn(state((i, results), finalResult)) } + override def visitRepeat[I, O]( + expr: Expr.Repeat[I, O, OP], + )(implicit + opO: OP[IterableOnce[O]], + ): I => IterableOnce[O] = { i => + val always = Eval.always { + expr.inputExpr.visit(this)(i) + } + val eval = if (expr.recompute) always else always.memoize + val unlimited = Iterator.continually(eval.value) + val iterable = expr.limit.fold(unlimited)(unlimited.take) + debugging(expr).invokeAndReturn(state((i, eval, expr.limit), iterable)) + } + override def visitSelect[I, A, B, O : OP](expr: Expr.Select[I, A, B, O, OP]): I => O = { i => val a = expr.inputExpr.visit(this)(i) val b = expr.lens.get(a) diff --git a/core-v1/src/main/scala/engine/StandardEngine.scala b/core-v1/src/main/scala/engine/StandardEngine.scala index 9b2b937d3..e9bbfa456 100644 --- a/core-v1/src/main/scala/engine/StandardEngine.scala +++ b/core-v1/src/main/scala/engine/StandardEngine.scala @@ -265,6 +265,12 @@ object StandardEngine { ExprResult.Or(expr, finalState, results) } + override def visitRepeat[I, O]( + expr: Expr.Repeat[I, O, OP], + )(implicit + opO: OP[IterableOnce[O]], + ): PO <:< I => ExprResult[PO, I, IterableOnce[O], OP] = ??? + override def visitSelect[I, A, B, O : OP]( expr: Expr.Select[I, A, B, O, OP], ): PO <:< I => ExprResult[PO, I, O, OP] = { implicit evPOisI => diff --git a/core-v1/src/test/scala/SimpleJustifiedRepeatSpec.scala b/core-v1/src/test/scala/SimpleJustifiedRepeatSpec.scala new file mode 100644 index 000000000..0fdd3134d --- /dev/null +++ b/core-v1/src/test/scala/SimpleJustifiedRepeatSpec.scala @@ -0,0 +1,163 @@ +package com.rallyhealth.vapors.v1 + +import data.{FactTable, Justified} +import example.FactTypes +import lens.DataPath + +import cats.data.NonEmptySeq +import munit.FunSuite +import shapeless.{HNil, Nat} + +class SimpleJustifiedRepeatSpec extends FunSuite { + + import dsl.uncached.justified._ + + test("repeat[A].take(10)") { + // TODO: The following use case should also be supported once .take is supported + // repeat(1.const).take(10) + } + + test("wrap(repeat[A], Seq[B]).zipToShortest produces a Seq[A :: B :: HNil]") { + val ageFacts = valuesOfType(FactTypes.Age) + val scoreFacts = valuesOfType(FactTypes.Scores) + val expr = wrap(ageFacts, scoreFacts).zipToShortest.flatMap { hl => + val age = hl.get(_.at(Nat._0)) + val scores = hl.get(_.at(Nat._1)) + wrap(repeatConstForever(age), scores).zipToShortest + } + val ageFact = FactTypes.Age(30) + val scoresFact = FactTypes.Scores(Seq(1.0, 2.0)) + val facts = FactTable(ageFact, scoresFact) + val observed = expr.run(facts) + val expected = Vector( + Justified.byInference( + "map", + 30 :: 1.0 :: HNil, + NonEmptySeq.of( + Justified.byInference( + "product", + (30, 1.0 :: HNil), + NonEmptySeq.of( + Justified.bySelection( + 30, + DataPath.empty.atKey(0), + Justified.ByInference( + "map", + 30 :: List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.byInference( + "product", + (30, List(1.0, 2.0) :: HNil), + NonEmptySeq.of( + Justified.byFact(ageFact), + Justified.byInference( + "map", + List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.byFact(scoresFact), + ), + ), + ), + ), + ), + ), + ), + Justified.byInference( + "map", + 1.0 :: HNil, + NonEmptySeq.of( + Justified.bySelection( + 1.0, + DataPath.empty.atKey(1).atIndex(0), + Justified.byInference( + "map", + 30 :: List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.byInference( + "product", + (30, List(1.0, 2.0) :: HNil), + NonEmptySeq.of( + Justified.byFact(ageFact), + Justified + .byInference("map", List(1.0, 2.0) :: HNil, NonEmptySeq.of(Justified.byFact(scoresFact))), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + Justified.byInference( + "map", + 30 :: 2.0 :: HNil, + NonEmptySeq.of( + Justified.byInference( + "product", + (30, 2.0 :: HNil), + NonEmptySeq.of( + Justified.bySelection( + 30, + DataPath.empty.atKey(0), + Justified.byInference( + "map", + 30 :: List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.ByInference( + "product", + (30, List(1.0, 2.0) :: HNil), + NonEmptySeq.of( + Justified.byFact(ageFact), + Justified.byInference( + "map", + List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.byFact(scoresFact), + ), + ), + ), + ), + ), + ), + ), + Justified.ByInference( + "map", + 2.0 :: HNil, + NonEmptySeq.of( + Justified.bySelection( + 2.0, + DataPath.empty.atKey(1).atIndex(1), + Justified.byInference( + "map", + 30 :: List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.ByInference( + "product", + (30, List(1.0, 2.0) :: HNil), + NonEmptySeq.of( + Justified.byFact(ageFact), + Justified.byInference( + "map", + List(1.0, 2.0) :: HNil, + NonEmptySeq.of( + Justified.byFact(scoresFact), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ) + assertEquals(observed, expected) + } +} diff --git a/core-v1/src/test/scala/SimpleRepeatSpec.scala b/core-v1/src/test/scala/SimpleRepeatSpec.scala new file mode 100644 index 000000000..0d0a11da9 --- /dev/null +++ b/core-v1/src/test/scala/SimpleRepeatSpec.scala @@ -0,0 +1,34 @@ +package com.rallyhealth.vapors.v1 + +import data.FactTable +import example.FactTypes + +import munit.FunSuite +import shapeless.{HNil, Nat} + +class SimpleRepeatSpec extends FunSuite { + + import dsl.uncached._ + + test("repeat[A].take(10)") { + // TODO: The following use case should also be supported once .take is supported + // repeat(1.const).take(10) + } + + test("wrap(repeat[A], Seq[B]).zipToShortest produces a Seq[A :: B :: HNil]") { + val ageFacts = valuesOfType(FactTypes.Age) + val scoreFacts = valuesOfType(FactTypes.Scores) + val expr = wrap(ageFacts.headOption, scoreFacts.headOption).zipToShortest.map { hl => + val age = hl.get(_.at(Nat._0)) + val scores = hl.get(_.at(Nat._1)) + wrap(repeatConstForever(age), scores).zipToShortest + } + val facts = FactTable( + FactTypes.Age(30), + FactTypes.Scores(Seq(1d, 2d)), + ) + val observed = expr.run(facts) + val expected = Some(Seq(30 :: 1d :: HNil, 30 :: 2d :: HNil)) + assertEquals(observed, expected) + } +} diff --git a/core-v1/src/test/scala/example/FactTypes.scala b/core-v1/src/test/scala/example/FactTypes.scala index 8d4974262..59f2dce48 100644 --- a/core-v1/src/test/scala/example/FactTypes.scala +++ b/core-v1/src/test/scala/example/FactTypes.scala @@ -15,6 +15,7 @@ object FactTypes extends OrderTimeImplicits { final val DateOfBirth = FactType[LocalDate]("date_of_birth") final val CombinedTags = FactType[CombinedTags]("combined_tags") final val GeoLocation = FactType[GeoLocation]("geolocation") + final val Scores = FactType[Seq[Double]]("scores") final val WeightLbs = FactType[Double]("weight_lbs") final val WeightKg = FactType[Double]("weight_kg") }