Slide 1

Slide 1 text

KΛTEGORY Functional Data Types & Abstractions for Kotlin 1

Slide 2

Slide 2 text

About Us Raul Raja CTO @47deg Paco Mistake-driven learning Simon Vergauwen Optometrist @raulraja @pacoworks @vergauwen_simon 2

Slide 3

Slide 3 text

What started as... Learning Exercise to learn FP over Slack 3

Slide 4

Slide 4 text

...Ended in Solution for Typed FP in Kotlin 4

Slide 5

Slide 5 text

What is KΛTEGORY? A library for typed FP in Kotlin with the traditional type classes and data types 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

A few syntax examples import kategory.Option Option(1).map { it + 1 } //Option(2) 7

Slide 8

Slide 8 text

A few syntax examples import kategory.Try Try { throw RuntimeException("BOOM!") }.map { it + 1 } //Failure(RuntimeException("BOOM!")) 8

Slide 9

Slide 9 text

A few syntax examples import kategory.* import kategory.Either.* val x = Right(1) val y = 1.right() x == y //true 9

Slide 10

Slide 10 text

Applicative Builder import kategory.* data class Profile(val id: Long, val name: String, val phone: Int) fun profile(val maybeId: Option, val maybeName: Option, val maybePhone: Option): Option = 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

Slide 11

Slide 11 text

Comprehensions - Vanilla Generalized to all monads fun profile(val maybeId: Option, val maybeName: Option, val maybePhone: Option): Option = 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

Slide 12

Slide 12 text

Comprehensions - Exception Aware Automatically captures exceptions for instances of MonadError Try.monadError().bindingE { val name = profileService().bind() val phone = phoneService().bind() throw RuntimeException("BOOM") // <-- `raises errors to MonadError` val addresses = addressService().bind() yields(Profile(name, phone, addresses)) } //Failure(RuntimeException("BOOM")) 12

Slide 13

Slide 13 text

Comprehensions - Stack-Safe Stack-Safe comprehensions for Stack-Unsafe data types fun stackSafeTestProgram(M: Monad, n: Int = 0, stopAt: Int = 1000000): Free = M.bindingStackSafe { val v = pure(n + 1).bind() // <-- auto binds on `Free` 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

Slide 14

Slide 14 text

Monad Comprehensions - Cancellable Supports cancelling tasks initiated inside the coroutine val (binding: IO>, 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>, Disposable>` unsafeCancel() //the disposable instance can cancel all operations inside the coroutine 14

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Optics without boilerplate You may define composable Lenses to work with immutable data transformations import kategory.* import kategory.optics.* val employeeCompany: Lens = Lens( get = { it.company }, set = { company -> { employee -> employee.copy(company = company) } } ) val companyAddress: Lens = Lens( get = { it.address }, set = { address -> { company -> company.copy(address = address) } } ) ... val employeeStreetName: Lens = employeeCompany compose companyAddress compose addressStrees compose streetName employeeStreetName.modify(employee, String::capitalize) 19

Slide 20

Slide 20 text

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 = Lens( - get = { it.company }, - set = { company -> { employee -> employee.copy(company = company) } } - ) - - val companyAddress: Lens = Lens( - get = { it.address }, - set = { address -> { company -> company.copy(address = address) } } - ) - ... val employeeStreetName: Lens = employeeCompany compose companyAddress compose addressStrees compose streetName employeeStreetName.modify(employee, String::capitalize) 20

Slide 21

Slide 21 text

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 = 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

Slide 22

Slide 22 text

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 = 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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Kotlin limitations for Typed FP 25

Slide 26

Slide 26 text

Kotlin limitations for Typed FP Emulated Higher Kinds through Lightweight higher-kinded Polymorphism interface HK class OptionHK private constructor() typealias OptionKind = kategory.HK inline fun OptionKind.ev(): Option = this as Option sealed class Option : OptionKind 26

Slide 28

Slide 28 text

Kotlin limitations for Typed FP No notion of implicits or Type class instance evidences verified at compile time fun A.some( AA: Applicative /*<-- User is forced to provide instances explicitly */): Option = AA.pure(this).ev() 1.some(Option.applicative()) //Option(1) 28

Slide 29

Slide 29 text

Kotlin limitations for Typed FP (for now) an implicit lookup system based on a global registry is provided fun A.some( AA: Applicative = applicative() /*<-- Instances are discovered implicitly */): Option = AA.pure(this).ev() 1.some() //Option(1) 29

Slide 30

Slide 30 text

KΛTEGORY ad-hoc polymorphism With emulated Higher Kinds and Type classes we can now write polymorphic code inline fun raiseError(e: E, ME: MonadError = monadError()): HK = ME.raiseError(e) raiseError, String, Int>("Not Found").ev() // <-- Not ideal //Left("Not Found") 30

Slide 31

Slide 31 text

Type Classes This is how you define Type Classes in KΛTEGORY (for now) interface Functor : Typeclass { fun map(fa: HK, f: (A) -> B): HK } 31

Slide 32

Slide 32 text

Implementing type class instances is easy... 32

Slide 33

Slide 33 text

@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 : OptionKind { ... } 33

Slide 34

Slide 34 text

@instance KΛTEGORY can also derive implicit discovery for manual instances @instance(Either::class) interface EitherFunctorInstance : Functor> { override fun map(fa: EitherKind, f: (A) -> B): Either = fa.ev().map(f) } 34

Slide 35

Slide 35 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Credits KΛTEGORY is inspired in great libraries that have proven useful to the FP community: Cats Scalaz Freestyle Monocle Funktionale Paguro 38

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Thanks! Thanks to everyone that makes KΛTEGORY possible 41