composable callbacks & listeners

Post on 14-Aug-2015

870 Views

Category:

Software

2 Downloads

Preview:

Click to see full reader

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で状態を扱えるようにする

top related