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

Introduction to KΛTEGORY

Introduction to KΛTEGORY

http://kategory.io/

KΛTEGORY is a library that started as a port of Typelevel Scala cats for the Kotlin Programming Language and is now evolving to bring the best of typed FP to Kotlin. This talk covers some of the main data types and abstractions that make typed Functional Programming in Kotlin Possible. Kategory features techniques such as monadic comprehensions, laws, applicative builders, emulated higher kinds, free monads, and global typeclass instances for the increasingly popular Kotlin programming language. We discuss some pragmatic applications of KΛTEGORY and some of the different styles you can adopt when working with typed FP in Kotlin alogn with a proposal to introduce type classes and higher kinds as part of the language.

https://github.com/47deg/kategory-intro

Raúl Raja Martínez

October 27, 2017
Tweet

More Decks by Raúl Raja Martínez

Other Decks in Programming

Transcript

  1. About Us Raul Raja CTO @47deg Paco Mistake-driven learning Simon

    Vergauwen Optometrist @raulraja @pacoworks @vergauwen_simon 2
  2. What is KΛTEGORY? A library for typed FP in Kotlin

    with the traditional type classes and data types 5
  3. Data types Many data types to cover general use cases.

    Error Handling Option , Try , Validated , Either , Ior Collections ListKW , SequenceKW , MapKW , SetKW RWS Reader , Writer , State Transformers ReaderT , WriterT , OptionT , StateT , EitherT Evaluation Eval , Trampoline , Free , FunctionN Effects IO , Free , ObservableKW Others Coproduct , Coreader , Const , ... 6
  4. A few syntax examples import kategory.Try Try { throw RuntimeException("BOOM!")

    }.map { it + 1 } //Failure(RuntimeException("BOOM!")) 8
  5. A few syntax examples import kategory.* import kategory.Either.* val x

    = Right(1) val y = 1.right() x == y //true 9
  6. Applicative Builder import kategory.* data class Profile(val id: Long, val

    name: String, val phone: Int) fun profile(val maybeId: Option<Long>, val maybeName: Option<String>, val maybePhone: Option<Int>): Option<Profile> = Option.applicative().map(id, name, phone, { (a, b, c) -> Profile(a, b, c) }) profile(1L.some(), "William Alvin Howard".some(), 555555555.some()) //Some(Profile(1L, "William Alvin Howard", 555555555) 10
  7. Comprehensions - Vanilla Generalized to all monads fun profile(val maybeId:

    Option<Long>, val maybeName: Option<String>, val maybePhone: Option<Int>): Option<Profile> = Option.monad().binding { // <-- `coroutine starts` val id = maybeId.bind() // <-- `suspended` val name = maybeName.bind() // <-- `suspended` val phone = maybePhone.bind() // <-- `suspended` yields(Profile(id, name, phone)) } // <-- `coroutine ends` profile(2L.some(), "Haskell Brooks Curry".some(), 555555555.some()) 11
  8. Comprehensions - Exception Aware Automatically captures exceptions for instances of

    MonadError<F, Throwable> Try.monadError().bindingE { val name = profileService().bind() val phone = phoneService().bind() throw RuntimeException("BOOM") // <-- `raises errors to MonadError<F, Throwable>` val addresses = addressService().bind() yields(Profile(name, phone, addresses)) } //Failure(RuntimeException("BOOM")) 12
  9. Comprehensions - Stack-Safe Stack-Safe comprehensions for Stack-Unsafe data types fun

    <F> stackSafeTestProgram(M: Monad<F>, n: Int = 0, stopAt: Int = 1000000): Free<F, Int> = M.bindingStackSafe { val v = pure(n + 1).bind() // <-- auto binds on `Free<F, Int>` val r = if (v < stopAt) stackSafeTestProgram(M, v, stopAt).bind() else pure(v).bind() yields(r) } val M = Id.monad() stackSafeTestProgram(M).run(M).ev() //Id(1000000) 13
  10. Monad Comprehensions - Cancellable Supports cancelling tasks initiated inside the

    coroutine val (binding: IO<List<User>>, unsafeCancel: Disposable) = ioMonadError.bindingECancellable { val userProfile = bindAsync(ioAsync) { getUserProfile("123") } val friendProfiles = userProfile.friends().map { friend -> bindAsync(ioAsync) { getProfile(friend.id) } } yields(listOf(userProfile) + friendProfiles) } // <- returns `Tuple2<IO<List<User>>, Disposable>` unsafeCancel() //the disposable instance can cancel all operations inside the coroutine 14
  11. Monad Comprehensions - Context Aware Allows switching contexts in which

    bind/flatMap takes place ioMonad.binding { val user = bindAsync(ioAsync) { getUserProfile("123") } //<-- binds on IO's pool bindIn(DatabaseContext) { storeUser(user) } //<-- binds on DB's pool bindIn(UIContext) { toastMessage("User cached!") } //<-- binds on UI thread yields(user) } 15
  12. Monad Comprehensions - 3rd party integrations Allows seamless integration with

    existing libraries like RxJava observableMonad.binding { val ticks = runAsync(observableAsync) { videoPlayer.getCurrentMilliseconds() }.subscribeOn(mainThread()) val initialTick = ticks.bind() val timer = Observable.interval(100, MilliSeconds).k().bind() val currentTick = ticks.bind() yields(currentTick - initialTick) } 16
  13. Transforming immutable data KΛTEGORY includes an optics library that make

    working with immutable data a breeze data class Street(val number: Int, val name: String) data class Address(val city: String, val street: Street) data class Company(val name: String, val address: Address) data class Employee(val name: String, val company: Company) val employee = Employee("John Doe", Company("Kategory", Address("Functional city", Street(23, "lambda street")))) employee //Employee(name=John Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=23, name 17
  14. Transforming immutable data while kotlin provides a synthetic copy dealing

    with nested data can be tedious employee.copy( company = employee.company.copy( address = employee.company.address.copy( street = employee.company.address.street.copy( name = employee.company.address.street.name.capitalize() ) ) ) ) //Employee(name=John Doe, company=Company(name=Kategory, address=Address(city=Functional city, street=Street(number=23, name 18
  15. Optics without boilerplate You may define composable Lenses to work

    with immutable data transformations import kategory.* import kategory.optics.* val employeeCompany: Lens<Employee, Company> = Lens( get = { it.company }, set = { company -> { employee -> employee.copy(company = company) } } ) val companyAddress: Lens<Company, Address> = Lens( get = { it.address }, set = { address -> { company -> company.copy(address = address) } } ) ... val employeeStreetName: Lens<Employee, String> = employeeCompany compose companyAddress compose addressStrees compose streetName employeeStreetName.modify(employee, String::capitalize) 19
  16. Optics without boilerplate Or just let KΛTEGORY @lenses do the

    dirty work + @lenses data class Employee(val name: String, val company: Company) - val employeeCompany: Lens<Employee, Company> = Lens( - get = { it.company }, - set = { company -> { employee -> employee.copy(company = company) } } - ) - - val companyAddress: Lens<Company, Address> = Lens( - get = { it.address }, - set = { address -> { company -> company.copy(address = address) } } - ) - ... val employeeStreetName: Lens<Employee, String> = employeeCompany compose companyAddress compose addressStrees compose streetName employeeStreetName.modify(employee, String::capitalize) 20
  17. Optics without boilerplate You can also define custom Prism for

    your sum types sealed class NetworkResult { data class Success(val content: String): NetworkResult() object Failure: NetworkResult() } val networkSuccessPrism: Prism<NetworkResult, NetworkResult.Success> = Prism( getOrModify = { networkResult -> when(networkResult) { is NetworkResult.Success -> networkResult.right() else -> networkResult.left() } }, reverseGet = { networkResult -> networkResult } //::identity ) val networkResult = NetworkResult.Success("content") networkSuccessPrism.modify(networkResult) { success -> success.copy(content = "different content") } //Success(content=different content) 21
  18. Optics without boilerplate Or let @prisms do that for you

    + @prisms sealed class NetworkResult { + data class Success(val content: String) : NetworkResult() + object Failure : NetworkResult() + } - sealed class NetworkResult { - data class Success(val content: String): NetworkResult() - object Failure: NetworkResult() - } - val networkSuccessPrism: Prism<NetworkResult, NetworkResult.Success> = Prism( - getOrModify = { networkResult -> - when(networkResult) { - is NetworkResult.Success -> networkResult.right() - else -> networkResult.left() - } - }, - reverseGet = { networkResult -> networkResult } //::identity - ) val networkResult = NetworkResult.Success("content") networkSuccessPrism.modify(networkResult) { success -> success.copy(content = "different content") } 22
  19. @free & @tagless is being ported to Kotlin by the

    team . Freestyle @47deg + @free interface GesturesDSL<F> : GesturesDSLKind<A> { + fun click(view: UiObject): FreeS<F, Boolean> + fun pinchIn(view: UiObject, percent: Int, val steps: Int): FreeS<F, Boolean> + fun pinchOut(view: UiObject, percent: Int, val steps: Int): FreeS<F, Boolean> + } - typealias ActionDSL<A> = Free<GesturesDSLHK, A> - @higherkind sealed class GesturesDSL<A> : GesturesDSLKind<A> { - object PressHome : GesturesDSL<Boolean>() - data class Click(val view: UiObject) : GesturesDSL<Boolean>() - data class PinchIn(val view: UiObject, val percent: Int, val steps: Int) : GesturesDSL<Boolean>() - data class PinchOut(val view: UiObject, val percent: Int, val steps: Int) : GesturesDSL<Boolean>() - companion object : FreeMonadInstance<GesturesDSLHK> - } - fun click(view: UiObject): ActionDSL<Boolean> = - Free.liftF(GesturesDSL.Click(ui)) - fun pinchIn(view: UiObject, percent: Int, val steps: Int): ActionDSL<Boolean> = - Free.liftF(GesturesDSL.PinchIn(percent, steps)) - fun pinchOut(view: UiObject, percent: Int, val steps: Int): ActionDSL<Boolean> = - Free.liftF(GesturesDSL.PinchOut(ui, percent, steps)) 23
  20. KΛTEGORY is becoming modular Pick and choose what you'd like

    to use. Module Contents typeclasses Semigroup , Monoid , Functor , Applicative , Monad ... data Option , Try , Either , Validated ... effects IO effects-rx2 ObservableKW , FlowableKW mtl MonadReader , MonadState , MonadFilter ,... free Free , FreeApplicative , Trampoline , ... freestyle @free , @tagless recursion-schemes optics Prism , Iso , Lens , ... collections meta @higherkind , @deriving , @implicit , @instance , @lenses , @prisms , @isos 24
  21. Kotlin limitations for Typed FP Emulated Higher Kinds through Lightweight

    higher-kinded Polymorphism interface HK<out F, out A> class OptionHK private constructor() typealias OptionKind<A> = kategory.HK<OptionHK, A> inline fun <A> OptionKind<A>.ev(): Option<A> = this as Option<A> sealed class Option<out A> : OptionKind<A> 26
  22. Kotlin limitations for Typed FP Fear not, @higherkind 's got

    your back! + @higherkind sealed class Either<A, B> : EitherKind<A, B> - - class EitherHK private constructor() - - typealias EitherKindPartial<A> = kindedj.Hk<EitherHK, A> - typealias EitherKind<A, B> = kindedj.Hk<EitherKindPartial<A>, B> - - inline fun <A, B> EitherKind<A, B>.ev(): Either<A, B> = this as Either<A, B> 27
  23. Kotlin limitations for Typed FP No notion of implicits or

    Type class instance evidences verified at compile time fun <F, A> A.some( AA: Applicative<OptionHK> /*<-- User is forced to provide instances explicitly */): Option<A> = AA.pure(this).ev() 1.some(Option.applicative()) //Option(1) 28
  24. Kotlin limitations for Typed FP (for now) an implicit lookup

    system based on a global registry is provided fun <A> A.some( AA: Applicative<OptionHK> = applicative() /*<-- Instances are discovered implicitly */): Option<A> = AA.pure(this).ev() 1.some() //Option(1) 29
  25. KΛTEGORY ad-hoc polymorphism With emulated Higher Kinds and Type classes

    we can now write polymorphic code inline fun <reified F, reified E, A> raiseError(e: E, ME: MonadError<F, E> = monadError()): HK<F, A> = ME.raiseError(e) raiseError<EitherKindPartial<String>, String, Int>("Not Found").ev() // <-- Not ideal //Left("Not Found") 30
  26. Type Classes This is how you define Type Classes in

    KΛTEGORY (for now) interface Functor<F> : Typeclass { fun <A, B> map(fa: HK<F, A>, f: (A) -> B): HK<F, B> } 31
  27. @deriving KΛTEGORY can derive instances based on conventions in your

    data types @higherkind @deriving( Functor::class, Applicative::class, Monad::class, Foldable::class, Traverse::class, TraverseFilter::class, MonadFilter::class) sealed class Option<out A> : OptionKind<A> { ... } 33
  28. @instance KΛTEGORY can also derive implicit discovery for manual instances

    @instance(Either::class) interface EitherFunctorInstance<L> : Functor<EitherKindPartial<L>> { override fun <A, B> map(fa: EitherKind<L, A>, f: (A) -> B): Either<L, B> = fa.ev().map(f) } 34
  29. KEEP-87 But we are not stopping here, we want to

    get rid of some of these hacks! KEEP-87 is A KEEP to introduce Type Classes in Kotlin https:/ /github.com/Kotlin/KEEP/pull/87 35
  30. KEEP-87 If KEEP-87 makes it to the lang extension interface

    Functor<F<_>> { // <-- real Higher kinds positions fun <A, B> map(fa: F<A>, f: (A) -> B): F<B> } extension object OptionFunctor: Functor<Option> { // <-- real Higher kinds positions fun <A, B> map(fa: Option<A>, f: (A) -> B): Option<B> } fun <F<_>, A, B> transform( fa: F<A>, f: (A) -> B): F<B> given Functor<F> = map(fa, f) // <-- compile time verified transform(Option(1), { it + 1 })// <-- no need to cast from HK representation //Option(2) 36
  31. What if KEEP-87 does not make it to Kotlin? @implicit

    as global implicits through a annotation processor / compiler plugin. Discuss if a fork to the compiler with compatibility for KEEP-87 under a compiler flag makes sense. 37
  32. Credits KΛTEGORY is inspired in great libraries that have proven

    useful to the FP community: Cats Scalaz Freestyle Monocle Funktionale Paguro 38
  33. Team anstaendig arturogutierrez ffgiraldez Guardiola31337 javipacheco JMPergar JorgeCastilloPrz jrgonzalezg nomisRev

    npatarino pablisco pakoito pedrovgs pt2121 raulraja wiyarmir andyscott Atternatt calvellido dominv GlenKPeterson israelperezglez sanogueralorenzo Takhion victorg1991 tonilopezmr NigelHeylen ersin-ertan 39
  34. Join us! Github Slack Gitter We provide 1:1 mentoring for

    both users & new contributors! https:/ /github.com/kategory https:/ /kotlinlang.slack.com/messages/C5UPMM0A0 https:/ /gitter.im/kategory/Lobby 40