Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Commit

Permalink
! core: Refactor concurrency support
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
stanch committed May 1, 2015
1 parent 16cb506 commit f58c761
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 109 deletions.
37 changes: 18 additions & 19 deletions macroid-core/src/main/scala/macroid/Creatures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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)
}
}

Expand All @@ -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
}
}

Expand Down
46 changes: 22 additions & 24 deletions macroid-core/src/main/scala/macroid/Snailing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
24 changes: 12 additions & 12 deletions macroid-core/src/main/scala/macroid/Tweaking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}

Expand Down
23 changes: 14 additions & 9 deletions macroid-core/src/main/scala/macroid/UiActions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(())
Expand All @@ -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()))
}
77 changes: 39 additions & 38 deletions macroid-core/src/main/scala/macroid/UiThreading.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]]) =
Expand Down
15 changes: 8 additions & 7 deletions macroid-core/src/main/scala/macroid/util/Effector.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit f58c761

Please sign in to comment.