From f58c76119d00f0f1c98eccfbb43b0b2de3dc7dac Mon Sep 17 00:00:00 2001 From: Nick Stanchenko Date: Fri, 1 May 2015 15:32:52 +0100 Subject: [PATCH] ! core: Refactor concurrency support * Tweak#apply, Snail#apply and Transformer#apply now return UI actions. * Removed InPlaceFuture and (partly) incorporated the in-place optimizations into UiFuture. * Effector#foreach now takes a UI action. * Removed Ui#flatRun, added Ui#withResult and Ui#withResultAsync. * Taking advantage of all of the above, simplified Tweaking and Snailing. --- .../src/main/scala/macroid/Creatures.scala | 37 +++++---- .../src/main/scala/macroid/Snailing.scala | 46 ++++++----- .../src/main/scala/macroid/Tweaking.scala | 24 +++--- .../src/main/scala/macroid/UiActions.scala | 23 +++--- .../src/main/scala/macroid/UiThreading.scala | 77 ++++++++++--------- .../main/scala/macroid/util/Effector.scala | 15 ++-- 6 files changed, 113 insertions(+), 109 deletions(-) diff --git a/macroid-core/src/main/scala/macroid/Creatures.scala b/macroid-core/src/main/scala/macroid/Creatures.scala index e72abc9..ad2161a 100644 --- a/macroid-core/src/main/scala/macroid/Creatures.scala +++ b/macroid-core/src/main/scala/macroid/Creatures.scala @@ -5,18 +5,18 @@ import scala.concurrent.Future /** A Tweak is something that mutates a widget */ case class Tweak[-W <: View](f: W ⇒ Unit) { - def apply(w: W) = f(w) + def apply(w: W) = Ui(f(w)) /** Combine (sequence) with another tweak */ - def +[W1 <: W](that: Tweak[W1]): Tweak[W1] = Tweak { x ⇒ - this(x) - that(x) + def +[W1 <: W](that: Tweak[W1]) = Tweak[W1] { x ⇒ + this.f(x) + that.f(x) } /** Combine (sequence) with a snail */ - def ++[W1 <: W](that: Snail[W1]): Snail[W1] = Snail { x ⇒ - this(x) - that(x) + def ++[W1 <: W](that: Snail[W1]) = Snail[W1] { x ⇒ + this.f(x) + that.f(x) } } @@ -27,20 +27,18 @@ object Tweak { /** A snail mutates the view slowly (e.g. animation) */ case class Snail[-W <: View](f: W ⇒ Future[Unit]) { - import UiThreading._ - - def apply(w: W) = f(w) + def apply(w: W) = Ui(f(w)) /** Combine (sequence) with another snail */ - def ++[W1 <: W](that: Snail[W1]): Snail[W1] = Snail { x ⇒ + def ++[W1 <: W](that: Snail[W1]) = Snail[W1] { x ⇒ // make sure to keep the UI thread - this(x).flatMapUi(_ ⇒ Ui(that(x))) + this.f(x).flatMap(_ ⇒ that.f(x))(UiThreadExecutionContext) } /** Combine (sequence) with a tweak */ - def +[W1 <: W](that: Tweak[W1]): Snail[W1] = Snail { x ⇒ + def +[W1 <: W](that: Tweak[W1]) = Snail[W1] { x ⇒ // make sure to keep the UI thread - this(x).mapUi(_ ⇒ Ui(that(x))) + this.f(x).map(_ ⇒ that.f(x))(UiThreadExecutionContext) } } @@ -50,12 +48,13 @@ object Snail { } case class Transformer(f: PartialFunction[View, Ui[Any]]) { - def apply(w: View): Unit = { - f.lift.apply(w).foreach(_.get) - w match { - case Transformer.Layout(children @ _*) ⇒ children.foreach(apply) - case _ ⇒ () + def apply(w: View): Ui[Any] = { + val self = f.applyOrElse(w, Function.const(Ui.nop)) + val children = w match { + case Transformer.Layout(children @ _*) ⇒ Ui.sequence(children.map(apply): _*) + case _ ⇒ Ui.nop } + self ~ children } } diff --git a/macroid-core/src/main/scala/macroid/Snailing.scala b/macroid-core/src/main/scala/macroid/Snailing.scala index 974aa9e..b0cfb9a 100644 --- a/macroid-core/src/main/scala/macroid/Snailing.scala +++ b/macroid-core/src/main/scala/macroid/Snailing.scala @@ -14,57 +14,55 @@ trait CanSnail[W, S, R] { object CanSnail { import UiThreading._ - implicit def `Widget is snailable with Snail`[W <: View, S <: Snail[W]](implicit ec: ExecutionContext) = + implicit def `Widget is snailable with Snail`[W <: View, S <: Snail[W]](implicit ec: ExecutionContext): CanSnail[W, S, W] = new CanSnail[W, S, W] { - def snail(w: W, s: S) = Ui { s(w).map(_ ⇒ w) } + def snail(w: W, s: S) = s(w).withResultAsync(w) } - implicit def `Widget is snailable with Future[Tweak]`[W <: View, T <: Tweak[W]](implicit ec: ExecutionContext) = + implicit def `Widget is snailable with Future[Tweak]`[W <: View, T <: Tweak[W]](implicit ec: ExecutionContext): CanSnail[W, Future[T], W] = new CanSnail[W, Future[T], W] { def snail(w: W, ft: Future[T]) = Ui { - ft.mapInPlace { t ⇒ t(w); w }(UiThreadExecutionContext) + ft.mapUi(t ⇒ t(w).withResult(w)) } } - implicit def `Widget is snailable with Option`[W <: View, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]) = + implicit def `Widget is snailable with Option`[W <: View, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]): CanSnail[W, Option[S], W] = new CanSnail[W, Option[S], W] { def snail(w: W, o: Option[S]) = o.fold(Ui(Future.successful(w))) { s ⇒ - canSnail.snail(w, s).map(f ⇒ f.map(_ ⇒ w)) + canSnail.snail(w, s).withResultAsync(w) } } - implicit def `Ui is snailable`[W, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]) = + implicit def `Ui is snailable`[W, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]): CanSnail[Ui[W], S, W] = new CanSnail[Ui[W], S, W] { - def snail(ui: Ui[W], s: S) = ui flatMap { w ⇒ canSnail.snail(w, s).map(f ⇒ f.map(_ ⇒ w)) } + def snail(ui: Ui[W], s: S) = ui flatMap { w ⇒ canSnail.snail(w, s).withResultAsync(w) } } - implicit def `Option is snailable`[W, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]) = + implicit def `Option is snailable`[W, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]): CanSnail[Option[W], S, Option[W]] = new CanSnail[Option[W], S, Option[W]] { def snail(o: Option[W], s: S) = o.fold(Ui(Future.successful(o))) { w ⇒ - canSnail.snail(w, s).map(f ⇒ f.map(_ ⇒ o)) + canSnail.snail(w, s).withResultAsync(o) } } - implicit def `Widget is snailable with Future`[W <: View, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]) = + implicit def `Widget is snailable with Future`[W <: View, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]): CanSnail[W, Future[S], W] = new CanSnail[W, Future[S], W] { - // we can call Ui.get, since we are already inside the UI thread def snail(w: W, f: Future[S]) = Ui { - f.flatMapInPlace(s ⇒ canSnail.snail(w, s).get.map(_ ⇒ w))(UiThreadExecutionContext) + f.flatMapUi(s ⇒ canSnail.snail(w, s).withResultAsync(w)) } } - implicit def `Future is snailable`[W, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]) = + implicit def `Future is snailable`[W, S, R](implicit ec: ExecutionContext, canSnail: CanSnail[W, S, R]): CanSnail[Future[W], S, W] = new CanSnail[Future[W], S, W] { - // we can call Ui.get, since we are already inside the UI thread def snail(f: Future[W], s: S) = Ui { - f.flatMapInPlace(w ⇒ canSnail.snail(w, s).get.map(_ ⇒ w))(UiThreadExecutionContext) + f.flatMapUi(w ⇒ canSnail.snail(w, s).withResultAsync(w)) } } - implicit def `Widget is snailable with List`[W <: View, S, R](implicit canSnail: CanSnail[W, S, R]) = - new CanSnail[W, List[S], W] { - def snail(w: W, l: List[S]) = Ui(async { - val it = l.iterator + implicit def `Widget is snailable with TraversableOnce`[W <: View, S, R](implicit canSnail: CanSnail[W, S, R]): CanSnail[W, TraversableOnce[S], W] = + new CanSnail[W, TraversableOnce[S], W] { + def snail(w: W, l: TraversableOnce[S]) = Ui(async { + val it = l.toIterator while (it.hasNext) { // we can call Ui.get, since we are already inside the UI thread await(canSnail.snail(w, it.next()).get) @@ -73,10 +71,10 @@ object CanSnail { }(UiThreadExecutionContext)) } - implicit def `List is snailable`[W, S, R](implicit canSnail: CanSnail[W, S, R]) = - new CanSnail[List[W], S, List[W]] { - def snail(l: List[W], s: S) = Ui(async { - val it = l.iterator + implicit def `TraversableOnce is snailable`[W, S, R, C[X] <: TraversableOnce[X]](implicit canSnail: CanSnail[W, S, R]): CanSnail[C[W], S, C[W]] = + new CanSnail[C[W], S, C[W]] { + def snail(l: C[W], s: S) = Ui(async { + val it = l.toIterator while (it.hasNext) { // we can call Ui.get, since we are already inside the UI thread await(canSnail.snail(it.next(), s).get) diff --git a/macroid-core/src/main/scala/macroid/Tweaking.scala b/macroid-core/src/main/scala/macroid/Tweaking.scala index 3efd724..68372eb 100644 --- a/macroid-core/src/main/scala/macroid/Tweaking.scala +++ b/macroid-core/src/main/scala/macroid/Tweaking.scala @@ -11,34 +11,34 @@ trait CanTweak[W, T, R] { } object CanTweak { - implicit def `Widget is tweakable with Tweak`[W <: View, T <: Tweak[W]] = + implicit def `Widget is tweakable with Tweak`[W <: View, T <: Tweak[W]]: CanTweak[W, T, W] = new CanTweak[W, T, W] { - def tweak(w: W, t: T) = Ui { t(w); w } + def tweak(w: W, t: T) = t(w).withResult(w) } - implicit def `Widget is tweakable with Snail`[W <: View, S <: Snail[W]] = + implicit def `Widget is tweakable with Snail`[W <: View, S <: Snail[W]]: CanTweak[W, S, W] = new CanTweak[W, S, W] { - def tweak(w: W, s: S) = Ui { s(w); w } + def tweak(w: W, s: S) = s(w).withResult(w) } - implicit def `Layout is tweakable with Transformer`[L <: ViewGroup] = + implicit def `Layout is tweakable with Transformer`[L <: ViewGroup]: CanTweak[L, Transformer, L] = new CanTweak[L, Transformer, L] { - def tweak(l: L, t: Transformer) = Ui { t(l); l } + def tweak(l: L, t: Transformer) = t(l).withResult(l) } - implicit def `Widget is tweakable with Effector`[W <: View, F[+_], T, R](implicit effector: Effector[F], canTweak: CanTweak[W, T, R]) = + implicit def `Widget is tweakable with Effector`[W <: View, F[+_], T, R](implicit effector: Effector[F], canTweak: CanTweak[W, T, R]): CanTweak[W, F[T], W] = new CanTweak[W, F[T], W] { - def tweak(w: W, f: F[T]) = Ui { effector.foreach(f)(t ⇒ canTweak.tweak(w, t).run); w } + def tweak(w: W, f: F[T]) = Ui { effector.foreach(f)(t ⇒ canTweak.tweak(w, t)); w } } - implicit def `Effector is tweakable`[W, F[+_], T, R](implicit effector: Effector[F], canTweak: CanTweak[W, T, R]) = + implicit def `Effector is tweakable`[W, F[+_], T, R](implicit effector: Effector[F], canTweak: CanTweak[W, T, R]): CanTweak[F[W], T, F[W]] = new CanTweak[F[W], T, F[W]] { - def tweak(f: F[W], t: T) = Ui { effector.foreach(f)(w ⇒ canTweak.tweak(w, t).run); f } + def tweak(f: F[W], t: T) = Ui { effector.foreach(f)(w ⇒ canTweak.tweak(w, t)); f } } - implicit def `Ui is tweakable`[W, T, R](implicit canTweak: CanTweak[W, T, R]) = + implicit def `Ui is tweakable`[W, T, R](implicit canTweak: CanTweak[W, T, R]): CanTweak[Ui[W], T, W] = new CanTweak[Ui[W], T, W] { - def tweak(ui: Ui[W], t: T) = ui flatMap { w ⇒ canTweak.tweak(w, t).map(_ ⇒ w) } + def tweak(ui: Ui[W], t: T) = ui flatMap { w ⇒ canTweak.tweak(w, t).withResult(w) } } } diff --git a/macroid-core/src/main/scala/macroid/UiActions.scala b/macroid-core/src/main/scala/macroid/UiActions.scala index 01e273e..c5cb42c 100644 --- a/macroid-core/src/main/scala/macroid/UiActions.scala +++ b/macroid-core/src/main/scala/macroid/UiActions.scala @@ -2,21 +2,29 @@ package macroid import android.os.Looper -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.util.{ Failure, Success, Try } /** A UI action that can be sent to the UI thread for execution */ -class Ui[+A](v: () ⇒ A) { +class Ui[+A](private[Ui] val v: () ⇒ A) { import macroid.UiThreading._ /** map combinator */ def map[B](f: A ⇒ B) = Ui(f(v())) /** flatMap combinator */ - def flatMap[B](f: A ⇒ Ui[B]) = Ui(f(v()).get) + def flatMap[B](f: A ⇒ Ui[B]) = Ui(f(v()).v()) + + /** Replace the resulting value with a new one */ + def withResult[B](result: B) = Ui { v(); result } + + /** Wait until this action is finished and replace the resulting value with a new one */ + def withResultAsync[B, C](result: B)(implicit evidence: A <:< Future[C], ec: ExecutionContext) = Ui { + evidence(v()) map (_ ⇒ result) + } /** Combine (sequence) with another UI action */ - def ~[B](next: ⇒ Ui[B]) = Ui { v(); next.get } + def ~[B](next: ⇒ Ui[B]) = Ui { v(); next.v() } /** Wait until this action is finished and combine (sequence) it with another one */ def ~~[B, C](next: ⇒ Ui[B])(implicit evidence: A <:< Future[C]) = Ui { @@ -33,15 +41,12 @@ class Ui[+A](v: () ⇒ A) { Future(v())(UiThreadExecutionContext) } - /** Run the action on the UI thread, flattening the Future it returns */ - def flatRun[B](implicit evidence: A <:< Future[B]) = Future.successful(()).flatMapUi(_ ⇒ Ui(evidence(v()))) - /** Get the result of executing the action on the current (hopefully, UI!) thread */ def get = v() } object Ui { - private lazy val uiThread = Looper.getMainLooper.getThread + private[macroid] lazy val uiThread = Looper.getMainLooper.getThread /** A UI action that does nothing */ def nop = Ui(()) @@ -50,5 +55,5 @@ object Ui { def apply[A](v: ⇒ A) = new Ui(() ⇒ v) /** Combine (sequence) several UI actions together */ - def sequence[A](vs: Ui[A]*) = Ui(vs.map(_.get)) + def sequence[A](vs: Ui[A]*) = Ui(vs.map(_.v())) } diff --git a/macroid-core/src/main/scala/macroid/UiThreading.scala b/macroid-core/src/main/scala/macroid/UiThreading.scala index 7c8c855..eed8cb0 100644 --- a/macroid-core/src/main/scala/macroid/UiThreading.scala +++ b/macroid-core/src/main/scala/macroid/UiThreading.scala @@ -23,52 +23,53 @@ private[macroid] trait UiThreading { /** Get the result of executing UI code on the current (hopefully, UI!) tread */ def getUi[A](ui: Ui[A]) = ui.get - /** Helpers to perform Future callbacks in-place if the future is already completed */ - implicit class InPlaceFuture[T](future: Future[T]) { - /** Same as map, but performed on the current thread (if completed) */ - def mapInPlace[S](f: Function[T, S])(implicit ec: ExecutionContext) = if (future.isCompleted) { - future.value.get match { - case Success(x) ⇒ Future.successful(f(x)) - case Failure(t) ⇒ Future.failed(t) - } - } else { - future.map(f) - } - - /** Same as flatMap, but performed on the current thread (if completed) */ - def flatMapInPlace[S](f: Function[T, Future[S]])(implicit ec: ExecutionContext) = if (future.isCompleted) { - future.value.get match { - case Success(x) ⇒ f(x) - case Failure(t) ⇒ Future.failed(t) - } - } else { - future.flatMap(f) - } - - /** Same as foreach, but performed on the current thread (if completed) */ - def foreachInPlace[U](f: Function[T, U])(implicit ec: ExecutionContext) = if (future.isCompleted) { - future.value.get.foreach(f) - } else { - future.foreach(f) - } - } - /** Helpers to run UI actions as Future callbacks */ implicit class UiFuture[T](future: Future[T]) { - private def applyUi[A, B](f: Function[A, Ui[B]]) = f andThen (_.get) + private def applyUi[A, B](f: Function[A, Ui[B]]): Function[A, B] = x ⇒ f(x).get private def partialApplyUi[A, B](f: PartialFunction[A, Ui[B]]) = f andThen (_.get) - /** Same as map, but performed on the UI thread */ + /** Same as map, but performed on the UI thread. + * + * If the future is already completed and the current thread is the UI thread, + * the UI action will be applied in-place, rather than asynchronously. + */ def mapUi[S](f: Function[T, Ui[S]]) = - future.map(applyUi(f))(UiThreadExecutionContext) + if (future.isCompleted && Ui.uiThread == Thread.currentThread) { + future.value.get.map(applyUi(f)) match { + case Success(x) ⇒ Future.successful(x) + case Failure(t) ⇒ Future.failed(t) + } + } else { + future.map(applyUi(f))(UiThreadExecutionContext) + } - /** Same as flatMap, but performed on the UI thread */ - def flatMapUi[S](f: Function[T, Ui[Future[S]]]) = - future.flatMap(applyUi(f))(UiThreadExecutionContext) + /** Same as flatMap, but performed on the UI thread + * + * If the future is already completed and the current thread is the UI thread, + * the UI action will be applied in-place, rather than asynchronously. + */ + def flatMapUi[S](f: Function[T, Ui[Future[S]]]) = { + if (future.isCompleted && Ui.uiThread == Thread.currentThread) { + future.value.get.map(applyUi(f)) match { + case Success(x) ⇒ x + case Failure(t) ⇒ Future.failed(t) + } + } else { + future.flatMap(applyUi(f))(UiThreadExecutionContext) + } + } - /** Same as foreach, but performed on the UI thread */ + /** Same as foreach, but performed on the UI thread + * + * If the future is already completed and the current thread is the UI thread, + * the UI action will be applied in-place, rather than asynchronously. + */ def foreachUi[U](f: Function[T, Ui[U]]) = - future.foreach(applyUi(f))(UiThreadExecutionContext) + if (future.isCompleted && Ui.uiThread == Thread.currentThread) { + future.value.get.foreach(applyUi(f)) + } else { + future.foreach(applyUi(f))(UiThreadExecutionContext) + } /** Same as recover, but performed on the UI thread */ def recoverUi[U >: T](pf: PartialFunction[Throwable, Ui[U]]) = diff --git a/macroid-core/src/main/scala/macroid/util/Effector.scala b/macroid-core/src/main/scala/macroid/util/Effector.scala index 3cbf036..ade24c0 100644 --- a/macroid-core/src/main/scala/macroid/util/Effector.scala +++ b/macroid-core/src/main/scala/macroid/util/Effector.scala @@ -1,30 +1,31 @@ package macroid.util +import macroid.Ui +import macroid.UiThreading.UiFuture + import scala.language.higherKinds import scala.concurrent.{ Future, ExecutionContext } import scala.util.Try trait Effector[-F[_]] { - def foreach[A](fa: F[A])(f: A ⇒ Any): Unit + def foreach[A](fa: F[A])(f: A ⇒ Ui[Any]): Unit } object Effector { implicit object `TraversableOnce is Effector` extends Effector[TraversableOnce] { - override def foreach[A](fa: TraversableOnce[A])(f: A ⇒ Any): Unit = fa.foreach(f) + override def foreach[A](fa: TraversableOnce[A])(f: A ⇒ Ui[Any]): Unit = fa.foreach(a ⇒ f(a).run) } implicit object `Option is Effector` extends Effector[Option] { - def foreach[A](fa: Option[A])(f: A ⇒ Any) = fa.foreach(f) + def foreach[A](fa: Option[A])(f: A ⇒ Ui[Any]) = fa.foreach(a ⇒ f(a).run) } implicit object `Try is Effector` extends Effector[Try] { - def foreach[A](fa: Try[A])(f: A ⇒ Any) = fa.foreach(f) + def foreach[A](fa: Try[A])(f: A ⇒ Ui[Any]) = fa.foreach(a ⇒ f(a).run) } implicit def `Future is Effector`(implicit ec: ExecutionContext) = new Effector[Future] { - import macroid.UiThreading.InPlaceFuture - - def foreach[A](fa: Future[A])(f: A ⇒ Any) = fa.foreachInPlace(f) + def foreach[A](fa: Future[A])(f: A ⇒ Ui[Any]) = fa.mapUi(f) } }