-
-
Save colomboe/20f14cb1c42dd823095d1cd7022186af to your computer and use it in GitHub Desktop.
| // Porting of https://gist.github.com/jdegoes/dd66656382247dc5b7228fb0f2cb97c8 | |
| typealias UserID = String | |
| data class UserProfile(val name: String) | |
| // The database module: | |
| interface DatabaseService { | |
| suspend fun dbLookup(id: UserID): UserProfile | |
| suspend fun dbUpdate(id: UserID, profile: UserProfile) | |
| } | |
| // The logger module: | |
| interface LoggerService { | |
| suspend fun logInfo(id: String) | |
| } | |
| // A concurrent-safe test database service, which uses a `Ref` to keep track | |
| // of changes to the test database state: | |
| class DatabaseTestService(val ref: Ref<ForIO, State>) : DatabaseService { | |
| data class State(val map: Map<UserID, UserProfile>, val ops: List<String>) { | |
| fun log(op: String): State = copy(ops = listOf(op) + ops) | |
| fun lookup(id: UserID): Tuple2<State, Option<UserProfile>> = | |
| Tuple2(log("Lookup($id)"), map.getOption(id)) | |
| fun update(id: UserID, profile: UserProfile): State = | |
| copy(map = map.plus(id to profile)).log("Update($id, $profile)") | |
| } | |
| override suspend fun dbLookup(id: UserID): UserProfile = | |
| ref.modify { it.lookup(id) }.fix().suspended().orNull()!! | |
| override suspend fun dbUpdate(id: UserID, profile: UserProfile) = | |
| ref.update { it.update(id, profile) }.fix().suspended() | |
| } | |
| // A concurrent-safe test logger service, which uses a `Ref` to keep track | |
| // of log output: | |
| class LoggerTestService(val ref: Ref<ForIO, List<String>>) : LoggerService { | |
| override suspend fun logInfo(line: String) = ref.update { it.plus(line) }.fix().suspended() | |
| } | |
| interface Env : DatabaseService, LoggerService | |
| // A helper function to run a test scenario, and extract out test data. | |
| // This function can be used many times across many unit tests. | |
| fun <A> testScenario(state: State, program: suspend Env.() -> A) : IO<Tuple3<Either<Throwable, A>, State, List<String>>> = | |
| IO.fx { | |
| val databaseRef = !ref { state } | |
| val loggerRef = !ref { emptyList<String>() } | |
| val env: Env = object : Env, | |
| DatabaseService by DatabaseTestService(databaseRef), | |
| LoggerService by LoggerTestService(loggerRef) {} | |
| val either = !attempt { env.program() } | |
| val dbState = !databaseRef.get() | |
| val loggerState = !loggerRef.get() | |
| Tuple3(either, dbState, loggerState) | |
| } | |
| // An example program that uses database and logger modules: | |
| suspend fun Env.lookedUpProfile(): UserProfile { | |
| val profile = dbLookup("abc") | |
| logInfo(profile.name) | |
| return profile | |
| } | |
| // Running a test scenario and unsafely executing it to see what happens: | |
| fun main() { | |
| val program = testScenario(State(mapOf("abc" to UserProfile("testName")), emptyList()), Env::lookedUpProfile) | |
| val result = program.unsafeRunSync() | |
| println(result) | |
| } |
It's the suspend version of raiseError. It's like IO.effect { none<Int>().getOrElse { throw RuntimeException("none") } }, it would look nicer with BIO tho.
You're right, I modelled it just following the Scala code, in a real implementation I'd model it as an Option.
I can't manage to make this run with arrow 0.10.3
I was able to make some parts type check again like :
val databaseRef = !Ref(state)
val loggerRef = !Ref(emptyList<String>())
But I've still some errors. Would it be possible to post a fully runnable example ?
Cheers,
I'm waiting for an API freeze before updating my Arrow examples, since the API in changing very fast in every release. But I'll try to update this if I find some time to work on it.
In the meanwhile, here is the same example with KIO bifunctor, that is much more similar to ZIO:
https://gist.github.com/colomboe/26a480ebd58130decc6ba8bba5b55272
In the future also Arrow should have something similar.
Thank you very much for taking the time to answer. It is indeed more similar / and a bit more straightforward. I'll try experimenting with it :)
dbLookup(id: UserID): UserProfileshould model absence with nullable types, if at all possible. That!!is asking for trouble :D