Composable Callbacks & Listeners

49
COMPOSABLE CALLBACKS & LISTENERS @OE_UIA

Transcript of Composable Callbacks & Listeners

Page 1: Composable Callbacks & Listeners

COMPOSABLE CALLBACKS & LISTENERS@OE_UIA

Page 2: Composable Callbacks & Listeners

Who am I ?Taisuke Oe (@OE_uia) https://github.com/taisukeoe/

AndroidアプリをScalaで作ってる人

ScalaMatsuri運営してます。

最近はND4s / DL4s contributor

Page 3: Composable Callbacks & Listeners

本日のテーマ

CallbackとListener

Page 4: Composable Callbacks & Listeners

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

Page 5: Composable Callbacks & Listeners

Listener

trait OnClickListener{ def onClick(b:Button):Unit } trait Button{ def setOnClickListener(l:OnClickListener):Unit }

* 何らかのイベントが発生する度に呼び出される処理 * イベントを発生させるオブジェクトに予め登録する * Non Blocking

Page 6: Composable Callbacks & Listeners

CallbackとListener

* 非同期でNon Blockingな処理をするのに便利な仕組み

* 便利故に、JavaやJavascriptのAPIには大量に溢れている

* 故に、複雑なイベント処理をしようとすると…

Page 7: Composable Callbacks & Listeners

_人人人人人人人人_ > 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] {

Page 8: Composable Callbacks & Listeners

* 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] {

Page 9: Composable Callbacks & Listeners

Callback地獄とは?

* Callback(やListener)の多重ネスト * ネストを外そうとCallbackをまとめると、似たような処理が繰り返されがちでDRYに保ちにくい

というジレンマ

* 非同期なJava APIにありがち * 特に、AndroidなどGUI / クライアント側アプリでありがち

Page 10: Composable Callbacks & Listeners

 再掲

Page 11: Composable Callbacks & Listeners

_人人人人人人人人_ > 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] {

Page 12: Composable Callbacks & Listeners

Callback地獄つらい

* 多重ネストつらい… * DRYじゃないのもつらい…

でも、本当に問題なのは何だろう?

Page 13: Composable Callbacks & Listeners

CallbackとListenerが composableじゃないこと

Page 14: Composable Callbacks & Listeners

合成可能にするための候補

Scala標準 Promise,Future

RxScala Observable

Scalaz Task

Scalaz 継続モナド(ContT)

Scalaz Freeモナド

Page 15: Composable Callbacks & Listeners

Composableな Callback/Listenerができれば

出来るだけ小さな単位でCallback/Listenerを定義

複雑なイベントは、Callback/Listenerのネストではなく合成で表現

(あと、failoverが楽だと良し)

Page 16: Composable Callbacks & Listeners

候補Callback Listener エラー処理 備考

Scala標準Future/Promise

RxScalaObservable

ScalazTask

ScalazContTScalazFree

Page 17: Composable Callbacks & Listeners

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オブジェクトを通じて値を書き込むことができる

Page 18: Composable Callbacks & Listeners

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 }

Page 19: Composable Callbacks & Listeners

Futureのエラー処理

val json = profileJsonFuture(“https://facebook.com/xxx") .recoverWith { case t =>

t.printStackTrace() profileJson("https://twitter.com/xxx") }

Page 20: Composable Callbacks & Listeners

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

Page 21: Composable Callbacks & Listeners

 再掲

Page 22: Composable Callbacks & Listeners

_人人人人人人人人_ > 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] {

Page 23: Composable Callbacks & Listeners

だいぶ楽になった…

でもここで一つ問題が

Page 24: Composable Callbacks & Listeners

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. */

Page 25: Composable Callbacks & Listeners

まとめCallback Listener エラー処理 備考

Scala標準Future/Promise

◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない

RxScalaObservable

ScalazTask

ScalazContTScalazFree

Page 26: Composable Callbacks & Listeners

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生成可能

Page 27: Composable Callbacks & Listeners

(余談)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

Page 28: Composable Callbacks & Listeners

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) } }) }

Page 29: Composable Callbacks & Listeners

Observableのエラー処理

