Skip to content

Commit

Permalink
Add support for Expr.Match
Browse files Browse the repository at this point in the history
- Add support for guard expressions
- Use existential types for MatchCase
- Add unit tests
- Add branch index to DebugArgs
  • Loading branch information
jeffmay committed Feb 17, 2022
1 parent 7f7b57f commit 56f5440
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 14 deletions.
101 changes: 99 additions & 2 deletions core-v1/src/main/scala/algebra/Expr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ package algebra

import data._
import debug.{DebugArgs, Debugging, NoDebugging}
import dsl.{ConvertToHList, ExprHList, ExprHNil, Sortable, ZipToShortest}
import dsl._
import lens.{CollectInto, VariantLens}
import logic.{Conjunction, Disjunction, Negation}
import math._

import cats.data.{NonEmptySeq, NonEmptyVector}
import cats.{Applicative, FlatMap, Foldable, Functor, SemigroupK, Traverse}
import shapeless.{::, HList, HNil}
import shapeless.{::, HList, HNil, Typeable}

import scala.annotation.nowarn
import scala.reflect.ClassTag

/**
* The root trait of all expression nodes.
Expand Down Expand Up @@ -324,6 +325,12 @@ object Expr {

def visitMapEvery[C[_] : Functor, A, B](expr: MapEvery[C, A, B, OP])(implicit opO: OP[C[B]]): C[A] ~:> C[B]

def visitMatch[I, S, B : ExtractValue.AsBoolean, O](
expr: Match[I, S, B, O, OP],
)(implicit
opO: OP[Option[O]],
): I ~:> Option[O]

def visitNot[I, B, W[+_]](
expr: Not[I, B, W, OP],
)(implicit
Expand Down Expand Up @@ -504,6 +511,13 @@ object Expr {
opO: OP[C[B]],
): H[C[A], C[B]] = proxy(underlying.visitMapEvery(expr))

override def visitMatch[I, S, B : ExtractValue.AsBoolean, O](
expr: Match[I, S, B, O, OP],
)(implicit
opO: OP[Option[O]],
): H[I, Option[O]] =
proxy(underlying.visitMatch(expr))

override def visitNot[I, B, F[+_]](
expr: Not[I, B, F, OP],
)(implicit
Expand Down Expand Up @@ -1225,6 +1239,87 @@ object Expr {
copy(debugging = debugging)
}

/**
* A case in an [[Expr.Match]] expression that defines the expected type, any additional guard condition,
* and then what to do with the value of the expected type.
*
* @tparam I the input type of the expression that must be casted
* @tparam S the expected type of the input. Must be a subtype of the expected input type.
* @tparam B the result type of the optional [[maybeGuardExpr]]
* @tparam O the output type of the [[thenExpr]]
*/
sealed abstract class MatchCase[-I, S, +B, +O, OP[_]] {

/** Cast the input the expected type. */
def cast: I => Option[S]

/** The expression to perform if the type is correct. Must return a truthy value before evaluating the [[thenExpr]] */
def maybeGuardExpr: Option[Expr[S, B, OP]]

/** The expression to perform if the type is correct and the value passes the guard expression (if any). */
def thenExpr: Expr[S, O, OP]
}

object MatchCase {

/**
* A case that matches a type, but does not check any condition.
*
* @note the [[cast]] method typically defers to [[Typeable.cast]], which requires that the expression produces
* a type that shapeless can generate a [[Typeable]] for. You can define your own for more complex types.
*
* @tparam I the input type of the expression that must be casted
* @tparam S the expected type of value for the [[thenExpr]]
* @tparam O the output type of the [[thenExpr]]
*/
final case class Unguarded[-I, S, +O, OP[_]](
cast: I => Option[S],
thenExpr: Expr[S, O, OP],
) extends MatchCase[I, S, Nothing, O, OP] {
override def maybeGuardExpr: Option[Nothing] = None
}

/**
* A case that matches a type and checks that the value meets a given condition.
*
* @note the [[cast]] method defers to [[Typeable.cast]], which requires that the expression produces a type that
* shapeless can generate a [[Typeable]] for. You can define your own for more complex types.
*
* @tparam I the input type of the expression that must be casted
* @tparam S the expected type of value for the [[thenExpr]]
* @tparam B the Boolean-like type that is a result of the [[guardExpr]]
* @tparam O the output type of the [[thenExpr]]
*/
final case class Guarded[-I, S, +B : ExtractValue.AsBoolean, +O, OP[_]](
cast: I => Option[S],
guardExpr: Expr[S, B, OP],
thenExpr: Expr[S, O, OP],
) extends MatchCase[I, S, B, O, OP] {
override def maybeGuardExpr: Option[Expr[S, B, OP]] = Some(guardExpr)
}
}

