Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Free Monads Sample #6

Merged
merged 19 commits into from Oct 7, 2017
Merged

Free Monads Sample #6

merged 19 commits into from Oct 7, 2017

Conversation

JorgeCastilloPrz
Copy link
Owner

@JorgeCastilloPrz JorgeCastilloPrz commented Oct 7, 2017

Free Monads Sample

Fixes #7

Motivations

The only purpose of using Free is to be able to model your app / system logic in an abstract way depending on Free instead of concrete types, and provide semantics (real effect-causing implementations) to it through an interpreter later on, the moment you want to run it.

The abstract execution tree that you created declaratively based on Free is called AST (Abstract syntax tree), and it is unfolded step by step by Free using an interpreter to provide semantics to each one of the steps (operations).

This allows you to decouple contracts from implementations and be able to swap side effect implementations by fakes at testing time easily. You would just need to provide a different interpreter which does not include real side effects.

More concise definition extracted from Functional and Reactive Domain Modeling book:

"The interpreter provides the denotation to each of the elements of algebra that your free monad has recursively built within your composed DSL. And the interpretation may also include side effects."

How to go Free with KΛTEGORY

KΛTEGORY enables Free and effects like IO or Async related effects to help us on this matter. The process of modeling a complete system or just a part of it using Free is composed of 4 different steps:

  1. Define your algebra of operations: You need to declare the operations that you want to model on your system / layer where you are implementing Free. It is an algebraic data type (ADT) that I am declaring using a Kotlin sealed class. The algebra declares a constrained amount of operations that will be available and composable just in case you need to create more complex ones.

    This is an example of an Algebra, where each one of the types on the sealed hierarchy represents an atomic operation:

    @higherkind sealed class HeroesAlgebra<A> : HeroesAlgebraKind<A> {
        object GetAll : HeroesAlgebra<List<CharacterDto>>()
        class GetSingle(val heroId: String) : HeroesAlgebra<CharacterDto>()
        class HandlePresentationEffects(val result: Either<CharacterError, List<CharacterDto>>) : 
    HeroesAlgebra<Unit>()
        class Attempt<A>(val fa: FreeHeroesAlgebra<A>): HeroesAlgebra<Either<Throwable, A>>()
        companion object : FreeMonadInstance<HeroesAlgebraHK>
    }

    We have a closed hierarchy of ops available here. Each one has it's particular return type
    depending on what's gonna do. So GetAll is an algebra of List<CharacterDto> because it's
    potentially returning a list of characters. Another example: HandlePresentationEffects would
    represent the operation causing effects to render use case results on screen (which would be
    equivalent to presentation logic) so, since it is representing a side effect by itself, it needs to return Unit.

  2. Lift everything to the context of Free: We need to lift each one of the algebra ops to the context of Free to be able to compose our app AST using those. This is how I am doing it:

    fun getAllHeroes(): FreeHeroesAlgebra<List<CharacterDto>> =
            Free.liftF(HeroesAlgebra.GetAll)
    
    fun getSingleHero(heroId: String): FreeHeroesAlgebra<CharacterDto> =
            Free.liftF(HeroesAlgebra.GetSingle(heroId))
    
    fun handlePresentationEffects(result: Either<CharacterError, List<CharacterDto>>): 
    FreeHeroesAlgebra<Unit> =
            Free.liftF(HeroesAlgebra.HandlePresentationEffects(result))
    
    fun <A> attempt(fa: FreeHeroesAlgebra<A>): FreeHeroesAlgebra<Either<Throwable, A>> =
            Free.liftF(HeroesAlgebra.Attempt(fa))

    As you can see, I am just providing handful utility pure functions to lift each operation to Free. Now we can start using those in our app to concatenate code through different layers and compose the whole AST.

  3. Provide semantics with the Interpreter: Once you have your whole AST depending on Free and never on concrete types, we need to create a interpreter to provide those details.
    This is my interpreter:

    fun <F> interpreter(ctx: SuperHeroesContext, ME: MonadError<F, Throwable>,
        AC: AsyncContext<F>): FunctionK<HeroesAlgebraHK, F> =
        object : FunctionK<HeroesAlgebraHK, F> {
          override fun <A> invoke(fa: HeroesAlgebraKind<A>): HK<F, A> {
            val op = fa.ev()
            return when (op) {
              is HeroesAlgebra.GetAll -> getAllHeroesImpl(ctx, ME, AC)
              is HeroesAlgebra.GetSingle -> getHeroDetailsImpl(ctx, ME, AC, op.heroId)
              is HeroesAlgebra.HandlePresentationEffects -> {
                ME.catch({ handlePresentationEffectsImpl(ctx, op.result) })
              }
              is HeroesAlgebra.Attempt<*> -> {
                val result = op.fa.foldMap(interpreter(ctx, ME, AC), ME)
                ME.attempt(result)
              }
            } as HK<F, A>
          }
        }
    
    private fun <F, A, B> runInAsyncContext(
        f: () -> A,
        onError: (Throwable) -> B,
        onSuccess: (A) -> B,
        AC: AsyncContext<F>): HK<F, B> {
      return AC.runAsync { proc ->
        async(CommonPool) {
          val result = Try { f() }.fold(onError, onSuccess)
          proc(result.right())
        }
      }
    }
    
    fun <F> getAllHeroesImpl(
        ctx: SuperHeroesContext,
        ME: MonadError<F, Throwable>,
        AC: AsyncContext<F>): HK<F, List<CharacterDto>> {
      return ME.bindingE {
        val query = Builder.create().withOffset(0).withLimit(50).build()
        runInAsyncContext(
            f = { ctx.apiClient.getAll(query).response.characters.toList() },
            onError = { ME.raiseError<List<CharacterDto>>(it) },
            onSuccess = { ME.pure(it) },
            AC = AC
        ).bind()
      }
    }
    
    fun <F> getHeroDetailsImpl(
        ctx: SuperHeroesContext,
        ME: MonadError<F, Throwable>,
        AC: AsyncContext<F>,
        heroId: String): HK<F, CharacterDto> =
        ME.bindingE {
          runInAsyncContext(
              f = { ctx.apiClient.getCharacter(heroId).response },
              onError = { ME.raiseError<CharacterDto>(it) },
              onSuccess = { ME.pure(it) },
              AC = AC
          ).bind()
        }
    
    fun handlePresentationEffectsImpl(
        ctx: SuperHeroesContext,
        result: Either<CharacterError, List<CharacterDto>>): Unit =
        when (result) {
          is Left -> {
            displayErrors(ctx, result.a); }
          is Right -> when (ctx) {
            is GetHeroesContext -> ctx.view.drawHeroes(result.b.map {
              SuperHeroViewModel(
                  it.id,
                  it.name,
                  it.thumbnail.getImageUrl(MarvelImage.Size.PORTRAIT_UNCANNY),
                  it.description)
        })
            is GetHeroDetailsContext -> ctx.view.drawHero(result.b.map {
              SuperHeroViewModel(
                  it.id,
                  it.name,
                  it.thumbnail.getImageUrl(MarvelImage.Size.PORTRAIT_UNCANNY),
                  it.description)
            }[0])
          }
        }
    
    fun displayErrors(ctx: SuperHeroesContext, c: CharacterError): Unit {
      when (c) {
        is NotFoundError -> ctx.view.showNotFoundError()
        is UnknownServerError -> ctx.view.showGenericError()
        is AuthenticationError -> ctx.view.showAuthenticationError()
      }
    }

    So the main point of interest here is the interpreter() function, which is providing an
    interpreter to use to resolve each one of the steps on the AST when the user decides to "unfold" it.
    It's just a simple when statement matching over the operation type and running the required
    effects in response by using the chosen monad instance.
    So literally, the interpreter here is in charge of providing all the implementation details / side
    effects that we wanted to abstract.

    The key point about which parts of your system should be declared as operations in the
    algebra and therefore abstracted using Free, is whether you need to replace those
    implementations at runtime or at testing time. This was my baseline thought used when
    implementing this sample.

    After all, Free is just a way of decoupling system declarations from implementation details
    used to run them.

  4. Unfold the AST using the interpreter: Finally you would just need to run the system passing in the interpreter to be used to provide semantics, and the monad implementation that you are chosing to do all the work inside of it.

    getSuperHeroes().unsafePerformEffects(GetHeroesContext(this, this))
    
    fun <A> FreeHeroesAlgebra<A>.unsafePerformEffects(ctx: SuperHeroesContext): Unit {
      val ME = IO.monadError()
      val result: IO<A> = this.foldMap(interpreter(ctx, ME, IO.asyncContext()), ME).ev()
    }

    The getSuperHeroes() function is just the beginning of the AST and it's gonna return a composed Free based tree. On this example I am chosing to use an instance of MonadError to foldMap the AST and use it on the interpreter provided on step 3. Now Free is able to run the whole chain since it has been provided implementation details (interpreter) and a monad instance to use to resolve those.

