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
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -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

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


# 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


Expand Down
1 change: 1 addition & 0 deletions free-monads/.gitignore
@@ -0,0 +1 @@
/build
84 changes: 84 additions & 0 deletions free-monads/build.gradle
@@ -0,0 +1,84 @@
buildscript {
repositories {
jcenter()
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
maven { url 'https://maven.fabric.io/public' }
}

dependencies {
classpath "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.0.M11"
}
}

apply plugin: 'kotlin-kapt'
apply from: rootProject.file('gradle/generated-kotlin-sources.gradle')
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.gitlab.arturbosch.detekt'

android {
compileSdkVersion compileVersion
buildToolsVersion build_tools_version

defaultConfig {
applicationId appId
minSdkVersion minSdk
targetSdkVersion targetSdk
versionCode version_code
versionName version_name
testInstrumentationRunner testInstrumentationRunner

buildConfigField "String", "MARVEL_PUBLIC_KEY", marvelPublicKey
buildConfigField "String", "MARVEL_PRIVATE_KEY", marvelPrivateKey
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
debug.java.srcDirs += 'build/generated/source/kaptKotlin/debug'
release.java.srcDirs += 'build/generated/source/kaptKotlin/release'
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}

lintOptions {
abortOnError false
}
}

detekt {
version = "1.0.0.M11"
input = "$project.projectDir.absolutePath"
config = "$project.rootDir/detekt.yml"
filters = ".*test.*,.*/resources/.*,.*/tmp/.*"
output = "$project.projectDir.absolutePath/reports/"
report = "$project.projectDir.absolutePath/reports/"
}

dependencies {
compile project(':shared')

kapt "io.kategory:kategory-annotations-processor:$kategory_version"
compile "com.android.support:appcompat-v7:$supportLibrary"
compile "com.android.support:design:$supportLibrary"
compile "com.android.support:cardview-v7:$supportLibrary"
compile "com.android.support:recyclerview-v7:$supportLibrary"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
compile "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
compile "com.squareup.picasso:picasso:2.5.2"
compile "io.kategory:kategory-annotations:$kategory_version"
compile "io.kategory:kategory:$kategory_version"
compile "io.kategory:kategory-effects:$kategory_version"
compile 'com.karumi:marvelapiclient:1.0.1'
}
21 changes: 21 additions & 0 deletions free-monads/proguard-rules.pro
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.github.jorgecastillo.kotlinandroid;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest {
@Test public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();

assertEquals("com.github.jorgecastillo.free_monads", appContext.getPackageName());
}
}
25 changes: 25 additions & 0 deletions free-monads/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.jorgecastillo.kotlinandroid">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".view.SuperHeroListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

<activity
android:name=".view.SuperHeroDetailActivity"
android:theme="@style/NoActionBar"/>
</application>
</manifest>
@@ -0,0 +1,26 @@
package com.github.jorgecastillo.kotlinandroid.di.context

import android.content.Context
import com.github.jorgecastillo.kotlinandroid.BuildConfig
import com.github.jorgecastillo.kotlinandroid.presentation.SuperHeroDetailView
import com.github.jorgecastillo.kotlinandroid.presentation.SuperHeroesListView
import com.github.jorgecastillo.kotlinandroid.presentation.SuperHeroesView
import com.karumi.marvelapiclient.CharacterApiClient
import com.karumi.marvelapiclient.MarvelApiConfig.Builder

sealed class SuperHeroesContext {

abstract val ctx: Context
abstract val view: SuperHeroesView

val apiClient
get() = CharacterApiClient(Builder(
BuildConfig.MARVEL_PUBLIC_KEY,
BuildConfig.MARVEL_PRIVATE_KEY).debug().build())

data class GetHeroesContext(override val ctx: Context, override val view: SuperHeroesListView) : SuperHeroesContext()
data class GetHeroDetailsContext(override val ctx: Context,
override val view: SuperHeroDetailView) : SuperHeroesContext()
}


@@ -0,0 +1,35 @@
package com.github.jorgecastillo.kotlinandroid.data

import com.github.jorgecastillo.kotlinandroid.data.CachePolicy.LocalFirst
import com.github.jorgecastillo.kotlinandroid.data.CachePolicy.LocalOnly
import com.github.jorgecastillo.kotlinandroid.data.CachePolicy.NetworkFirst
import com.github.jorgecastillo.kotlinandroid.data.CachePolicy.NetworkOnly
import com.github.jorgecastillo.kotlinandroid.free.algebra.FreeHeroesAlgebra
import com.github.jorgecastillo.kotlinandroid.free.algebra.getAllHeroes
import com.github.jorgecastillo.kotlinandroid.free.algebra.getSingleHero
import com.karumi.marvelapiclient.model.CharacterDto