/**
* Matches on a series of [[MatchCase]]s by type & an optional guard expression, and if the case matches,
* evaluates the expression in the given case. If none of the cases match, then the whole result is None.
*
* This is effectively a more powerful [[When]] expression that can additionally match on subtypes.
*
* @note this will explore the branches in the order they are given.
*
* @param branches the cases to attempt to match, in order
*/
final case class Match[-I, +S, +B : ExtractValue.AsBoolean, +O, OP[_]](
branches: IndexedSeq[MatchCase[I, _ <: S, B, O, OP]],
override private[v1] val debugging: Debugging[Nothing, Nothing] = NoDebugging,
)(implicit
opO: OP[Option[O]],
) extends Expr[I, Option[O], OP]("matching") {
override def visit[G[-_, +_]](v: Visitor[G, OP]): G[I, Option[O]] = v.visitMatch(this)
override private[v1] def withDebugging(debugging: Debugging[Nothing, Nothing]): Match[I, S, B, O, OP] =
copy(debugging = debugging)
}

/**
* A container for a condition and an expression to compute if the condition is met.
*
Expand All @@ -1241,6 +1336,8 @@ object Expr {
/**
* A branching if [ / elif ...] / else conditional operation.
*
* This is effectively a [[Match]] expression, but simplified to boolean cases without subtype matching.
*
* @param conditionBranches all of the branches that are guarded by condition expressions
* @param defaultExpr the expression to run if none of the branch conditions matches
* @tparam B a Boolean-like type that is used to determine if a branch condition is matched should run
Expand Down
1 change: 0 additions & 1 deletion core-v1/src/main/scala/data/Justified.scala
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ object Justified extends LowPriorityJustifiedImplicits {
path: DataPath,
element: O,
)(implicit
opA: Any,
opB: Any,
): Justified[O] = {
container.withView((_: Any) => VariantLens(path, (_: Any) => element))
Expand Down
9 changes: 8 additions & 1 deletion core-v1/src/main/scala/debug/DebugArgs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import lens.VariantLens

import cats.Eval
import cats.data.{NonEmptySeq, NonEmptyVector}
import com.rallyhealth.vapors.v1.algebra.Expr.MatchCase
import izumi.reflect.Tag
import shapeless.HList
import shapeless.{unexpected, HList}

import scala.reflect.ClassTag

Expand Down Expand Up @@ -305,6 +306,12 @@ object DebugArgs {
override type Out = Seq[O]
}

implicit def debugMatch[I, S, B, O, OP[_]]: Aux[Expr.Match[I, S, B, O, OP], OP, (I, Option[Int]), Option[O]] =
new DebugArgs[Expr.Match[I, S, B, O, OP], OP] {
override type In = (I, Option[Int])
override type Out = Option[O]
}

implicit def debugWhen[I, B, O, OP[_]]: Aux[Expr.When[I, B, O, OP], OP, (I, Int), O] =
new DebugArgs[Expr.When[I, B, O, OP], OP] {
override type In = (I, Int)
Expand Down
36 changes: 35 additions & 1 deletion core-v1/src/main/scala/dsl/BuildExprDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import math.{Add, Power}

import cats.data.{NonEmptySeq, NonEmptyVector}
import cats.{Applicative, FlatMap, Foldable, Functor, Id, Order, Reducible, SemigroupK, Traverse}
import shapeless.{Generic, HList}
import com.rallyhealth.vapors.v1.algebra.Expr.MatchCase
import izumi.reflect.Tag
import shapeless.{Generic, HList, Typeable}

import scala.reflect.ClassTag

trait BuildExprDsl
extends DebugExprDsl
Expand Down Expand Up @@ -265,6 +269,30 @@ You should prefer put your declaration of dependency on definitions close to whe
): Expr.Repeat[I, O, OP] =
Expr.Repeat(expr, recompute = true, limit = Some(n))

type Case[T, +O] = MatchCase[W[Any], W[T], W[Boolean], W[O], OP]
def Case[T : Typeable : Tag]: CasePartiallyApplied[T]

abstract class CasePartiallyApplied[T : Typeable : Tag] {

def ==>[O](
thenExpr: W[T] ~:> W[O],
)(implicit
opT: OP[T],
): Case[T, O]

def when[O](whenExprBuilder: W[T] =~:> W[Boolean]): CaseWithGuardPartiallyApplied[T]
}

abstract class CaseWithGuardPartiallyApplied[T : Typeable : Tag](proof: W[T] =~:> W[Boolean]) {

def ==>[O](
thenExpr: W[T] ~:> W[O],
)(implicit
opT: OP[T],
opWT: OP[W[T]],
): Case[T, O]
}

// TODO: Is this redundant syntax worth keeping around?
implicit def inSet[I, A](inputExpr: I ~:> W[A]): InSetExprBuilder[I, A]

Expand All @@ -290,6 +318,12 @@ You should prefer put your declaration of dependency on definitions close to whe
): Expr.Select[I, W[A], B, O, OP]

def getAs[C[_]]: GetAsWrapper[I, W, A, C, OP]

def matching[O](
cases: Case[_ <: A, O]*,
)(implicit
opO: OP[Option[W[O]]],
): AndThen[I, W[A], Option[W[O]]]
}

implicit def xhlOps[I, WL <: HList](exprHList: ExprHList[I, WL, OP]): ExprHListOpsBuilder[I, WL]
Expand Down
42 changes: 40 additions & 2 deletions core-v1/src/main/scala/dsl/UnwrappedBuildExprDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package com.rallyhealth.vapors.v1
package dsl

import algebra._
import data.{Extract, FactType, FactTypeSet, SliceRange, TypedFact}
import data._
import lens.{CollectInto, IterableInto, VariantLens}
import logic.Logic
import math.{Add, Power}

import cats.data.NonEmptySeq
import cats.{FlatMap, Foldable, Functor, Id, Order, Reducible, Traverse}
import shapeless.{Generic, HList, Nat}
import izumi.reflect.Tag
import shapeless.{Generic, HList, Nat, Typeable}

trait UnwrappedBuildExprDsl
extends BuildExprDsl
Expand Down Expand Up @@ -162,6 +163,34 @@ trait UnwrappedBuildExprDsl
): ConstExprBuilder[constType.Out, OP] =
new ConstExprBuilder(constType.wrapConst(value))

override type Case[T, +O] = Expr.MatchCase[Any, T, Boolean, O, OP]
override def Case[T : Typeable : Tag]: UnwrappedCasePartiallyApplied[T] = new UnwrappedCasePartiallyApplied[T]

final class UnwrappedCasePartiallyApplied[T : Typeable : Tag] extends CasePartiallyApplied[T] {

override def ==>[O](
thenExpr: T ~:> O,
)(implicit
opT: OP[T],
): Case[T, O] =
Expr.MatchCase.Unguarded(Typeable[T].cast, thenExpr)

override def when[O](whenExprBuilder: T =~:> Boolean): UnwrappedCaseWithGuardPartiallyApplied[T] =
new UnwrappedCaseWithGuardPartiallyApplied(whenExprBuilder)
}

final class UnwrappedCaseWithGuardPartiallyApplied[T : Typeable : Tag](whenExprBuilder: T =~:> Boolean)
extends CaseWithGuardPartiallyApplied(whenExprBuilder) {

override def ==>[O](
thenExpr: T ~:> O,
)(implicit
opT: OP[T],
opWT: OP[T],
): Case[T, O] =
Expr.MatchCase.Guarded(Typeable[T].cast, whenExprBuilder(ident(opWT)), thenExpr)
}

// TODO: Is this redundant syntax worth keeping around?
override implicit final def inSet[I, A](inputExpr: I ~:> A): UnwrappedInSetExprBuilder[I, A] =
new UnwrappedInSetExprBuilder(inputExpr)
Expand Down Expand Up @@ -193,6 +222,15 @@ trait UnwrappedBuildExprDsl
}

override def getAs[C[_]]: GetAsUnwrapped[I, A, C, OP] = new GetAsUnwrapped(inputExpr)

override def matching[O](
cases: Case[_ <: A, O]*,
)(implicit
opO: OP[Option[O]],
): AndThen[I, A, Option[O]] =
inputExpr.andThen {
Expr.Match(cases.toIndexedSeq)
}
}

override implicit final def xhlOps[I, WL <: HList](xhl: ExprHList[I, WL, OP]): UnwrappedExprHListOpsBuilder[I, WL] =
Expand Down
2 changes: 0 additions & 2 deletions core-v1/src/main/scala/dsl/WrapSelected.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ trait WrapSelected[W[+_], OP[_]] {
path: DataPath,
element: O,
)(implicit
opA: OP[I],
opB: OP[O],
): W[O]
}
Expand All @@ -43,7 +42,6 @@ object WrapSelected {
path: DataPath,
element: B,
)(implicit
opA: Any,
opB: Any,
): B = element
}
Expand Down

0 comments on commit 56f5440

Please sign in to comment.