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

An Arrow to the Kotlin

An Arrow to the Kotlin

Kotlin has been a revolution in the past two years, especially in the Android world which were using outdated versions of Java.

The language leap is so high, that now we are even allowed to put some Functional Programming in the mix.

This is being done through Arrow project, a FP typed library for Kotlin (and compatible with Java)

But, instead of doing a theoretical introduction to FP principles, we will introduce how we can complement our classic OOP with FP structures and patterns Arrow provides in certain practical problems, and how can we apply them to have more concise and robust code.

This way, as developers we can set a small foothold on the FP world and expand the range of tools we have at our disposal.

Saúl Díaz

October 20, 2018
Tweet

More Decks by Saúl Díaz

Other Decks in Programming

Transcript

  1. OOP was born on the premise that using concepts we

    intuitively know, we could split code better 5
  2. THIS IS READABLE fun printAllAttendees(conference: Conference): List<String> { val attendees

    = HashSet<Attendee>() for (talk in conference.talks) { for (attendee in talk.attendees) { attendees += attendee } } val names = ArrayList<String>() for (attendee in attendees) { names.add( "${attendee.id} - ${attendee.name}") } return names } Borrowed from @pacoworks’ “State of fuctional Kotlin ecosystem 2018” "https://youtu.be/q_1xPYQLyaU" 6
  3. THIS IS READABLE FAMILIAR fun printAllAttendees(conference: Conference): List<String> { val

    attendees = HashSet<Attendee>() for (talk in conference.talks) { for (attendee in talk.attendees) { attendees += attendee } } val names = ArrayList<String>() for (attendee in attendees) { names.add( "${attendee.id} - ${attendee.name}") } return names } Borrowed from @pacoworks’ “State of fuctional Kotlin ecosystem 2018” "https://youtu.be/q_1xPYQLyaU" 7
  4. THIS CAN BE ALSO FAMILIAR fun printParticipants(conference: Conference): List<String> {

    return conference.talks.map { talk -> talk.attendees } .flatten() .toSet() .map { attendee -> "${attendee.id} - ${attendee.name}" } } 9
  5. WATCH OUT FOR MUTABILITY fun `problems_of_mutability`() { val origin =

    mutableListOf("0") mutability(origin) // Whoops, I accidentally cleaned origin too :( } fun mutability(origin: MutableList<String>) { val shallow = origin // Do whatever shallow.clear() } 13
  6. WATCH OUT FOR MUTABILITY fun `problems_of_mutability`() { val origin =

    listOf("0") mutability(origin) // Origin is safe } fun mutability(origin: List<String>) { val deep = origin.toMutableList() // Do whatever deep.clear() } 14
  7. CORNER SIDE EFFECTS fun log(){ this.log.append("Increased by 1") } fun

    incrementPure() : Int { return this.current + 1 } 17
  8. FIRST CLASS CITIZENS fun funWithFunctions() { var list = listOf("0",

    "1","2","3","4","5") // Effective function polymorphism morphList(list, ::firstFive) morphList(list, ::reverse) morphList(list, ::empty) } fun <E> morphList(list: List< E>, transform: (List< E>) -> List< E>): List<E> { return transform(list) } fun <E> firstFive(list: List< E>): List<E> = list.subList( 0, 5) fun <E> reverse(list : List< E>) : List<E> = list.reversed() fun <E> empty(list : List< E>) : List<E> = return emptyList() 19
  9. FIRST CLASS CITIZENS fun funWithFunctions() { var list = listOf("0",

    "1", "2", "3", "4", "5") morphList(list, functionFactory(System.currentTimeMillis())) } fun <E> morphList(list: List< E>, transform: (List< E>) -> List< E>): List<E> { return transform(list) } fun <E> functionFactory(time: Long): (List< E>) -> List< E> { return when(time % 3) { 0L -> ::firstFive 1L -> ::reverse else -> ::empty } } 20
  10. REPRESENTING ABSENCE Requires "io.arrow-kt:arrow-core" val schrodinger = Option(Cat()) val schrodinger

    = Cat().some() // Some(Cat()) val schrodinger = Option.fromNullable(catRoulette()) val schrodinger = None 24
  11. ARROW DATA TYPES class Nullable<T>(val value : T?) class NotNullable<T>(

    val value : T) // NotNullable(null) == BOOOM! 25
  12. MANIPULATING DATATYPES Option<String> val value = stringifiedCat.fold( { "" })

    { it } "" "Schrodinger" Left or Error case Right or Success case 31
  13. OUR FIRST STEP IS DONE fun onRegisterClicked() { val username

    = Option.fromNullable( etUsername) .map { editText -> editText.text } .map { text -> text.toString() } .getOrElse { "" } val password = Option.fromNullable( etPassword) .map { editText -> editText.text } .map { text -> text.toString() } .getOrElse { "" } } 32
  14. VALIDATING INPUTS Requires "io.arrow-kt:arrow-data" val valid = Valid("Schrodinger") val valid

    = "Schrodinger".valid<Error, String>() val invalid = Invalid("Error") val invalid = "Error".invalid<String, Response>() 34
  15. VALIDATING INPUTS Requires "io.arrow-kt:arrow-data" val valid = Valid("Schrodinger") val valid

    = "Schrodinger".validNel<Error, String>() val invalid = Invalid(NonEmptyList.just("Error")) val invalid = "Error".invalidNel<String, Response>() 35
  16. ERROR HANDLING val errorTry: Try<String> = Try { throw Exception("BOOOOM!")

    } // <Throwable, Int> val errorValidated: ValidatedNel<Throwable, String> = Exception( "BOOOOM!").invalidNel() val errorEither: Either<Throwable, String> = Exception( "BOOOOM!").left() 37
  17. ERROR HANDLING COMPARED Option Try Validated Either Sucess case Any

    Any Any Any Error case Nothing Throwable Any Any TL;DR Error case is absence An exception is good enough Accumulates errors with Nel flavor You need to control the type of error 38
  18. APPLICATIVE BUILDER .fix() .fold({ errorList -> errorList.all.forEach { error ->

    when (error) { // Notify all the accumulated errors is FormError.EmptyUsername -> { } is LoginViewTest.FormError.InvalidUsername -> {} is LoginViewTest.FormError.EmptyPassword -> {} is LoginViewTest.FormError.LengthInsufficient -> {} is LoginViewTest.FormError.RequiresSpecialCharacters -> {} } } }) { result -> // User and password got validated, proceed to next stage } 41
  19. BACKGROUND WORK Requires "io.arrow-kt:arrow-effects" IO.async<String> { api.register(username, password) } .unsafeRunAsync

    { result: Either<Throwable, String> -> result.fold({ error -> view?.notifyError(error) }) { success -> view?.notifySucess(success) } } 43
  20. COROUTINES INTEGRATION Requires "io.arrow-kt:arrow-effects-coroutines" DeferredK(CommonPool) { api.register(username, password) } .unsafeRunAsync

    { result: Either<Throwable, String> -> result.fold({ error -> view?.notifyError(error) }) { success -> view?.notifySucess(success) } } 44
  21. MONAD FUNCTOR ForDeferredK extensions { binding { DeferredK(CommonPool) { api.register(user,

    pass) }.bind() DeferredK(CommonPool) { api.login(user, pass) }.bind() }.fix() .unsafeRunAsync { result -> DeferredK( UI) { result.fold( { ex -> view?.notifyRegisterError(ex) }) { view?.navigateToHome() } } } } 45
  22. THE FP RESULT val username = Option.fromNullable(view?. username).getOrElse { ""

    } val password = Option.fromNullable(view?. password).getOrElse { "" } Validated. applicativeError(NonEmptyList. semigroup<FormError>()) .tupled(isUsernameBlank(username), hasUsernameValidCharacters(username), isPasswordBlank(password), isPasswordLongEnough(password), isPasswordStrongEnough(password)) . fix() .fold( { errorList -> errorList. all.forEach { error -> when (error) { is FormError.EmptyUsername -> { ... } is LoginViewTest.FormError.InvalidUsername -> { ... } is LoginViewTest.FormError.EmptyPassword -> {... } is LoginViewTest.FormError.LengthInsufficient -> {...} is LoginViewTest.FormError.RequiresSpecialCharacters -> {...} } } }) { result -> ForDeferredK extensions { binding { DeferredK(CommonPool) { api.register(username, password) }.bind() DeferredK(CommonPool) { api.login(username, password) }.bind() }.fix() . unsafeRunAsync { result -> DeferredK( UI) {result.fold( { error -> view?.notifyRegisterError(error) }) { view?.navigateToHome() } } } } } 47
  23. EXTRACT VALIDATOR CLASS class FormValidator( val username: String, val password:

    String) { fun validate(): Validated<NonEmptyList<FormError>, Tuple2<String, String>> { return Validated.applicativeError(NonEmptyList. semigroup<FormError>()) .tupled(isUsernameBlank( username), hasUsernameValidCharacters( username), isPasswordBlank( password), isPasswordLongEnough( password), isPasswordStrongEnough( password)) . fix() .map { result -> Tuple2(username, password) } } // … } 49
  24. BUILD USE CASES open class UseCase<P, R>(val logic: (P) ->

    R) { fun defer(thread: CoroutineDispatcher = CommonPool, params: P): DeferredK< R> { return DeferredK.async(thread) { logic.invoke(params) } } } class RegisterUser( val api: Api) : UseCase<Tuple2<String, String>, Unit>( { (username, password) -> api.register(username, password) }) class LoginUser(val api: Api) : UseCase<Tuple2<String, String>, String>( { (username, password) -> api.login(username, password) }) fun <A> DeferredK< A>.executeAndReturnOnUi(onError: (Throwable) -> Unit, onSuccess: ( A) -> Unit) { this.unsafeRunAsync { result -> DeferredK(UI) { result.fold(onError, onSuccess) } } } 50
  25. A LITTLE OF EXTRACT METHODS private fun showFormErrors(errors: List<FormError>) {

    errors.forEach { error -> when (error) { is FormError.EmptyUsername -> { ... } is FormError.InvalidUsername -> { ... } is FormError.EmptyPassword -> { ... } is FormError.LengthInsufficient -> { ... } is FormError.RequiresSpecialCharacters -> { ... } } } } 51
  26. THE FINAL RESULT val username = Option.fromNullable(view?. username).getOrElse { ""

    } val password = Option.fromNullable(view?. password).getOrElse { "" } FormValidator(username, password) .validate() .fold( { errorList -> view?.showFormErrors(errorList. all) }) { (username, password) -> Registration(api) .defer(Tuple2(username, password)) . executeAndReturnOnUi({ error -> view?.notifyRegisterError(ex) }) { token -> view?.navigateToHome() } } } 52
  27. FP Null handling Independent computation Sucess/Error result handling WE HAVE

    INTRODUCED A LOT OF TECHNIQUES OOP SOLID principles Extension functions Deferred validation Clean architecture 53
  28. OPTICS Providing inmutable models and the way to modify them

    @optics data class User(val address: Address) { companion object } @optics data class Address(val city: City) { companion object } @optics data class City(val name: String) { companion object } fun uppercaseCity() { val user = User(Address(City( "Łódź"))) // In OOP val modifiedUserInOOP = user.copy( address = user. address.copy( city = City(user. address.city.name.toUpperCase() ) ) ) // With FP and Arrow val modifiedUserWithArrow = User. address.city.modify(user) { city -> City(city.name.toUpperCase()) } } 55
  29. HIGHER KIND Writing generic code which can be executed polymorphically

    on runtime abstract class UseCase<P, R>( val logic: (P) -> R) { fun defer(thread: CoroutineDispatcher = CommonPool, params: P): DeferredK<R> { return DeferredK.async(thread) { logic.invoke(params) } } } class RegisterUser(val api: Api) : UseCase<Tuple2<String, String>, Unit>( /.../) 56
  30. HIGHER KIND Writing generic code which can be executed polymorphically

    on runtime abstract class HKindUseCase<P, R, F>( val async: Async<F>, val logic: (P) -> R) : Async<F> by async { fun defer(thread: CoroutineDispatcher = CommonPool, params: P): Kind<F, R> { return async(thread) { logic.invoke(params) } } } class Register<F>(async: Async<F>, val api: Api) : HKindUseCase<Tuple2<String, String>, Unit,F> (async, /.../}) fun testRegister() { Register(DeferredK. async(), Api()) Register(IO. async(), Api()) Register(FluxK. async(), Api()) Register(ObservableK. async(), Api()) } 57
  31. ▸ The more tools you know, the better dev you

    become ▸ No silver bullets, approach each problems with the right tools ▸ Don’t be dogmatic WRAPPING UP 58
  32. THIS CAN BE READABLE val username = Option.fromNullable(view?. username).getOrElse {

    "" } val password = Option.fromNullable(view?. password).getOrElse { "" } Validated. applicativeError(NonEmptyList. semigroup<FormError>()) .tupled(isUsernameBlank(username), hasUsernameValidCharacters(username), isPasswordBlank(password), isPasswordLongEnough(password), isPasswordStrongEnough(password)) . fix() .fold( { errorList -> errorList. all.forEach { error -> when (error) { is FormError.EmptyUsername -> { ... } is LoginViewTest.FormError.InvalidUsername -> { ... } is LoginViewTest.FormError.EmptyPassword -> {... } is LoginViewTest.FormError.LengthInsufficient -> {...} is LoginViewTest.FormError.RequiresSpecialCharacters -> {...} } } }) { result -> ForDeferredK extensions { binding { DeferredK(CommonPool) { api.register(username, password) }.bind() DeferredK(CommonPool) { api.login(username, password) }.bind() }.fix() . unsafeRunAsync { result -> DeferredK( UI) {result.fold( { error -> view?.notifyRegisterError(error) }) { view?.navigateToHome() } } } } } 59
  33. BUT THIS IS INTUITIVE val username = Option.fromNullable(view?. username).getOrElse {

    "" } val password = Option.fromNullable(view?. password).getOrElse { "" } FormValidator(username, password) .validate() .fold( { errorList -> view?.showFormErrors(errorList. all) }) { (username, password) -> Registration(api) .defer(Tuple2(username, password)) . executeAndReturnOnUi({ error -> view?.notifyRegisterError(ex) }) { token -> view?.navigateToHome() } } } 60
  34. CREDITS Special thanks to all the people who made this

    presentation possible: ▸ Jorge Castillo for proofreading this talk ▸ Francisco Estévez & Raúl Raja for their mentorship ▸ The Arrow team ▸ Arrow documentation ▸ Bow is Swift’s implementation of Arrow by Tomás Ruiz López ▸ Presentation template by SlidesCarnival ▸ https://speakerdeck.com/sefford/an-arrow-to-the-kotlin