So cheers, there we have our Free Monads sample over Android! 🎉

Recommended bibliography

@JorgeCastilloPrz
Copy link
Owner Author

Thanks @raulraja for helping on understanding and polishing some details on this. You always become very helpful 👏 .

Copy link
Collaborator

@raulraja raulraja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

README.md Outdated
@@ -51,7 +51,9 @@ Higher Kinds to make our code depend on typeclass constrained behaviors, leaving
which concrete types to use to the moment when we want to run the code.
[You will really want to look at this PR to have a very good and detailed description of what tagless-final is](https://github.com/JorgeCastilloPrz/KotlinAndroidFunctional/pull/2).
## Free Monads
TBA. This is going to be another cool approach using FP, but still not ready.
This FP style is very trendy. We are applying it over Android thanks to Kategory here, on the `free-monads` project module. It's highly recommended to take a look at [this PR](https://github.com/JorgeCastilloPrz/KotlinAndroidFunctional/pull/6) in order to understand the approach.
**Free Monads** is based on the idea of composing an **AST** (abstract syntax tree) of computations with type `Free<T>` which will never depend on implementation details but on abstractions defined by an algebra, which is an algebraic data type (ADT). We are defining it through a `sealed` class on this sample.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Free<S, A> where S is your algebra

README.md Outdated
TBA. This is going to be another cool approach using FP, but still not ready.
This FP style is very trendy. We are applying it over Android thanks to Kategory here, on the `free-monads` project module. It's highly recommended to take a look at [this PR](https://github.com/JorgeCastilloPrz/KotlinAndroidFunctional/pull/6) in order to understand the approach.
**Free Monads** is based on the idea of composing an **AST** (abstract syntax tree) of computations with type `Free<T>` which will never depend on implementation details but on abstractions defined by an algebra, which is an algebraic data type (ADT). We are defining it through a `sealed` class on this sample.
Those ops can be combined as blocks to create more complex ones. Then, we need an **interpreter** which will be in charge to provide implementation details for the moment when the user decides to run the whole AST providing semantics to it and a `Monad` instance to resolve all the effects. The user has the power of chosing which interpreter to use and which monad instance he wants to solve the problem. That enables testing, since we can easily remove our real side effects in the app at our testing environment by switching the interpreter by a fake one.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve all effects / perform execution of effects in a controlled context

README.md Outdated
TBA. This is going to be another cool approach using FP, but still not ready.
This FP style is very trendy. We are applying it over Android thanks to Kategory here, on the `free-monads` project module. It's highly recommended to take a look at [this PR](https://github.com/JorgeCastilloPrz/KotlinAndroidFunctional/pull/6) in order to understand the approach.
**Free Monads** is based on the idea of composing an **AST** (abstract syntax tree) of computations with type `Free<T>` which will never depend on implementation details but on abstractions defined by an algebra, which is an algebraic data type (ADT). We are defining it through a `sealed` class on this sample.
Those ops can be combined as blocks to create more complex ones. Then, we need an **interpreter** which will be in charge to provide implementation details for the moment when the user decides to run the whole AST providing semantics to it and a `Monad` instance to resolve all the effects. The user has the power of chosing which interpreter to use and which monad instance he wants to solve the problem. That enables testing, since we can easily remove our real side effects in the app at our testing environment by switching the interpreter by a fake one.

# Goals and rationales
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rationale

@JorgeCastilloPrz JorgeCastilloPrz merged commit d97f002 into master Oct 7, 2017
@JorgeCastilloPrz JorgeCastilloPrz deleted the free-monads-sample branch October 7, 2017 15:22
@JorgeCastilloPrz JorgeCastilloPrz restored the free-monads-sample branch October 7, 2017 15:28
@JorgeCastilloPrz JorgeCastilloPrz deleted the free-monads-sample branch October 7, 2017 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Free Monads sample module
2 participants