From d4ce5915f5e619ee2dcf58277712300f99783529 Mon Sep 17 00:00:00 2001 From: Matt Dziuban Date: Fri, 25 Sep 2020 16:55:27 -0400 Subject: [PATCH 1/3] Add `GCursor` and `KLens` types for traversing data with history; upgrade cats and scala-js. --- build.sbt | 4 +- .../src/main/scala/jsDynamicParsed.scala | 1 - .../src/test/scala/CanParseProp.scala | 4 +- project/plugins.sbt | 4 +- .../shared/src/main/scala/typify/Cursor.scala | 7 +- .../src/main/scala/typify/CursorHistory.scala | 16 +- .../src/main/scala/typify/GCursor.scala | 339 ++++++++++++++++++ .../shared/src/main/scala/typify/KLens.scala | 49 +++ .../scala/typify/StringLabelledGeneric.scala | 35 ++ .../shared/src/main/scala/typify/Typify.scala | 1 - 10 files changed, 438 insertions(+), 22 deletions(-) create mode 100644 typify/shared/src/main/scala/typify/GCursor.scala create mode 100644 typify/shared/src/main/scala/typify/KLens.scala create mode 100644 typify/shared/src/main/scala/typify/StringLabelledGeneric.scala diff --git a/build.sbt b/build.sbt index f2b6bcc..36f9286 100644 --- a/build.sbt +++ b/build.sbt @@ -13,7 +13,7 @@ def scalaVersionSpecificFolders(srcName: String, srcBaseDir: java.io.File, scala lazy val baseSettings = Seq( scalaVersion := scala213, crossScalaVersions := Seq(scala212, scala213), - version := "3.0.0-RC6", + version := "4.0.0-RC1", addCompilerPlugin("io.tryp" %% "splain" % "0.5.0" cross CrossVersion.patch), addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.patch), scalacOptions ++= Seq("-P:splain:all", "-P:splain:keepmodules:500"), @@ -40,7 +40,7 @@ lazy val root = project.in(file(".")) bintrayReleaseOnPublish in ThisBuild := false ) -lazy val cats = Def.setting { "org.typelevel" %%% "cats-core" % "2.1.1" } +lazy val cats = Def.setting { "org.typelevel" %%% "cats-core" % "2.2.0" } lazy val circe = "io.circe" %% "circe-core" % "0.13.0" lazy val json4s = "org.json4s" %% "json4s-jackson" % "3.6.7" lazy val playJson = "com.typesafe.play" %% "play-json" % "2.8.1" diff --git a/jsdynamic-typify/src/main/scala/jsDynamicParsed.scala b/jsdynamic-typify/src/main/scala/jsDynamicParsed.scala index e5d62ac..ccbeff0 100644 --- a/jsdynamic-typify/src/main/scala/jsDynamicParsed.scala +++ b/jsdynamic-typify/src/main/scala/jsDynamicParsed.scala @@ -1,6 +1,5 @@ package scala.scalajs.js.typify -import cats.instances.option._ import cats.syntax.either._ import cats.syntax.option._ import cats.syntax.traverse._ diff --git a/jsdynamic-typify/src/test/scala/CanParseProp.scala b/jsdynamic-typify/src/test/scala/CanParseProp.scala index eede934..db1ee60 100644 --- a/jsdynamic-typify/src/test/scala/CanParseProp.scala +++ b/jsdynamic-typify/src/test/scala/CanParseProp.scala @@ -19,8 +19,8 @@ object MakeJsDynamic extends MakeParsed[js.Dynamic] { case MPOS => MPOS(v).map(x => literal(k -> x)).getOrElse(none) case MPI => literal(k -> v) case MPOI => MPOI(v).map(x => literal(k -> x)).getOrElse(none) - case MPL => literal(k -> v) - case MPOL => MPOL(v).map(x => literal(k -> x)).getOrElse(none) + case MPL => literal(k -> v.toDouble) + case MPOL => MPOL(v).map(x => literal(k -> x.toDouble)).getOrElse(none) case MPD => literal(k -> v) case MPOD => MPOD(v).map(x => literal(k -> x)).getOrElse(none) case MPB => literal(k -> v) diff --git a/project/plugins.sbt b/project/plugins.sbt index ae1fe2a..d5fa233 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.8") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.32") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0") addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.6.13") diff --git a/typify/shared/src/main/scala/typify/Cursor.scala b/typify/shared/src/main/scala/typify/Cursor.scala index a58a1b6..aea0145 100644 --- a/typify/shared/src/main/scala/typify/Cursor.scala +++ b/typify/shared/src/main/scala/typify/Cursor.scala @@ -1,7 +1,6 @@ package typify import cats.Applicative -import cats.instances.option._ import cats.kernel.Eq import cats.syntax.eq._ import java.io.Serializable @@ -32,13 +31,13 @@ sealed abstract class Cursor[A]( /** * The operations that have been performed so far. */ - final def history: CursorHistory[A] = { - @tailrec def go(res: CursorHistory[A], next: Option[Cursor[A]]): CursorHistory[A] = next match { + final def history: CursorHistory = { + @tailrec def go(res: CursorHistory, next: Option[Cursor[A]]): CursorHistory = next match { case Some(c) => go(res :+ c.lastOp, c.lastCursor) case None => res } - go(CursorHistory.empty[A], Some(this)) + go(CursorHistory.empty, Some(this)) } /** diff --git a/typify/shared/src/main/scala/typify/CursorHistory.scala b/typify/shared/src/main/scala/typify/CursorHistory.scala index d3fb1ae..5c3f6e7 100644 --- a/typify/shared/src/main/scala/typify/CursorHistory.scala +++ b/typify/shared/src/main/scala/typify/CursorHistory.scala @@ -2,31 +2,27 @@ package typify import cats.data.NonEmptyVector import cats.Eq -import cats.instances.option._ -import cats.instances.vector._ import cats.syntax.eq._ -class CursorHistory[A]( +final case class CursorHistory( val successfulOps: Vector[CursorOp], val failedOps: Option[NonEmptyVector[CursorOp]] ) { - override final def toString: String = s"CursorHistory($successfulOps, $failedOps)" - def success: Boolean = failedOps.isEmpty def failedOp: Option[CursorOp] = failedOps.map(_.last) def toVector: Vector[CursorOp] = failedOps.fold(Vector[CursorOp]())(_.toVector) ++ successfulOps - def :+(op: Either[CursorOp, CursorOp]): CursorHistory[A] = + def :+(op: Either[CursorOp, CursorOp]): CursorHistory = op.fold( - f => new CursorHistory[A](successfulOps, Some(failedOps.fold(NonEmptyVector.of(f))(_ :+ f))), - s => new CursorHistory[A](successfulOps :+ s, failedOps)) + f => copy(failedOps = Some(failedOps.fold(NonEmptyVector.of(f))(_ :+ f))), + s => copy(successfulOps = successfulOps :+ s)) } object CursorHistory { - def empty[A]: CursorHistory[A] = new CursorHistory(Vector(), None) + lazy val empty: CursorHistory = CursorHistory(Vector(), None) - implicit def eqCursorHistory[A]: Eq[CursorHistory[A]] = + implicit val eqCursorHistory: Eq[CursorHistory] = Eq.instance((a, b) => a.successfulOps === b.successfulOps && a.failedOps === b.failedOps) } diff --git a/typify/shared/src/main/scala/typify/GCursor.scala b/typify/shared/src/main/scala/typify/GCursor.scala new file mode 100644 index 0000000..6820c8c --- /dev/null +++ b/typify/shared/src/main/scala/typify/GCursor.scala @@ -0,0 +1,339 @@ +package typify + +import cats.{Functor, Id} +import cats.data.{NonEmptyList, NonEmptyVector, Validated, ValidatedNel} +import cats.syntax.either._ +import cats.syntax.functor._ +import cats.syntax.list._ +import cats.syntax.semigroup._ +import cats.syntax.validated._ +import scala.annotation.tailrec +import scala.language.dynamics +import shapeless.tag + +/** A cursor representing a view of a specific value of type `A` in an overall value of type `T` + * + * @constructor Constructor + * @tparam T The type of the overall root value + * @tparam A The type of the current value + * @param value The current value + * @param lastOp The last operation performed to produce this cursor + */ +sealed abstract class GCursor[T, A](val value: A, val lastOp: CursorOp) extends Dynamic { self0 => + import GCursor._ + + /** The type of the current focus, may differ from `A` if additional data is needed to support moving the focus */ + type Focus + + /** The type of the last cursor that produced this cursor */ + type Last <: GCursor[T, _] + + /** The root cursor focused on a value of type `T` */ + def root: GCursor.Top[T] + + /** The current focus */ + def focus: Focus + + /** The last cursor that produced this cursor */ + def last: Option[Last] + + private final type Self = GCursor.AuxL[T, A, Focus, Last] + @inline private final val self: Self = self0 + + /** The type of a new cursor produced by this cursor + * + * @tparam NewValue The type of the new value + * @param NewFocus The type of the new focus + */ + final type Next[NewValue, NewFocus] = GCursor.AuxL[T, NewValue, NewFocus, Self] + + /** The history of `CursorOp`s that led to this cursor */ + @inline final lazy val history: Vector[CursorOp] = { + @tailrec def go(res: Vector[CursorOp], next: Option[GCursor[T, _]]): Vector[CursorOp] = next match { + case Some(c) => go(res :+ c.lastOp, c.last) + case None => res + } + + go(Vector(), Some(this)) + } + + /** Create a failed `CursorHistory` */ + private final def failedHistory(failedOp: CursorOp): CursorHistory = + CursorHistory(history, Some(NonEmptyVector.of(failedOp))) + + @inline final private def id[B](b: B): Id[B] = b + @inline final private def dup[B](b: B): Id[(B, B)] = (b, b) + + /** Move the cursor to a new focus and value + * + * @tparam F[_] A functor the next cursor will be wrapped in + * @param op The `CursorOp` representing the move being made + * @param newVals The new focus and value wrapped in the functor `F` + * @return The next cursor wrapped in the functor `F` + */ + private def move[F[_]: Functor, NewFocus, NewValue](op: CursorOp)( + newVals: F[(NewFocus, NewValue)] + ): F[Next[NewValue, NewFocus]] = + newVals.map { case (newFocus, newValue) => new GCursor[T, NewValue](newValue, op) { + type Focus = NewFocus + type Last = GCursor.AuxL[T, A, self.Focus, self.Last] + + lazy val root: GCursor.Top[T] = self.root + lazy val focus: NewFocus = newFocus + lazy val last: Option[GCursor.AuxL[T, A, self.Focus, self.Last]] = Some(self) + }} + + /** If the focus is a list, move the cursor horizontally along it + * + * @tparam I The newly focused index in the list + * @param op The `CursorOp` representing the move being made + * @param getNewIdx A function from the currently focused index to the newly focused index + * @param ev Evidence that the current focus is a `(List[A], Int)` representing the full list and the current index. + * This implies that the current `value` (of type `A`) is the element in the list at the given index. + * @return An optional cursor focused on the new index, `None` if the index doesn't exist in the list. + */ + private def moveList( + op: CursorOp, + newFocus: Focus => (List[A], Int), + failedOp: CursorOp => CursorOp = identity + ): Either[CursorHistory, Next[A, (List[A], Int)]] = + move(op)(newFocus(focus) |> { case t @ (l, ni) => + Either.fromOption(l.lift(ni).map(t -> _), failedHistory(failedOp(op))) }) + + private def updIdx(f: Int => Int)(implicit ev: Focus =:= (List[A], Int)): Focus => (List[A], Int) = + ev(_) |> { case (l, i) => (l, f(i)) } + + /** Map the current value to a new type + * + * @tparam B The type of the new value + * @param f A function from the current value to the new value + * @return A new cursor focused on the value produced by the function f + */ + final def map[B](f: A => B): Next[B, B] = move(CursorOp.WithFocus(identity[B]))(dup(f(value))) + + /** If the focus is a list, move one element to the left + * + * @param ev Evidence that the current focus is a `(List[A], Int)` + * @see def moveList + */ + final def left(implicit ev: Focus =:= (List[A], Int)): Either[CursorHistory, Next[A, (List[A], Int)]] = + moveList(CursorOp.MoveLeft, updIdx(_ - 1)) + + /** If the focus is a list, move `n` elements to the left + * + * @param n How many elements to move + * @param ev Evidence that the current focus is a `(List[A], Int)` + * @see def moveList + */ + final def leftN(n: Int)(implicit ev: Focus =:= (List[A], Int)): Either[CursorHistory, Next[A, (List[A], Int)]] = + moveList(CursorOp.LeftN(n), updIdx(_ - n)) + + /** If the focus is a list, move one element to the right + * + * @param ev Evidence that the current focus is a `(List[A], Int)` + * @see def moveList + */ + final def right(implicit ev: Focus =:= (List[A], Int)): Either[CursorHistory, Next[A, (List[A], Int)]] = + moveList(CursorOp.MoveRight, updIdx(_ + 1)) + + /** If the focus is a list, move `n` elements to the right + * + * @param n How many elements to move + * @param ev Evidence that the current focus is a `(List[A], Int)` + * @see def moveList + */ + final def rightN(n: Int)(implicit ev: Focus =:= (List[A], Int)): Either[CursorHistory, Next[A, (List[A], Int)]] = + moveList(CursorOp.RightN(n), updIdx(_ + n)) + + /** If the focus is a list, move to the first element + * + * @param ev Evidence that the current focus is a `(List[A], Int)` + * @see def moveList + */ + final def first(implicit ev: Focus =:= (List[A], Int)): Either[CursorHistory, Next[A, (List[A], Int)]] = + moveList(CursorOp.MoveFirst, updIdx(_ => 0)) + + /** If the current value is a `List[B]`, move to the first element in the list + * + * @tparam B The type of values in the list + * @param ev Evidence that the current focus is a `List[B]` + * @return A new cursor focused on the first element in the list, `None` if the list was empty + */ + final def downArray[B](implicit ev: A =:= List[B]): Either[CursorHistory, Next[B, (List[B], Int)]] = + move(CursorOp.DownArray(false))(ev(value) |> (l => + Either.fromOption(l.headOption.map((l, 0) -> _), failedHistory(CursorOp.DownArray(true))))) + + /** If the current value is a `NonEmptyList[B]`, move to the first element in the list + * + * @tparam B The type of values in the list + * @param ev Evidence that the current focus is a `List[B]` + * @return A new cursor focused on the first element in the list, `None` if the list was empty + */ + final def downNel[B](implicit ev: A =:= NonEmptyList[B]): Next[B, (List[B], Int)] = + move(CursorOp.DownArray(false))(ev(value) |> (n => id(((n.toList, 0), n.head)))) + + /** If the current value has a field at a key of type `K`, move to the value at the key + * + * @tparam K The type of the key + * @param k The key itself + * @return A new cursor focused on the value of the field + */ + final def downField[K <: String](k: K)(implicit l: KLens[A, K]): Next[l.Out, l.Out] = + move(CursorOp.DownField(k))(dup(l(value))) + + /** Alias for `downField` */ + final def get[K <: Singleton with String](k: K)(implicit l: KLens[A, K]): Next[l.Out, l.Out] = + downField[K](k) + + /** Alias for `downField`, provides shapeless record-like access, e.g. `x("foo")` */ + final def apply[K <: Singleton with String](k: K)(implicit l: KLens[A, K]): Next[l.Out, l.Out] = + downField[K](k) + + /** Alias for `downField` that allows dynamic access, e.g. `x.foo` instead of `x.downField("foo")` + * + * @see https://www.scala-lang.org/api/current/scala/Dynamic.html + */ + final def selectDynamic(k: String)(implicit l: KLens[A, tag.@@[String, k.type]]): Next[l.Out, l.Out] = + downField(tag[k.type](k)) + + /** Return a successful validation of the current value */ + final def valid[L]: ValidatedNel[L, A] = + value.validNel[L] + + /** Validate the current value + * + * @tparam B The success type of the validation + * @param f A validation given the current value of type `A` + * @return The result of running the current value through the validation + */ + final def validate[L, B](f: A => ValidatedNel[L, B]): ValidatedNel[L, B] = + f(value) + + /** Validate the current value, lifting the failure case into a `NonEmptyList` */ + final def validate[L, B](f: A => Validated[L, B])(implicit @deprecated("unused", "") d: Dummy1): ValidatedNel[L, B] = + validate(f(_: A).leftMap(NonEmptyList.of(_))) + + /** Validate the current optional cursor given a validation + * for a non-optional cursor focused on a value of type `B` + * + * @tparam B The inner type of the `Option` that `A` is proven to be + * @tparam C The success type of the validation + * @param f A validation given a cursor focused on a value of type `B` (if the current value was a `Some[A]`) + * @return The result of running the current `Some[A]` value through the validation + */ + final def validateO[L, B, C](f: Next[B, B] => ValidatedNel[L, C])(implicit ev: A =:= Option[B]): ValidatedNel[L, Option[C]] = + validate(ev(_: A).fold(Option.empty[C].validNel[L])(b => f(map(_ => b)).map(Some(_)))) + + /** Validate the current optional cursor, lifting the failure case into a `NonEmptyList` */ + final def validateO[L, B, C](f: Next[B, B] => Validated[L, C])(implicit ev: A =:= Option[B], @deprecated("unused", "") d: Dummy1): ValidatedNel[L, Option[C]] = + validateO(f(_: Next[B, B]).leftMap(NonEmptyList.of(_))) + + /** Validate the current optional value */ + final def validateO[L, B, C](f: B => ValidatedNel[L, C])(implicit ev: A =:= Option[B], @deprecated("unused", "") d: Dummy2): ValidatedNel[L, Option[C]] = + validateO((c: Next[B, B]) => f(c.value)) + + /** Validate the current optional value, lifting the failure case into a `NonEmptyList` */ + final def validateO[L, B, C](f: B => Validated[L, C])(implicit ev: A =:= Option[B], @deprecated("unused", "") d: Dummy3): ValidatedNel[L, Option[C]] = + validateO(f(_: B).leftMap(NonEmptyList.of(_))) + + /** Ensure the current value matches a boolean predicate + * + * @param e The validation errors to return if the predicate returns false + * @param f A predicate function from A to boolean + * @return A validation of the value, successful if the predicate was true + */ + final def ensure[L](e: => NonEmptyList[L])(f: A => Boolean): ValidatedNel[L, A] = + validate((a: A) => Validated.cond(f(a), a, e)) + + /** Ensure the current value matches a boolean predicate and lift the failure case into a `NonEmptyList` */ + final def ensure[L](e: => L)(f: A => Boolean)(implicit @deprecated("unused", "") d: Dummy1): ValidatedNel[L, A] = + ensure(NonEmptyList.of(e))(f) + + /** If the current value is a `NonEmptyList[B]`, traverse it with the provided validation + * + * @tparam B The type of values in the list + * @tparam C The success type of the validation + * @param f A function to validate a single element in the list + * @param err A function to produce a failure case when the list is empty + * @param ev Evidence that the current focus is a `List[B]` + * @return The result of traversing the list and running the validation on each value + */ + final def parseNel[L, B, C](f: GCursor.Arr[T, B] => ValidatedNel[L, C])(implicit ev: A =:= NonEmptyList[B]): ValidatedNel[L, NonEmptyList[C]] = { + @tailrec + def go(acc: ValidatedNel[L, NonEmptyList[C]], next: Either[CursorHistory, GCursor.Arr[T, B]]): ValidatedNel[L, NonEmptyList[C]] = + next match { + case Right(c) => go(acc |+| f(c).map(NonEmptyList.of(_)), c.right) + case Left(_) => acc + } + + downNel[B] |> (c => go(f(c).map(NonEmptyList.of(_)), Right(c))) + } + + private final def parseNel0[L, B, C, O](f: GCursor.Arr[T, B] => ValidatedNel[L, C])( + succ: ValidatedNel[L, NonEmptyList[C]] => ValidatedNel[L, O], + err: CursorHistory => ValidatedNel[L, O] + )(implicit ev: A =:= List[B]): ValidatedNel[L, O] = + ev(value).toNel.fold(err(failedHistory(CursorOp.DownArray(true))))(n => succ(map(_ => n).parseNel(f))) + + /** If the current value is a `List[B]`, prove that it's non-empty and traverse it with the provided validation + * + * @tparam B The type of values in the list + * @tparam C The success type of the validation + * @param f A function to validate a single element in the list + * @param err A function to produce a failure case when the list is empty + * @param ev Evidence that the current focus is a `List[B]` + * @return The result of traversing the list and running the validation on each value + */ + final def parseNel[L, B, C]( + f: GCursor.Arr[T, B] => ValidatedNel[L, C], + err: CursorHistory => NonEmptyList[L] + )(implicit ev: A =:= List[B]): ValidatedNel[L, NonEmptyList[C]] = + parseNel0(f)(identity, err(_).invalid[NonEmptyList[C]]) + + /** If the current value is a `List[B]`, traverse it with the provided validation + * + * @tparam B The type of values in the list + * @tparam C The success type of the validation + * @param f A function to validate a single element in the list + * @param ev Evidence that the current focus is a `List[B]` + * @return The result of traversing the list and running the validation on each value + */ + final def parseList[L, B, C](f: GCursor.Arr[T, B] => ValidatedNel[L, C])(implicit ev: A =:= List[B]): ValidatedNel[L, List[C]] = + parseNel0(f)(_.map(_.toList), _ => List[C]().validNel[L]) +} + +object GCursor { + type Aux[T, A, F] = GCursor[T, A] { + type Focus = F + } + + type AuxL[T, A, F, L <: GCursor[T, _]] = GCursor[T, A] { + type Focus = F + type Last = L + } + + type Top[T] = AuxL[T, T, T, Nothing] + + type Arr[T, A] = Aux[T, A, (List[A], Int)] + + /** Construct a `GCursor` for a given value of type `T` focused on the same value + * + * @tparam T The root type of the cursor + * @param t The root value to focus on + * @return The constructed cursor + */ + def top[T](t: T): Top[T] = + new GCursor[T, T](t, CursorOp.MoveTop) { self => + type Focus = T + type Last = Nothing + lazy val root = self + lazy val focus = t + lazy val last: Option[Nothing] = None + } + + private[GCursor] implicit class PipeOps[A](private val a: A) extends AnyVal { def |>[B](f: A => B): B = f(a) } + + private[GCursor] sealed trait Dummy1; object Dummy1 { implicit val inst: Dummy1 = new Dummy1 {} } + private[GCursor] sealed trait Dummy2; object Dummy2 { implicit val inst: Dummy2 = new Dummy2 {} } + private[GCursor] sealed trait Dummy3; object Dummy3 { implicit val inst: Dummy3 = new Dummy3 {} } +} diff --git a/typify/shared/src/main/scala/typify/KLens.scala b/typify/shared/src/main/scala/typify/KLens.scala new file mode 100644 index 0000000..4e2508f --- /dev/null +++ b/typify/shared/src/main/scala/typify/KLens.scala @@ -0,0 +1,49 @@ +package typify + +import shapeless.HList +import shapeless.ops.record.Selector +import shapeless.tag.@@ + +/** Evidence that a value of type `A` contains a value of type `Out` at a typed key `K` + * i.e. a `shapeless.ops.record.Selector[A, K]` without the constraint that `A` is an `HList` + * + * @tparam A The type of the containing value + * @tparam K The type of the key + */ +trait KLens[A, K] { + type Out + def apply(a: A): Out +} + +object KLens { + /** A type alias that lifts the type member `Out` into a type parameter + * so the compiler can properly infer the output type of an implicit field + * + * @see https://gigiigig.github.io/posts/2015/09/13/aux-pattern.html + */ + type Aux[A, K, O] = KLens[A, K] { type Out = O } + + private final class Inst[K](val dummy: Boolean = true) extends AnyVal { + def apply[A, O](f: A => O): Aux[A, K, O] = new KLens[A, K] { + type Out = O + def apply(a: A): O = f(a) + } + } + + private def inst[K] = new Inst[K] + + /** Derive a `KLens[L, K]` given a `Selector[L, K]` provided by shapeless if `L` is a record containing `K` */ + implicit def kLensFromSelector[A <: HList, K](implicit s: Selector[A, K]): Aux[A, K, s.Out] = + inst[K](s(_: A)) + + /** Derive a `KLens[A, K]` given a generic representation of `A` as the record `Repr` and a `Selector[Repr, K]` */ + implicit def kLensFromReprSelector[A, Repr <: HList, K]( + implicit lg: StringLabelledGeneric.Aux[A, Repr], + s: Selector[Repr, K] + ): Aux[A, K, s.Out] = + inst[K]((a: A) => s(lg.to(a))) + + /** Derive a `KLens[L, String @@ K]` given a `KLens[L, K]` -- used for `selectDynamic` */ + implicit def kLensForTaggedString[A, K](implicit f: KLens[A, K]): Aux[A, String @@ K, f.Out] = + inst[String @@ K](f(_: A)) +} diff --git a/typify/shared/src/main/scala/typify/StringLabelledGeneric.scala b/typify/shared/src/main/scala/typify/StringLabelledGeneric.scala new file mode 100644 index 0000000..36631b8 --- /dev/null +++ b/typify/shared/src/main/scala/typify/StringLabelledGeneric.scala @@ -0,0 +1,35 @@ +package typify + +import shapeless.{HList, Poly1} +import shapeless.labelled.FieldType +import shapeless.ops.hlist.Mapper +import shapeless.tag.@@ + +trait StringLabelledGeneric[A] extends shapeless.LabelledGeneric[A] + +object StringLabelledGeneric { + type Aux[A, R] = StringLabelledGeneric[A] { type Repr = R } + + def apply[A](implicit l: StringLabelledGeneric[A]): Aux[A, l.Repr] = l + def toRecord[A](a: A)(implicit l: StringLabelledGeneric[A]): l.Repr = l.to(a) + + object stringify extends Poly1 { + implicit def sym[K, A]: Case.Aux[FieldType[Symbol @@ K, A], FieldType[K, A]] = + at(_.asInstanceOf[FieldType[K, A]]) + } + + object symbolify extends Poly1 { + implicit def str[K, A]: Case.Aux[FieldType[K, A], FieldType[Symbol @@ K, A]] = + at(_.asInstanceOf[FieldType[Symbol @@ K, A]]) + } + + implicit def inst[A, SymRepr <: HList, StrRepr <: HList]( + implicit lg: shapeless.LabelledGeneric.Aux[A, SymRepr], + toStr: Mapper.Aux[stringify.type, SymRepr, StrRepr], + fromStr: Mapper.Aux[symbolify.type, StrRepr, SymRepr] + ): Aux[A, StrRepr] = new StringLabelledGeneric[A] { + type Repr = StrRepr + def to(a: A): StrRepr = toStr(lg.to(a)) + def from(r: StrRepr): A = lg.from(fromStr(r)) + } +} diff --git a/typify/shared/src/main/scala/typify/Typify.scala b/typify/shared/src/main/scala/typify/Typify.scala index ebda3de..12d2a73 100644 --- a/typify/shared/src/main/scala/typify/Typify.scala +++ b/typify/shared/src/main/scala/typify/Typify.scala @@ -1,7 +1,6 @@ package typify import cats.data.ValidatedNel -import cats.instances.option._ import cats.syntax.traverse._ import cats.syntax.validated._ import scala.reflect.ClassTag From 678f160a792954337ba317dc40a713977b2d5033 Mon Sep 17 00:00:00 2001 From: Matt Dziuban Date: Mon, 28 Sep 2020 09:38:12 -0400 Subject: [PATCH 2/3] Use `Self` type alias, use `moveList` for `downArray` too. --- typify/shared/src/main/scala/typify/GCursor.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/typify/shared/src/main/scala/typify/GCursor.scala b/typify/shared/src/main/scala/typify/GCursor.scala index 6820c8c..7333d7c 100644 --- a/typify/shared/src/main/scala/typify/GCursor.scala +++ b/typify/shared/src/main/scala/typify/GCursor.scala @@ -13,7 +13,6 @@ import shapeless.tag /** A cursor representing a view of a specific value of type `A` in an overall value of type `T` * - * @constructor Constructor * @tparam T The type of the overall root value * @tparam A The type of the current value * @param value The current value @@ -76,11 +75,11 @@ sealed abstract class GCursor[T, A](val value: A, val lastOp: CursorOp) extends ): F[Next[NewValue, NewFocus]] = newVals.map { case (newFocus, newValue) => new GCursor[T, NewValue](newValue, op) { type Focus = NewFocus - type Last = GCursor.AuxL[T, A, self.Focus, self.Last] + type Last = self.Self lazy val root: GCursor.Top[T] = self.root lazy val focus: NewFocus = newFocus - lazy val last: Option[GCursor.AuxL[T, A, self.Focus, self.Last]] = Some(self) + lazy val last: Option[self.Self] = Some(self) }} /** If the focus is a list, move the cursor horizontally along it @@ -92,11 +91,11 @@ sealed abstract class GCursor[T, A](val value: A, val lastOp: CursorOp) extends * This implies that the current `value` (of type `A`) is the element in the list at the given index. * @return An optional cursor focused on the new index, `None` if the index doesn't exist in the list. */ - private def moveList( + private def moveList[B]( op: CursorOp, - newFocus: Focus => (List[A], Int), + newFocus: Focus => (List[B], Int), failedOp: CursorOp => CursorOp = identity - ): Either[CursorHistory, Next[A, (List[A], Int)]] = + ): Either[CursorHistory, Next[B, (List[B], Int)]] = move(op)(newFocus(focus) |> { case t @ (l, ni) => Either.fromOption(l.lift(ni).map(t -> _), failedHistory(failedOp(op))) }) @@ -160,8 +159,7 @@ sealed abstract class GCursor[T, A](val value: A, val lastOp: CursorOp) extends * @return A new cursor focused on the first element in the list, `None` if the list was empty */ final def downArray[B](implicit ev: A =:= List[B]): Either[CursorHistory, Next[B, (List[B], Int)]] = - move(CursorOp.DownArray(false))(ev(value) |> (l => - Either.fromOption(l.headOption.map((l, 0) -> _), failedHistory(CursorOp.DownArray(true))))) + moveList(CursorOp.DownArray(false), _ => (ev(value), 0), _ => CursorOp.DownArray(true)) /** If the current value is a `NonEmptyList[B]`, move to the first element in the list * From fe226aa72a21139d4058987b1bb9e337c5d6664c Mon Sep 17 00:00:00 2001 From: Matt Dziuban Date: Mon, 28 Sep 2020 09:49:43 -0400 Subject: [PATCH 3/3] `downArray` => `downList`, add `Functor` and `Comonad`, bump versions. --- build.sbt | 8 +++---- project/plugins.sbt | 2 +- .../src/main/scala/typify/GCursor.scala | 23 ++++++++++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index 36f9286..c37b0ec 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges -lazy val scala212 = "2.12.10" -lazy val scala213 = "2.13.1" +lazy val scala212 = "2.12.12" +lazy val scala213 = "2.13.3" def scalaVersionSpecificFolders(srcName: String, srcBaseDir: java.io.File, scalaVersion: String): Seq[java.io.File] = CrossVersion.partialVersion(scalaVersion) match { @@ -13,8 +13,8 @@ def scalaVersionSpecificFolders(srcName: String, srcBaseDir: java.io.File, scala lazy val baseSettings = Seq( scalaVersion := scala213, crossScalaVersions := Seq(scala212, scala213), - version := "4.0.0-RC1", - addCompilerPlugin("io.tryp" %% "splain" % "0.5.0" cross CrossVersion.patch), + version := "4.0.0-RC2", + addCompilerPlugin("io.tryp" %% "splain" % "0.5.7" cross CrossVersion.patch), addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.patch), scalacOptions ++= Seq("-P:splain:all", "-P:splain:keepmodules:500"), scalacOptions --= Seq( diff --git a/project/plugins.sbt b/project/plugins.sbt index d5fa233..658170d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.8") +addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.13") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0") diff --git a/typify/shared/src/main/scala/typify/GCursor.scala b/typify/shared/src/main/scala/typify/GCursor.scala index 7333d7c..bc05bee 100644 --- a/typify/shared/src/main/scala/typify/GCursor.scala +++ b/typify/shared/src/main/scala/typify/GCursor.scala @@ -1,6 +1,6 @@ package typify -import cats.{Functor, Id} +import cats.{Comonad, Functor, Id} import cats.data.{NonEmptyList, NonEmptyVector, Validated, ValidatedNel} import cats.syntax.either._ import cats.syntax.functor._ @@ -158,7 +158,7 @@ sealed abstract class GCursor[T, A](val value: A, val lastOp: CursorOp) extends * @param ev Evidence that the current focus is a `List[B]` * @return A new cursor focused on the first element in the list, `None` if the list was empty */ - final def downArray[B](implicit ev: A =:= List[B]): Either[CursorHistory, Next[B, (List[B], Int)]] = + final def downList[B](implicit ev: A =:= List[B]): Either[CursorHistory, Next[B, (List[B], Int)]] = moveList(CursorOp.DownArray(false), _ => (ev(value), 0), _ => CursorOp.DownArray(true)) /** If the current value is a `NonEmptyList[B]`, move to the first element in the list @@ -300,7 +300,7 @@ sealed abstract class GCursor[T, A](val value: A, val lastOp: CursorOp) extends parseNel0(f)(_.map(_.toList), _ => List[C]().validNel[L]) } -object GCursor { +object GCursor extends GCursorInstances0 { type Aux[T, A, F] = GCursor[T, A] { type Focus = F } @@ -335,3 +335,20 @@ object GCursor { private[GCursor] sealed trait Dummy2; object Dummy2 { implicit val inst: Dummy2 = new Dummy2 {} } private[GCursor] sealed trait Dummy3; object Dummy3 { implicit val inst: Dummy3 = new Dummy3 {} } } + +private[typify] abstract class GCursorInstances0 { + implicit def gcursorComonad[T]: Comonad[GCursor[T, ?]] = new GCursorComonad[T] {} +} + +private[typify] abstract class GCursorInstances1 { + implicit def gcursorFunctor[T]: Functor[GCursor[T, ?]] = new GCursorFunctor[T] {} +} + +private[typify] trait GCursorComonad[T] extends Comonad[GCursor[T, ?]] with GCursorFunctor[T] { + def coflatMap[A, B](c: GCursor[T, A])(f: GCursor[T, A] => B): GCursor[T, B] = map(c)(_ => f(c)) + def extract[A](c: GCursor[T, A]): A = c.value +} + +private[typify] trait GCursorFunctor[T] extends Functor[GCursor[T, ?]] { + def map[A, B](c: GCursor[T, A])(f: A => B): GCursor[T, B] = c.map(f) +}