Last active
December 28, 2021 21:37
-
-
Save raulraja/e82e352c10f7e158758ff48aca733d3c to your computer and use it in GitHub Desktop.
Services as composable suspended functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package arrow.meta.continuations | |
import arrow.core.Either | |
import arrow.core.computations.either | |
import arrow.core.flatMap | |
/** | |
* Models errors as values | |
*/ | |
sealed interface Failure | |
object CustomFailure : Failure | |
data class Exceptional(val ex: Throwable) : Failure | |
/** | |
* A service is a suspended function that returns either a Failure or a | |
* computed value of A | |
*/ | |
fun interface Service<out A> { | |
suspend operator fun invoke(): Either<Failure, A> | |
} | |
/** | |
* if functions can form Functors so can services | |
*/ | |
fun <A, B> Service<A>.map(f: (A) -> B): Service<B> = | |
Service { invoke().map(f) } | |
/** | |
* if functions can form Monads so can services | |
*/ | |
fun <A, B> Service<A>.flatMap(f: (A) -> Service<B>): Service<B> = | |
Service { | |
invoke().flatMap { | |
f(it).invoke() | |
} | |
} | |
/** | |
* Some user wishes to describe crud like operations as services | |
* Since we don't know what we are getting A is generic | |
*/ | |
fun interface Get<out A> : Service<A> | |
fun interface Save<out A> : Service<A> | |
/** | |
* There are cases where we are talking about a specific use case | |
* that observes a technology in this case that is the model of redis sessions | |
* and connections | |
*/ | |
object Session | |
object RedisConnection | |
object SessionID | |
/** | |
* to save a redis [Session] we need contextual access to a [RedisConnection] | |
*/ | |
fun RedisConnection.saveRedisSession(session: Session): Save<Unit> = | |
Save { Either.Right(println("saving session $session")) } | |
/** | |
* to get a redis [Session] we need a [SessionID] and access to a [RedisConnection] | |
*/ | |
fun RedisConnection.getRedisSession(session: SessionID): Get<Session> = | |
Get { Either.Right(Session) } | |
/** | |
* Operations like [saveRedisSession] and [getRedisSession] sessions may be combined | |
* ad-hoc at the value level to produce new lazy services. | |
* In this case the [Service] is of [Unit] because it's last operation [saveRedisSession] | |
* returns Unit. | |
*/ | |
fun RedisConnection.customRedisWorkFlow(sessionId: SessionID): Service<Unit> = | |
getRedisSession(sessionId).flatMap(::saveRedisSession) | |
/** | |
* Since services operate in the context of Either we can use either blocks | |
* to create services by using invoke and bind instead of relying on map and flatMap. | |
* this covers the use case when we prefer to bring imperative syntax to our users instead of function combinators | |
*/ | |
fun RedisConnection.customRedisWorkFlow2(sessionId: SessionID): Service<Unit> = | |
Service { | |
either { | |
val session = getRedisSession(sessionId)() | |
saveRedisSession(session.bind())() | |
} | |
} | |
suspend fun main() { | |
/** Every operation results in a program that is a suspended function **/ | |
val program: Service<Unit> = RedisConnection.customRedisWorkFlow(SessionID) | |
/** At the edge of the world or where appropriate you can choose to | |
* run your effects by invoking the program or final function that triggers all others that have been composed | |
**/ | |
val executedProgram: Either<Failure, Unit> = program() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment