From 2feecf8a1ffa7beb58770a5a47d37f07fb975c14 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 15 May 2020 22:06:50 -0400 Subject: [PATCH] Selective functor This implements Selective functor for `Either[A, B]` "task" (`Initialize[Task[Either[A, B]]]`). The selective functor allows an encoding of if-expression: ``` def ifS[A]( x: Def.Initialize[Task[Boolean]] )(t: Def.Initialize[Task[A]])(e: Def.Initialize[Task[A]]): Def.Initialize[Task[A]] ``` The benefit of this approach is that task dependencies are still visible to inspect command. --- .../scala/sbt/internal/util/Classes.scala | 4 +++ main-settings/src/main/scala/sbt/Def.scala | 35 +++++++++++++++++++ .../src/main/scala/sbt/Structure.scala | 14 ++++++++ sbt/src/sbt-test/actions/ifs/build.sbt | 25 +++++++++++++ sbt/src/sbt-test/actions/ifs/test | 7 ++++ .../src/main/scala/sbt/Action.scala | 25 +++++++++++++ .../src/main/scala/sbt/std/TaskExtra.scala | 13 ++++--- .../src/main/scala/sbt/std/Transform.scala | 1 + 8 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 sbt/src/sbt-test/actions/ifs/build.sbt create mode 100644 sbt/src/sbt-test/actions/ifs/test diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala index 596bc39f9..2d7c7aeac 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Classes.scala @@ -14,6 +14,10 @@ object Classes { def map[S, T](f: S => T, v: M[S]): M[T] } + trait Selective[M[_]] extends Applicative[M] { + def select[A, B](fab: M[Either[A, B]])(fn: M[A => B]): M[B] + } + trait Monad[M[_]] extends Applicative[M] { def flatten[T](m: M[M[T]]): M[T] } diff --git a/main-settings/src/main/scala/sbt/Def.scala b/main-settings/src/main/scala/sbt/Def.scala index b63e58d3b..dd1a616dd 100644 --- a/main-settings/src/main/scala/sbt/Def.scala +++ b/main-settings/src/main/scala/sbt/Def.scala @@ -235,6 +235,41 @@ object Def extends Init[Scope] with TaskMacroExtra with InitializeImplicits { def inputTaskDyn[T](t: Def.Initialize[Task[T]]): Def.Initialize[InputTask[T]] = macro inputTaskDynMacroImpl[T] + private[sbt] def selectITask[A, B]( + fab: Initialize[Task[Either[A, B]]], + fin: Initialize[Task[A => B]] + ): Initialize[Task[B]] = + fab.zipWith(fin)((ab, in) => TaskExtra.select(ab, in)) + + import Scoped.syntax._ + + // derived from select + private[sbt] def branchS[A, B, C]( + x: Def.Initialize[Task[Either[A, B]]] + )(l: Def.Initialize[Task[A => C]])(r: Def.Initialize[Task[B => C]]): Def.Initialize[Task[C]] = { + val lhs = { + val innerLhs: Def.Initialize[Task[Either[A, Either[B, C]]]] = + x.map((fab: Either[A, B]) => fab.right.map(Left(_))) + val innerRhs: Def.Initialize[Task[A => Either[B, C]]] = + l.map((fn: A => C) => fn.andThen(Right(_))) + selectITask(innerLhs, innerRhs) + } + selectITask(lhs, r) + } + + // derived from select + def ifS[A]( + x: Def.Initialize[Task[Boolean]] + )(t: Def.Initialize[Task[A]])(e: Def.Initialize[Task[A]]): Def.Initialize[Task[A]] = { + val condition: Def.Initialize[Task[Either[Unit, Unit]]] = + x.map((p: Boolean) => if (p) Left(()) else Right(())) + val left: Def.Initialize[Task[Unit => A]] = + t.map((a: A) => { _ => a }) + val right: Def.Initialize[Task[Unit => A]] = + e.map((a: A) => { _ => a }) + branchS(condition)(left)(right) + } + // The following conversions enable the types Initialize[T], Initialize[Task[T]], and Task[T] to // be used in task and setting macros as inputs with an ultimate result of type T diff --git a/main-settings/src/main/scala/sbt/Structure.scala b/main-settings/src/main/scala/sbt/Structure.scala index 65e0ebd8d..6ec525230 100644 --- a/main-settings/src/main/scala/sbt/Structure.scala +++ b/main-settings/src/main/scala/sbt/Structure.scala @@ -315,6 +315,20 @@ object Scoped { final def ??[T >: S](or: => T): Initialize[T] = Def.optional(scopedKey)(_ getOrElse or) } + // Duplicated with ProjectExtra. + private[sbt] object syntax { + implicit def richInitializeTask[T](init: Initialize[Task[T]]): Scoped.RichInitializeTask[T] = + new Scoped.RichInitializeTask(init) + + implicit def richInitializeInputTask[T]( + init: Initialize[InputTask[T]] + ): Scoped.RichInitializeInputTask[T] = + new Scoped.RichInitializeInputTask(init) + + implicit def richInitialize[T](i: Initialize[T]): Scoped.RichInitialize[T] = + new Scoped.RichInitialize[T](i) + } + /** * Wraps an [[sbt.Def.Initialize]] instance to provide `map` and `flatMap` semantics. */ diff --git a/sbt/src/sbt-test/actions/ifs/build.sbt b/sbt/src/sbt-test/actions/ifs/build.sbt new file mode 100644 index 000000000..c3758e0e0 --- /dev/null +++ b/sbt/src/sbt-test/actions/ifs/build.sbt @@ -0,0 +1,25 @@ +val condition = taskKey[Boolean]("") +val trueAction = taskKey[Unit]("") +val falseAction = taskKey[Unit]("") +val foo = taskKey[Unit]("") +val output = settingKey[File]("") + +lazy val root = (project in file(".")) + .settings( + name := "ifs", + output := baseDirectory.value / "output.txt", + condition := true, + trueAction := { IO.write(output.value, s"true\n", append = true) }, + falseAction := { IO.write(output.value, s"false\n", append = true) }, + foo := (Def.ifS(condition)(trueAction)(falseAction)).value, + TaskKey[Unit]("check") := { + val lines = IO.read(output.value).linesIterator.toList + assert(lines == List("true")) + () + }, + TaskKey[Unit]("check2") := { + val lines = IO.read(output.value).linesIterator.toList + assert(lines == List("false")) + () + }, + ) diff --git a/sbt/src/sbt-test/actions/ifs/test b/sbt/src/sbt-test/actions/ifs/test new file mode 100644 index 000000000..89589feca --- /dev/null +++ b/sbt/src/sbt-test/actions/ifs/test @@ -0,0 +1,7 @@ +> foo +> check + +$ delete output.txt +> set condition := false +> foo +> check2 diff --git a/tasks-standard/src/main/scala/sbt/Action.scala b/tasks-standard/src/main/scala/sbt/Action.scala index 603eb5fc3..56cd2214e 100644 --- a/tasks-standard/src/main/scala/sbt/Action.scala +++ b/tasks-standard/src/main/scala/sbt/Action.scala @@ -57,6 +57,31 @@ final case class Join[T, U](in: Seq[Task[U]], f: Seq[Result[U]] => Either[Task[T Join[T, U](in.map(g.fn[U]), sr => f(sr).left.map(g.fn[T])) } +/** + * A computation that conditionally falls back to a second transformation. + * This can be used to encode `if` conditions. + */ +final case class Selected[A, B](fab: Task[Either[A, B]], fin: Task[A => B]) extends Action[B] { + private def ml = AList.single[Either[A, B]] + type K[L[x]] = L[Either[A, B]] + + private[sbt] def mapTask(g: Task ~> Task) = + Selected[A, B](g(fab), g(fin)) + + /** + * Encode this computation as a flatMap. + */ + private[sbt] def asFlatMapped: FlatMapped[B, K] = { + val f: Either[A, B] => Task[B] = { + case Right(b) => std.TaskExtra.task(b) + case Left(a) => std.TaskExtra.singleInputTask(fin).map(_(a)) + } + FlatMapped[B, K](fab, { + f compose std.TaskExtra.successM + }, ml) + } +} + /** Combines metadata `info` and a computation `work` to define a task. */ final case class Task[T](info: Info[T], work: Action[T]) { override def toString = info.name getOrElse ("Task(" + info + ")") diff --git a/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala b/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala index d3fd9c700..8b0c3cc10 100644 --- a/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala +++ b/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala @@ -146,10 +146,7 @@ trait TaskExtra { def failure: Task[Incomplete] = mapFailure(idFun) def result: Task[Result[S]] = mapR(idFun) - // The "taskDefinitionKey" is used, at least, by the ".previous" functionality. - // But apparently it *cannot* survive a task map/flatMap/etc. See actions/depends-on. - private def newInfo[A]: Info[A] = - Info[A](AttributeMap(in.info.attributes.entries.filter(_.key.label != "taskDefinitionKey"))) + private def newInfo[A]: Info[A] = TaskExtra.newInfo(in.info) def flatMapR[T](f: Result[S] => Task[T]): Task[T] = Task(newInfo, new FlatMapped[T, K](in, f, ml)) @@ -285,5 +282,13 @@ object TaskExtra extends TaskExtra { def incompleteDeps(incs: Seq[Incomplete]): Incomplete = Incomplete(None, causes = incs) + def select[A, B](fab: Task[Either[A, B]], f: Task[A => B]): Task[B] = + Task(newInfo(fab.info), new Selected[A, B](fab, f)) + + // The "taskDefinitionKey" is used, at least, by the ".previous" functionality. + // But apparently it *cannot* survive a task map/flatMap/etc. See actions/depends-on. + private[sbt] def newInfo[A](info: Info[_]): Info[A] = + Info[A](AttributeMap(info.attributes.entries.filter(_.key.label != "taskDefinitionKey"))) + private[sbt] def existToAny(in: Seq[Task[_]]): Seq[Task[Any]] = in.asInstanceOf[Seq[Task[Any]]] } diff --git a/tasks-standard/src/main/scala/sbt/std/Transform.scala b/tasks-standard/src/main/scala/sbt/std/Transform.scala index ee024fd9a..f2259f3b3 100644 --- a/tasks-standard/src/main/scala/sbt/std/Transform.scala +++ b/tasks-standard/src/main/scala/sbt/std/Transform.scala @@ -46,6 +46,7 @@ object Transform { case Pure(eval, _) => uniform(Nil)(_ => Right(eval())) case m: Mapped[t, k] => toNode[t, k](m.in)(right ∙ m.f)(m.alist) case m: FlatMapped[t, k] => toNode[t, k](m.in)(left ∙ m.f)(m.alist) + case s: Selected[_, t] => val m = s.asFlatMapped; toNode(m.in)(left ∙ m.f)(m.alist) case DependsOn(in, deps) => uniform(existToAny(deps))(const(Left(in)) compose all) case Join(in, f) => uniform(in)(f) }