composable callbacks & listeners
TRANSCRIPT
COMPOSABLE CALLBACKS & LISTENERS@OE_UIA
Who am I ?Taisuke Oe (@OE_uia) https://github.com/taisukeoe/
AndroidアプリをScalaで作ってる人
ScalaMatsuri運営してます。
最近はND4s / DL4s contributor
本日のテーマ
CallbackとListener
Callback
trait SimpleCallback[-T, -E <: Throwable] { def onSuccess(t: T): Unit
def onFailure(e: E): Unit }
trait SNSClient{ def getProfileAsync(url: String, callback: SimpleCallback[String, Exception]): Unit = ??? }
* 何らかのイベントの完了時に、(一般的には?)一度だけ呼び出される処理。
* 非同期な関数に引数として渡される * Non Blocking
Listener
trait OnClickListener{ def onClick(b:Button):Unit } trait Button{ def setOnClickListener(l:OnClickListener):Unit }
* 何らかのイベントが発生する度に呼び出される処理 * イベントを発生させるオブジェクトに予め登録する * Non Blocking
CallbackとListener
* 非同期でNon Blockingな処理をするのに便利な仕組み
* 便利故に、JavaやJavascriptのAPIには大量に溢れている
* 故に、複雑なイベント処理をしようとすると…
_人人人人人人人人_ > Callback地獄 < ‾Y^Y^Y^Y^Y^Y^Y‾
Button.setOnClickListener(new OnClickListener { override def onClick(b: Button): Unit = SNSClient.getProfileAsync("https://facebook.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] { override def onSuccess(profileUrl: String): Unit = SNSClient.getImageAsync(profileUrl, new SimpleCallback[Array[Byte], Exception] { override def onSuccess(t: Array[Byte]): Unit = println(t)
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = { e.printStackTrace()
SNSClient.getProfileAsync("https://twitter.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] {
* 1. Buttonがクリックされる * 2. ユーザーのSNSプロフィールのJSONを取得
* 2-2. 失敗後、他のSNSからJSON取得 * 3. JSONをParseして、プロフィール画像のURL取得 * 4. URLから画像データのByte列を取得
Button.setOnClickListener(new OnClickListener { override def onClick(b: Button): Unit = SNSClient.getProfileAsync("https://facebook.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] { override def onSuccess(profileUrl: String): Unit = SNSClient.getImageAsync(profileUrl, new SimpleCallback[Array[Byte], Exception] { override def onSuccess(t: Array[Byte]): Unit = println(t)
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = { e.printStackTrace()
SNSClient.getProfileAsync("https://twitter.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] {
Callback地獄とは?
* Callback(やListener)の多重ネスト * ネストを外そうとCallbackをまとめると、似たような処理が繰り返されがちでDRYに保ちにくい
というジレンマ
* 非同期なJava APIにありがち * 特に、AndroidなどGUI / クライアント側アプリでありがち
再掲
_人人人人人人人人_ > Callback地獄 < ‾Y^Y^Y^Y^Y^Y^Y‾
Button.setOnClickListener(new OnClickListener { override def onClick(b: Button): Unit = SNSClient.getProfileAsync("https://facebook.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] { override def onSuccess(profileUrl: String): Unit = SNSClient.getImageAsync(profileUrl, new SimpleCallback[Array[Byte], Exception] { override def onSuccess(t: Array[Byte]): Unit = println(t)
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = { e.printStackTrace()
SNSClient.getProfileAsync("https://twitter.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] {
Callback地獄つらい
* 多重ネストつらい… * DRYじゃないのもつらい…
でも、本当に問題なのは何だろう?
CallbackとListenerが composableじゃないこと
合成可能にするための候補
Scala標準 Promise,Future
RxScala Observable
Scalaz Task
Scalaz 継続モナド(ContT)
Scalaz Freeモナド
Composableな Callback/Listenerができれば
出来るだけ小さな単位でCallback/Listenerを定義
複雑なイベントは、Callback/Listenerのネストではなく合成で表現
(あと、failoverが楽だと良し)
候補Callback Listener エラー処理 備考
Scala標準Future/Promise
RxScalaObservable
ScalazTask
ScalazContTScalazFree
Scala標準のFuture
scala.concurrent.Future
非同期でNon-Blockingな処理を簡便に行うための便利ツール
Future[+T]#flatMap[S](f:T=>Future[S]):Future[S] により他のFuture同士と合成可能
flatMapなのでfor-comprehensionで合成を表現可能
エラー処理を簡便に行うための関数群(e.g. recoverWith, onFailure)
Future#applyで生成する他、Promiseオブジェクトを通じて値を書き込むことができる
CallbackをFuture化する
def profileImg(imgUrl: String): Future[Array[Byte]] = { val p = Promise[Array[Byte]]() val f = p.future SNSClient.getImageAsync(imgUrl, new SimpleCallback[Array[Byte], Exception] { override def onSuccess(imgData: Array[Byte]): Unit = p.success(imgData)
override def onFailure(e: Exception): Unit = p.failure(e) }) f }
Futureのエラー処理
val json = profileJsonFuture(“https://facebook.com/xxx") .recoverWith { case t =>
t.printStackTrace() profileJson("https://twitter.com/xxx") }
Future化したものを合成
val dataFuture: Future[Array[Byte]] = for { json <- profileJsonFuture(“https://facebook.com/xxx") .recoverWith { case t => t.printStackTrace() profileJsonFuture("https://twitter.com/xxx") } imgUrl <- parseFuture(json) data <- profileImgFuture(imgUrl) } yield data
再掲
_人人人人人人人人_ > Callback地獄 < ‾Y^Y^Y^Y^Y^Y^Y‾
Button.setOnClickListener(new OnClickListener { override def onClick(b: Button): Unit = SNSClient.getProfileAsync("https://facebook.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] { override def onSuccess(profileUrl: String): Unit = SNSClient.getImageAsync(profileUrl, new SimpleCallback[Array[Byte], Exception] { override def onSuccess(t: Array[Byte]): Unit = println(t)
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = e.printStackTrace() })
override def onFailure(e: Exception): Unit = { e.printStackTrace()
SNSClient.getProfileAsync("https://twitter.com/xxx", new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = SNSJSONParser.extractProfileUrlAsync(json, new SimpleCallback[String, Exception] {
だいぶ楽になった…
でもここで一つ問題が
Future / Promiseの注意点Promise Futureでも1度しか書き込めないため、複数回呼ばれうるListenerには使えない
def onClickFuture(button:Button): Future[Button] = { val p = Promise[Button]() val f = p.future button.setOnClickListener(new LoggingOnClickListener { override def onClick(b: Button): Unit = { super.onClick(b) p.success(b) } }) f }
val clickFuture:Future[Button] = onClickFuture(Button)
//clickFuture succeeds Button.click() Button.click()
/* [error] (run-main-2) java.lang.IllegalStateException: Promise already completed. */
まとめCallback Listener エラー処理 備考
Scala標準Future/Promise
◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない
RxScalaObservable
ScalazTask
ScalazContTScalazFree
RxScala Observable非同期なイベントストリームを扱うためのライブラリ
Listener及びCallbackを、イベントストリーム(Observable)に見立てる
Observable[+T]#flatMap[U](f:T=>Observable[U]):Observable[U]によりObservable同士で合成可能
onErrorResumeNext[T](f:Throwable => Observable[T]):Observable[T]で、エラー処理
Observable.from[T](f:Future[T]):Observable[T]で、FutureからObservable生成可能
(余談)ReactiveXのDocにも…Callbacks Have Their Own Problems
Callbacks solve the problem of premature blocking on Future.get() by not allowing anything to block. They are naturally efficient because they execute when the response is ready.
But as with Futures, while callbacks are easy to use with a single level of asynchronous execution, with nested composition they become unwieldy.
http://reactivex.io/intro.html
ListenerをObservable化
def onClickObs(button: Button): Observable[Button] = Observable { asSubscriber => button.setOnClickListener(new OnClickListener { override def onClick(b: Button): Unit = { super.onClick(b) asSubscriber.onNext(b) } }) }
Observableのエラー処理
val json:Observable[String] = profileJson(“https://facebook.com/xxx") .onErrorResumeNext { t => t.printStackTrace() profileJson("https://twitter.com/xxx") }
Observable同士を合成
val dataObservable: Observable[Array[Byte]] = for { _ <- onClick(Button) json <- Observable.from(profileJson(“https://facebook.com/xxx") .recoverWith { case t => t.printStackTrace() profileJson("https://twitter.com/xxx") }) imgUrl <- Observable.from(parse(json)) data <- Observable.from(profileImg(imgUrl)) } yield data
RxScala Observableの メリット・デメリット
メリット
Callback, Listenerを統一的なインターフェースで扱える
ストリーム処理をしたくなっても、同じ型のまま扱える
デメリット
(少なくとも標準では)Monadではない。
(少なくとも標準には)MonadTransformerがない。
https://github.com/everpeace/rxscalaz
ObservableのMonadなどの型クラスインスタンス各種と、MonadTransformer有。
まとめCallback Listener エラー処理 備考
Scala標準Future/Promise
◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない
RxScalaObservable ◯ ◯ ◯
Scala標準Futureと相互運用可能。
モナド化、モナドトランスフォーマー化可能。
ScalazTask
ScalazContTScalazFree
Scalaz Taskscalaz.concurrent.Task[+A]
Task.async[A](register: ((Throwable \/ A) => Unit) => Unit): Task[A]という、callbackをラップするための関数がある。Listenerについても使える。
非同期でNon-Blockingな処理を簡便に行うためのモナド
flatMap有り〼
handleWith[B>:A](f: PartialFunction[Throwable,Task[B]]):Task[B]などによるエラー処理
Scalaz TaskScala標準のFutureとは違い、Taskインスタンスを生成してもrunAsyncなどを明示的に呼び出すまで計算されない
Task.forkにより明示的に異なる論理スレッドで実行可能
その他Scalazの便利関数が大量に。
参考: Scalaz Task - the missing documentation
http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/
Scalaz Task化したCallback
def profileJsonTask(url: String): Task[String] = Task.async[String] { f => SNSClient.getProfileAsync(url, new SimpleCallback[String, Exception] { override def onSuccess(json: String): Unit = f(\/-(json))
override def onFailure(e: Exception): Unit = f(-\/(e)) }) }
dataTask.runAsync { case \/-(data) => println(data) case -\/(e) => e.printStackTrace() }
Task.async[A](register: ((Throwable \/ A) => Unit) => Unit): Task[A]
Task化したcallbackを合成 val dataTask: Task[Array[Byte]] = for { _ <- onClickTask(Button) json <- profileJsonTask(“https://facebook.com/xxx") .handleWith { case t => t.printStackTrace() profileJsonTask("https://twitter.com/xxx") } imgUrl <- parseTask(json) data <- profileImgTask(imgUrl) } yield data
dataTask.runAsync { case \/-(data) => println(data) case -\/(e) => e.printStackTrace() }
まとめCallback Listener エラー処理 備考
Scala標準Future/Promise
◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない
RxScalaObservable ◯ ◯ ◯
Scala標準Futureと相互運用可能。
モナド化、モナドトランスフォーマー化可能。
ScalazTask ◯ ◯ ◯
ScalazContTScalazFree
Scalaz ContTScalazの継続モナド(のMonad Transformer)
ある処理の後続の処理を継続(Continuation)として渡すスタイル(継続渡し、CPS)をモナド化したもの
ContT.apply[M[_],R,A](f:(A => M[R]) => M[R]) :ContT[M[_],R,A] で生成
エラー処理はM[_] (今回はFuture)に移譲
Scalaz ContTPureScript作者Phil FreemanがCallback地獄をContTで解決する記事を書いている
原文
https://leanpub.com/purescript/read
日本語訳
http://hiruberuto.bitbucket.org/purescript/chapter12.html
Listener/CallbackをContT化type Callback[T] = ContT[Future, Unit, T]
object Callback { def apply[T](f: (T => Future[Unit]) => Future[Unit]): Callback[T] = ContT.apply[Future, Unit, T](f) }
def onClickCont(button: Button): Callback[Button] = Callback { f => button.setOnClickListener(new LoggingOnClickListener { override def onClick(b: Button): Unit = { super.onClick(b) f(b) } }) Future.successful(Unit) }
import ScalaStdFutureExample._
def profileImgCont(imgUrl: String): Callback[Array[Byte]] = Callback(profileImgFuture(imgUrl).flatMap(_))
ContTのエラー処理
type Callback[T] = ContT[Future, Unit, T]
object Callback { def apply[T](f: (T => Future[Unit]) => Future[Unit]): Callback[T] = ContT.apply[Future, Unit, T](f) } def recoverCont[T](failedCont: Callback[T], recover: => Future[T]): Callback[T] = Callback { f => failedCont.run(f).recoverWith { case t => t.printStackTrace() recover.flatMap(f) } }
ContT化したCallbackを合成
val dataCont:Callback[Array[Byte]] = for { b <- onClickCont(Button) json <- recoverCont(profileJsonCont(“https://facebook.com/xxx"), profileJsonFuture("https://twitter.com/xxx")) imgUrl <- parseCont(json) data <- profileImgCont(imgUrl) } yield data
dataCont.run { ba => println(ba) Future.successful(Unit) }
まとめCallback Listener エラー処理 備考
Scala標準Future/Promise
◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない
RxScalaObservable ◯ ◯ ◯
Scala標準Futureと相互運用可能。
モナド化、モナドトランスフォーマー化可能。
ScalazTask ◯ ◯ ◯
ScalazContT ◯ ◯ △
ScalazFree
Scalaz FreeScalazのFree
Functorをモナド化して扱うための仕組み
Coyonedaを使うと、1階のカインドの型をFunctor化できる
故に、1階のカインドの型をモナド化できる!(Operationalモナド)
… というのを、吉田さんが書いたサンプルを見て勉強しました
Freeモナド化したCallbacksealed abstract class Program[A] extends Product with Serializable
final case class OnClick(button: Button) extends Program[Button]
final case class ProfileImage(imageUrl: String) extends Program[Array[Byte]]
final case class ProfileJson(url: String) extends Program[String]
final case class ParseJson(json: String) extends Program[String]
import ScalazTaskExample._
val interpreter: Program ~> Task = new (Program ~> Task) { override def apply[A](fa: Program[A]) = fa match { case OnClick(button) => onClickTask(button)
case ProfileImage(imageUrl) => profileImgTask(imageUrl)
case ProfileJson(url) => profileJsonTask(url)
case ParseJson(json) => parseTask(json) } }
val task: Task[String] = Free.runFC(liftFC(ProfileJson(“https://facebook.com/xxx"))(interpreter) task.runAsync { case \/-(data) => println(data) case -\/(e) => e.printStackTrace() }
Freeモナド化したCallbackのエラー処理def getTaskFrom(interpreter: Program ~> Task): Task[Array[Byte]] = for { json <- Free.runFC( for { _ <- liftFC(OnClick(Button)) json <- liftFC(ProfileJson("https://facebook.com/xxx")) } yield json )(interpreter).handleWith { case t => t.printStackTrace() profileJsonTask("https://twitter.com/xxx") } data <- Free.runFC( for { imgUrl <- liftFC(ParseJson(json)) dt <- liftFC(ProfileImage(imgUrl)) } yield dt )(interpreter) } yield data
まとめCallback Listener エラー処理 備考
Scala標準Future/Promise
◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない
RxScalaObservable ◯ ◯ ◯
Scala標準Futureと相互運用可能。
モナド化、モナドトランスフォーマー化可能。
ScalazTask ◯ ◯ ◯
ScalazContT ◯ ◯ △
ScalazFree - - - interpreterの差し替えが
簡単
まとめCallbackやListenerをモナドなどでcomposableにすると、DRYで再利用可能性が上がり使い勝手がよくなる
何を使うべきかは場合にもよるが、趣味も…?
ひとまずScala標準のFuture/PromiseでCallbackだけcomposableにしておいて、後で必要に応じてscalaz.ContT化するとか
単体で使うならscalaz.concurrent.TaskがCallbackとListenerを統一したインターフェースで扱えるので便利かなとか
今回使用したコードはこちら
https://github.com/taisukeoe/ScalaFPEvent
Future Work
3つ以上関数をもつ、複雑なCallbackへの対応(Prism?)
Listenerで状態を扱えるようにする