Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Android

Functional Android

Jorge Castillo

January 13, 2021
Tweet

More Decks by Jorge Castillo

Other Decks in Programming

Transcript

  1. Why FP? Achieve determinism to ght race conditions. Pure functions

    improve code reasoning. Keep side e ects under control.
  2. Concern separation Staying declarative and deferred. Memory representation of the

    program (algebras). Decoupled runtime - optimizations. Simple example: Kotlin Sequences terminal ops to consume - toList()
  3. Compose Also applies concern separation Creates an in-memory representation of

    the UI tree (Slot Table) The runtime interprets it executes it runtime optimizations. (Run composable functions in parallel, in di erent order, smart recomposition...).
  4. Composable functions Similar to suspend functions Description of an e

    ect (UI). Callable from within other composable functions or a prepared environment integration point setContent {} Ensures Composer is implicitly passed. Makes the e ect compile time tracked.
  5. Suspend functions Note the similarity. Description of an e ect

    (Not only UI). Callable from within other suspend functions or a prepared environment integration point coroutine. Ensures Continuation is implicitly passed. Makes the e ect compile time tracked.
  6. Flag e ects as suspend Make 'em pure! e ects

    description of e ects interface UserService { - fun loadUser(): User + suspend fun loadUser(): User } class UserPersistence { - fun loadUser(): User = TODO() + suspend fun loadUser(): User = TODO() } class AnalyticsTracker { - fun trackEvent(event: Event): Unit = TODO() + suspend fun trackEvent(event: Event): Unit = TODO() }
  7. But we'll need a runtime Every suspended program requires an

    environment (runtime) to run. Crawls the call stack up until the integration point create coroutine.
  8. Environment in KotlinX KotlinX Coroutines builders launch, async. class MyFragment:

    Fragment() { override fun onViewCreated(...) { /* ... */ viewLifecycleOwner.lifecycleScope.launch { // suspended program } } }
  9. Environment in FP Arrow Fx Coroutines Environment. Takes care of

    the execution strategy / context to run the program. class MyFragment: Fragment() { override fun onViewCreated(...) { /* ... */ val env = Environment() val cancellable = env.unsafeRunAsyncCancellable( { /* suspended program */ }, { e -> /* handle errors unhandled by the program */ }, { a -> /* handle result of the program */ } ) } }
  10. App entry points Also called "edge of the world". Android

    no suspend entry points. Inversion of control. Lifecycle callbacks entry points to hook logic.
  11. The suspended program Or in other words, our pure logics.

    Leverage data types to raise concerns over the data. Either<L, R> will be our friend
  12. Railway oriented programming By Scott Wlaschin from 11 May 2013

    Post Talk video + slides fsharpforfunandpro t.com/posts/recipe-part2/ fsharpforfunandpro t.com/rop/
  13. Either<L, R> A path we want to follow, vs an

    "alternative" one Compute over the happy path plug error handlers. Make disjunction explicit both paths need to be handled.
  14. In code Either<A, B> models this scenario. Convention: Errors on

    Left, success on the Right. Biased towards the Right side compute over the happy path. sealed class Either<out A, out B> { data class Left<out A>(val a: A) : Either<A, Nothing>() data class Right<out B>(val b: B) : Either<Nothing, B>() // operators like map, flatMap, fold, mapLeft... }
  15. fold to handle both sides fun loadUser: Either<UserNotFound, User> =

    Right(User("John")) // or Left(UserNotFound) // Alternatively: user.right() or exception.left() val user: Either<UserNotFound, User> = loadUser().fold( ifLeft = { e -> handleError(e) }, ifRight = { user -> render(user) } )
  16. Nullable data Option<A> getting deprecated. Alternative 1: A? Alternative 2:

    Either<Unit, A> typealias EpisodeNotFound = Unit fun EpisodeDB.loadEpisode(episodeId: String): Either<EpisodeNotFound, List<Character>> = Either.fromNullable(findEpisode("id1")) .map { episode -> episode.characters }
  17. Integration with 3rd parties Either.catch to capture suspended e ects.

    Use mapLeft to strongly type domain errors. Either#catch is meant to run e ects suspended. suspend fun loadSpeakers(): Either<Errors, List<Speaker>> = Either.catch { service.loadSpeakers() } // any suspended op .mapLeft { it.toDomainError() } // strongly type errors
  18. Composing logics We got means to write our logic as

    pure functions. We need the glue for them flatMap. Programs sequence of computations. How does flatMap work for Either?
  19. Failing fast When two operations are dependent, you cannot perform

    the second one without a successful result by the rst one. This means we can save computation time in that case. Either#flatMap sequential.
  20. Sequential e ects 2 dependent operations. suspend fun loadSpeaker(id: SpeakerId):

    Either<SpeakerNotFound, Speaker> = TODO() suspend fun loadTalks(ids: List<TalkId>): Either<InvalidIds, List<Talk>> = TODO() suspend fun main() { val talks = loadSpeaker("SomeId") .flatMap { loadTalks(it.talkIds) } // listOf(Talk(...), Talk(...), Talk(...)) }
  21. Sequential e ects - bindings Alternative syntax Either bindings sugar

    suspend fun main() { val talks = either { // Either<Error, List<Talk>> val speaker = !loadSpeaker("SomeId") val talks = !loadTalks(speaker.talkIds) talks } // listOf(Talk(...), Talk(...), Talk(...)) } //sampleEnd
  22. Fail fast First operation fails short circuits suspend fun main()

    { val events = either { val speaker = !loadSpeaker("SomeId") // Left(SpeakerNotFound)! val talks = !loadTalks(speaker.talkIds) val events = talks.map { !loadEvent(it.event) } events } println(events) // Left(SpeakerNotFound) }
  23. Error accumulation? Interested in all errors occurring, not a single

    one. Only in the context of independent computations.
  24. Validated & ValidatedNel ValidatedNel alias for error accumulation on a

    NonEmptyList. sealed class Validated<out E, out A> { data class Valid<out A>(val a: A) : Validated<Nothing, A>() data class Invalid<out E>(val e: E) : Validated<E, Nothing>() } typealias ValidatedNel<E, A> = Validated<NonEmptyList<E>, A>
  25. Validated & ValidatedNel Independent data validation with the applicative. suspend

    fun loadSpeaker(id: SpeakerId): ValidatedNel<SpeakerNotFound, Speaker> = Validated.catchNel { throw Exception("Boom !!") }.mapLeft { SpeakerNotFound.nel() } suspend fun loadEvents(ids: List<TalkId>): ValidatedNel<InvalidIds, List<Event>> = Validated.catchNel { throw Exception("Boom !!") }.mapLeft { InvalidIds.nel() } suspend fun main() { val accumulator = NonEmptyList.semigroup<Error>() val res = Validated.applicative(accumulator) .tupledN( loadSpeaker("SomeId"), loadEvents(listOf("1", "2"))
  26. Limitations Either or Validated are eager. We want them deferred

    declarative. suspend will do the work But what about threading / concurrency?
  27. Arrow Fx Coroutines Functional concurrency framework. Functional operators to run

    suspended e ects. Cancellation system ✅ All Arrow Fx operators automatically check for cancellation.
  28. Environment Our runtime. Picks the execution strategy. interface to implement

    custom ones. // synchronous env.unsafeRunSync { greet() } // asynchronous env.unsafeRunAsync( fa = { greet() }, e = { e -> println(e)}, a = { a -> println(a) } ) // cancellable asynchronous val disposable = env.unsafeRunAsyncCancellable( fa = { greet() },
  29. evalOn(ctx) O oad an e ect to an arbitrary context

    and get back to the original one. suspend fun loadTalks(ids: List<TalkId>): Either<Error.TalksNotFound, List<Talk>> = evalOn(IOPool) { // supports any suspended effects Either.catch { fetchTalksFromNetwork() } .mapLeft { Error.TalksNotFound } }
  30. parMapN / parTupledN Run N parallel e ects. Cancel parent

    cancels all children. Child failure cancels other children All results are required. suspend fun loadEvent(): Event { val op1 = suspend { loadSpeakers() } val op2 = suspend { loadRooms() } return parMapN(op1, op2) { speakers, rooms -> Event(speakers, rooms) } // val res: Tuple2<List<Speaker>, List<Room>> = parTupledN(op1, op2) }
  31. parTraverse / parSequence Traverses a dynamic amount of elements running

    an e ect for each, all of them in parallel. Cancellation works the same way. suspend fun loadEvents() { val eventIds = listOf(1, 2, 3) return eventIds.parTraverse(IOPool) { id -> eventService.loadEvent(id) } // val ops = listOf( // suspend { service.loadTalks(eventId1) }, // suspend { service.loadTalks(eventId2) }, // suspend { service.loadTalks(eventId3) }) // ops.parSequence()
  32. raceN Racing parallel e ects. Returns the winner, cancels losers.

    Cancelling parent cancels all children. Child failure cancels other children. suspend fun main() { val res = raceN(::op1, ::op2, ::op3) // suspended ops res.fold( ifA = {}, ifB = {}, ifC = {} ) }
  33. Android use case Racing against the Android lifecycle suspend fun

    AppCompatActivity.suspendUntilDestroy() = suspendCoroutine<Unit> { cont -> val lifecycleObserver = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroyListener() { cont.resumeWith(Result.success(Unit)) } } this.lifecycle.addObserver(lifecycleObserver) } suspend fun longRunningComputation(): Int = evalOn(IOPool) { delay(5000)
  34. Retrying / repeating Highly composable retry policies for suspended e

    ects. fun <A> complexPolicy() = Schedule.exponential<A>(10.milliseconds) .whileOutput { it.seconds < 60.seconds } .andThen(spaced<A>(60.seconds) and recurs(100)) suspend fun loadTalk(id: TalkId): List<Talks> = retry(complexPolicy()) { fetchTalk(id) // retry any suspended effect }
  35. Concurrent Error handling All Arrow Fx Coroutines operators rethrow on

    failure. Can use Either.catch, Validated.catch, Validated.catchNel, at any level ✨
  36. FRP? Android apps as a combination of Streams. Unidirectional data

    ow architectures. Lifecycle events, user interactions, application state updates...
  37. What we need Emit multiple times. Evaluate suspended e ects

    emit result over the Stream. Compatible with all Arrow Fx Coroutines operators. Cold streams purity declarative. Composition.
  38. Embedding e ects Evaluates a suspended e ect, emits result.

    Cold. Describes what will happen when the stream is interpreted. Terminal operator to run it. Errors raised into the Stream. val s = Stream.effect { loadUser(id) } .flatMap {} .map {} ... s.drain() // consume stream // Run!
  39. Embedding e ects Any Arrow Fx Coroutines operators can be

    evaluated. Result is emitted over the Stream. Threading via Arrow Fx Coroutines: parMapN, parTupledN, evalOn, parTraverse, parSequence... etc. val s = Stream.effect { // any suspended effect parMapN(op1, op2, op3) { speakers, rooms, venues -> Event(speakers, rooms, venues) } } s.drain()
  40. parJoin Composing streams in parallel. Concurrently emits values as they

    come unexpected order. val s1 = Stream.effect { 1 } val s2 = Stream.effect { 2 } val s3 = Stream.effect { 3 } val program = Stream(s1, s2, s3).parJoinUnbounded() // or parJoin(maxOpen = 3) val res = program.toList() println(res) // [2, 1, 3]
  41. Cancellable async wrapper Wrap callback based apis in a cancellable

    Stream. Return a CancelToken to avoid leaks. fun SwipeRefreshLayout.refreshes(): Stream<Unit> = Stream.cancellable { val listener = OnRefreshListener { emit(Unit) } [email protected](listener) // Return a cancellation token CancelToken { [email protected](listener) } }
  42. bracket Scope resources to the Stream life span. Calls release

    lambda once the Stream terminates. Stream.bracket({ openFile() }, { closeFile() }) .effectMap { canWorkWithFile() } .handleErrorWith { alternativeResult() } .drain()
  43. Other relevant operators The usual ones. Stays declarative and deferred

    until drain() Stream.effect { loadSpeakers() } .handleErrorWith { Stream.empty() } .effectMap { loadTalks(it.map { it.id }) } // flatMap + effect .map { talks -> talks.map { it.id } } .drain() // terminal - suspend
  44. interruptWhen + lifecycle Arbitrary Stream interruption by racing streams. Will

    terminate your program as soon as a lifecycle ON_DESTROY event is emited. program() .interruptWhen(lifecycleDestroy()) // races both .drain()
  45. interruptWhen + lifecycle Stream out of lifecycle events destroy fun

    Fragment.lifecycleDestroy(): Stream<Boolean> = Stream.callback { viewLifecycleOwner.lifecycle.addObserver( LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_DESTROY) { emit(true) } }) }
  46. Consuming streams safely Terminal ops are suspend Stream has to

    run within a safe environment. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { val env = Environment() env.unsafeRunAsync { HomeDependencies.program() // program as a Stream .interruptWhen(lifecycleDestroy()) .drain() // suspended - terminal op to consume } } }
  47. Thank you! @JorgeCastilloPr To expand on these ideas Fully- edged

    Functional Android course. Bookable as a group / company. www.47deg.com/trainings/Functional-Android-development/