val json:Observable[String] = profileJson(“https://facebook.com/xxx")   .onErrorResumeNext     { t =>  t.printStackTrace()  profileJson("https://twitter.com/xxx")   }  

Page 30: Composable Callbacks & Listeners

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

Page 31: Composable Callbacks & Listeners

RxScala Observableの メリット・デメリット

メリット

Callback, Listenerを統一的なインターフェースで扱える

ストリーム処理をしたくなっても、同じ型のまま扱える

デメリット

(少なくとも標準では)Monadではない。

(少なくとも標準には)MonadTransformerがない。

https://github.com/everpeace/rxscalaz

ObservableのMonadなどの型クラスインスタンス各種と、MonadTransformer有。

Page 32: Composable Callbacks & Listeners

まとめCallback Listener エラー処理 備考

Scala標準Future/Promise

◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない

RxScalaObservable ◯ ◯ ◯

Scala標準Futureと相互運用可能。

モナド化、モナドトランスフォーマー化可能。

ScalazTask

ScalazContTScalazFree

Page 33: Composable Callbacks & Listeners

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]などによるエラー処理

Page 34: Composable Callbacks & Listeners

Scalaz TaskScala標準のFutureとは違い、Taskインスタンスを生成してもrunAsyncなどを明示的に呼び出すまで計算されない

Task.forkにより明示的に異なる論理スレッドで実行可能

その他Scalazの便利関数が大量に。

参考: Scalaz Task - the missing documentation

http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/

Page 35: Composable Callbacks & Listeners

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]

Page 36: Composable Callbacks & Listeners

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() }

Page 37: Composable Callbacks & Listeners

まとめCallback Listener エラー処理 備考

Scala標準Future/Promise

◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない

RxScalaObservable ◯ ◯ ◯

Scala標準Futureと相互運用可能。

モナド化、モナドトランスフォーマー化可能。

ScalazTask ◯ ◯ ◯

ScalazContTScalazFree

Page 38: Composable Callbacks & Listeners

Scalaz ContTScalazの継続モナド(のMonad Transformer)

ある処理の後続の処理を継続(Continuation)として渡すスタイル(継続渡し、CPS)をモナド化したもの

ContT.apply[M[_],R,A](f:(A => M[R]) => M[R]) :ContT[M[_],R,A] で生成

エラー処理はM[_] (今回はFuture)に移譲

Page 39: Composable Callbacks & Listeners

Scalaz ContTPureScript作者Phil FreemanがCallback地獄をContTで解決する記事を書いている

原文

https://leanpub.com/purescript/read

日本語訳

http://hiruberuto.bitbucket.org/purescript/chapter12.html

Page 40: Composable Callbacks & Listeners

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(_))

Page 41: Composable Callbacks & Listeners

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) } }

Page 42: Composable Callbacks & Listeners

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) }

Page 43: Composable Callbacks & Listeners

まとめCallback Listener エラー処理 備考

Scala標準Future/Promise

◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない

RxScalaObservable ◯ ◯ ◯

Scala標準Futureと相互運用可能。

モナド化、モナドトランスフォーマー化可能。

ScalazTask ◯ ◯ ◯

ScalazContT ◯ ◯ △

ScalazFree

Page 44: Composable Callbacks & Listeners

Scalaz FreeScalazのFree

Functorをモナド化して扱うための仕組み

Coyonedaを使うと、1階のカインドの型をFunctor化できる

故に、1階のカインドの型をモナド化できる!(Operationalモナド)

… というのを、吉田さんが書いたサンプルを見て勉強しました

Page 45: Composable Callbacks & Listeners

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() }

Page 46: Composable Callbacks & Listeners

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

Page 47: Composable Callbacks & Listeners

まとめCallback Listener エラー処理 備考

Scala標準Future/Promise

◯ ☓ ◯ Scala標準なので、依存ライブラリが増えない

RxScalaObservable ◯ ◯ ◯

Scala標準Futureと相互運用可能。

モナド化、モナドトランスフォーマー化可能。

ScalazTask ◯ ◯ ◯

ScalazContT ◯ ◯ △

ScalazFree - - - interpreterの差し替えが

簡単

Page 48: Composable Callbacks & Listeners

まとめCallbackやListenerをモナドなどでcomposableにすると、DRYで再利用可能性が上がり使い勝手がよくなる

何を使うべきかは場合にもよるが、趣味も…?

ひとまずScala標準のFuture/PromiseでCallbackだけcomposableにしておいて、後で必要に応じてscalaz.ContT化するとか

単体で使うならscalaz.concurrent.TaskがCallbackとListenerを統一したインターフェースで扱えるので便利かなとか

今回使用したコードはこちら

https://github.com/taisukeoe/ScalaFPEvent

Page 49: Composable Callbacks & Listeners

Future Work

3つ以上関数をもつ、複雑なCallbackへの対応(Prism?)

Listenerで状態を扱えるようにする