Based on my understanding, in CLEAN architecture, ...
# arrow
k
Based on my understanding, in CLEAN architecture, a use case is essentially a class you pass setup stuff to (like repository, etc.) and then later you’ll pass params to it to get some meaningful info. For example
Copy code
class UseCase(private val repository: MyDatabase) {
    operator fun invoke(file: File): Long { repository.getSomeLong() }
}

fun test() {
    val repo: MyDatabase = TODO()
    val useCase = UseCase(repo)
    // some hundreds of lines later
    useCase(File("/path/to/foo"))
}
I haven’t used
State
yet, but the descriptions I see largely seem like it’s somewhat similar in concept. You create an object with parameters representing things you’ll read/write to (like a repository) and later do something with it like DB insertions, retrievals, all to manage app state or something? Is my understanding correct?
c
I understand the state monad as a replacement for mutable state. For injection of dependencies (like a repository) the reader monad seems like a appropriate abstraction. Just my opinion (kind of FP newbie myself)
👍 2
j
@Christian Maus that is correct. "Dependency injection" with the reader monad is very common in the fp world, In the haskell world this is called the
ReaderT
pattern,
ZIO
(from scala) has a version of this built in in their
IO
monad. First of: a reader is a wrapper around a partially applied function:
Reader<R, A> = (R) -> A
and the transformer version
ReaderT<F, R, A> = (R) -> Kind<F, A>
. This means this pattern is no different from the code example @kyleg posted. The benefits are: typeclass instances (it is a monad (and a few other things), which means you can also do
fx
comprehensions over it) Second: Using a reader in arrow depends on how you use arrow: If you use arrow fx, you should use
ReaderT<ForIO, R, A>
to make this pattern work well with
IO
. There is also plans to make this pattern in-built into
IO
because it is so common, so using this for now and refractoring to the inbuilt version later should be easy as they are isomorphic. If you don't use arrow fx using the normal
Reader
should work fine ^^ The
UseCase
class is technically no different from
Reader<MyDatabase, (File) -> Long>
. This is better modeled as
(File) -> Reader<MyDatabase, Long>
but those two work basically the same anyway. The benefit is, that it can be composed easily
👍 2
k
Hm, for some reason I thought that State was basically just a Reader and a Writer put together. I haven’t dug into any of the three yet but when I’d see tutorials, often they’d discuss all three in the same article. I guess since I knew a state was something you’d read and write, I assumed that that’s what it was, haha. Kind of like a ReadStream and WriteStream put together is aReadWriteStream
j
It is if you look at the representation for both:
ReaderT = (R) -> Kind<F, A>
,
WriterT = Kind<F, Tuple2<W, A>>
now lets compose them with `R == W`:
ReaderT<WriterTPartial<F, R>, R, A> == (R) -> WriterT<F, R, A> == (R) -> Kind<F, Tuple2<R, A>> == StateT<F, R, A>
. So in theory they are the same, but the semantics of
Writer
and
Reader
are different which means the composition of the two does not work in practice (mainly because the output from the writer is never fed into the reader, so we actually have no changeable state). They are only equal in representation 😅 They also all evolved from similar problems and they are the most fundamental monad-transformers out there (Their
Id
variants
ReaderT<ForId, *, *> == Reader<*, *>
are also really easy to explain). So anyway here are the problems which led to ppl using those types: • Constantly passing around parameters or partially applied functions for DI is solved by
Reader
turning
(A) -> B
into
Reader<A, B>
which allows the function to use the context, but doesn't force callers to provide it (they just need to return a
Reader<A, B>
like as well. • Constantly passing around accumulated logs, values etc is solved by
Writer
turning a function returning
(W, B)
into a function returning
Writer<W, B>
, this has the same benefits, but abstracts away the log. • Constantly passing around and feeding changed state is solved by
State
turning functions
(S) -> (S, A)
into
State<S, A>
again providing a nice abstraction.
❤️ 1
k
Dammit @Jannis you are too helpful and kind
❤️ 1
a
@Jannis great explanation 👏 if you would like to do something similar over IO, would it be possible to use Kleisli there?
j
Do you mean using a
Reader
in
IO
context? In that case
Kleisli<ForIO, R, A>
will do just fine.
ReaderT
is also just a typealias for
Kleisli
anyway.
👌 1
k
So I dived straight into refactoring my code to learn about readers, and it seems that for anything of type
Reader<A, B>
created with
Reader { context -> returnsB() }
I have to add on
.run { Id(this) }
at the end. Same goes for
ReaderApi.ask<A>().map context -> returnsB() }
Did Arrow change the API for this? Because looking at this sample project using Arrow Reader, they don’t appear to have had to do this. He actually imports Reader from
arrow.data
but I have to import it from
arrow.mtl
as it does not appear to exist in
arrow-data
module anymore.
For instance,
Copy code
typealias AppReader<T> = Reader<AppContext, T>

fun scanVideo(root: String): AppReader<IO<Sequence<File>>> = Reader { ctx ->
    Id(IO.effect { /*snip*/ })
}
j
Yes that is quite outdated.
Reader
is an alias for
ReaderT<ForId, R, A>
which is an alias for
Kleisli<ForId, R, A>
(for category theory reasons I think). So using the constructor invokes the
Kleisli
constructor which is
f: (R) -> Kind<F, A>
. The easiest way to get into reader context is to use
ReaderApi
as in the linked repo. Then you can also skip the
Id(...)
ReaderApi.ask<A>().map { context -> returnsB() }
If
returnsB
returns
Reader<A, *>
(given that
context
has type
A
here) what you want would be
ReaderApi.ask<A>().flatMap { ctx -> returnsB() }
. You could also do fx:
Copy code
Reader.monad(Id.monad()).fx.monad { // Not sure if there is a shorthand somewhere, so I'll fully qualify it... You may also need more type params
  val cts = ReaderApi.ask<A>().bind()
  ...
  returnsB().bind()
}
Yes that is quite outdated
Although mtl hasn't changed too much in recent versions, so while stuff got moved, fx and some typeclasses changed, the functions from mtl are still similar, if not the same
k
I think what was throwing me was that I’m running 10.4 but I still have to manually tag
.andThen { Id(it) }
on the end of my
Reader { … }
constructors. It doesn’t appear to be making use of the
Reader(run: …) = ReaderT(run.andThen(Id(it)})
code that Arrow has. I need to figure out why that is.
j
Oh right that exists ^^. That is indeed confusing. I haven't used the non-transformer variants enough...