-
-
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): UserProfile
should model absence with nullable types, if at all possible. That!!
is asking for trouble :D