sealed class CachePolicy {
object NetworkOnly : CachePolicy()
object NetworkFirst : CachePolicy()
object LocalOnly : CachePolicy()
object LocalFirst : CachePolicy()
}

fun getHeroesWithCachePolicy(policy: CachePolicy): FreeHeroesAlgebra<List<CharacterDto>> =
when (policy) {
is NetworkOnly -> getAllHeroes()
is NetworkFirst -> getAllHeroes()
is LocalOnly -> getAllHeroes()
is LocalFirst -> getAllHeroes()
}

fun getHeroDetails(policy: CachePolicy, heroId: String): FreeHeroesAlgebra<CharacterDto> =
when (policy) {
is NetworkOnly -> getSingleHero(heroId)
is NetworkFirst -> getSingleHero(heroId)
is LocalOnly -> getSingleHero(heroId)
is LocalFirst -> getSingleHero(heroId)
}

fun getHeroesFromAvengerComicsWithCachePolicy(policy: CachePolicy): FreeHeroesAlgebra<List<CharacterDto>> = TODO()
@@ -0,0 +1,21 @@
package com.github.jorgecastillo.kotlinandroid.domain.model

import kategory.*

/**
* This sealed class represents all the possible errors that the app is going to model inside its
* domain. All the exceptions / errors provoked by third party libraries or APIs are mapped to any
* of the types defined on this class.
*
* Mapping exceptions to errors allows the domain use case functions to be referentially
* transparent, which means that they are completely clear and straightforward about what they
* return just by reading their public function output types.
*
* Other approaches like exceptions + callback propagation (to be able to surpass thread limits)
* bring not required complexity to the architecture introducing asynchronous semantics.
*/
sealed class CharacterError {
object AuthenticationError : CharacterError()
object NotFoundError : CharacterError()
data class UnknownServerError(val e: Option<Throwable> = Option.None) : CharacterError()
}
@@ -0,0 +1,3 @@
package com.github.jorgecastillo.kotlinandroid.domain.model

data class SuperHero(val name: String)
@@ -0,0 +1,17 @@
package com.github.jorgecastillo.kotlinandroid.domain.usecase

import com.github.jorgecastillo.kotlinandroid.data.CachePolicy.NetworkOnly
import com.github.jorgecastillo.kotlinandroid.free.algebra.FreeHeroesAlgebra
import com.github.jorgecastillo.kotlinandroid.data.getHeroDetails
import com.github.jorgecastillo.kotlinandroid.data.getHeroesFromAvengerComicsWithCachePolicy
import com.github.jorgecastillo.kotlinandroid.data.getHeroesWithCachePolicy
import com.karumi.marvelapiclient.model.CharacterDto

fun getHeroesUseCase(): FreeHeroesAlgebra<List<CharacterDto>> =
getHeroesWithCachePolicy(NetworkOnly)

fun getHeroDetailsUseCase(heroId: String): FreeHeroesAlgebra<CharacterDto> =
getHeroDetails(NetworkOnly, heroId)

fun getHeroesFromAvengerComicsUseCase(): FreeHeroesAlgebra<List<CharacterDto>> =
getHeroesFromAvengerComicsWithCachePolicy(NetworkOnly)
@@ -0,0 +1,38 @@
package com.github.jorgecastillo.kotlinandroid.free.algebra

import com.github.jorgecastillo.kotlinandroid.domain.model.CharacterError
import com.karumi.marvelapiclient.model.CharacterDto
import kategory.*

/**
* Algebra for Hero data sources. Algebras are defined by a sealed class (ADT) with a limited amount of implementations reflecting the operations available.
*/
@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>
}

typealias FreeHeroesAlgebra<A> = Free<HeroesAlgebraHK, A>

inline fun <reified F> Free<HeroesAlgebraHK, List<CharacterDto>>.run(
interpreter: FunctionK<HeroesAlgebraHK, F>, MF: Monad<F> = monad()): HK<F, List<CharacterDto>> =
this.foldMap(interpreter, MF)

/**
* Module definition (Data Source methods). Here we lift to the Free context all the operation blocks defined on the algebra.
*/
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))