Skip to content

Instantly share code, notes, and snippets.

@raulraja
Last active December 28, 2021 21:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raulraja/e82e352c10f7e158758ff48aca733d3c to your computer and use it in GitHub Desktop.
Save raulraja/e82e352c10f7e158758ff48aca733d3c to your computer and use it in GitHub Desktop.
Services as composable suspended functions
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