wondering if anyone have some input on a broader f...
# arrow
p
wondering if anyone have some input on a broader functional design question. is there a good way to group functions as dependencies (if that’s even a good way to think about it), simple example of a data store:
Copy code
fun saveSession(session: Session): Either<Throwable, Unit>
fun getSession(id: SessionID): Either<Throwable, Session>
for a specific implementation (Redis vs Relational vs mock) with additional dependencies:
Copy code
fun saveSession(connection: RedisConnection, session: Session): Either<Throwable, Unit>
fun getSession(connection: ResdisConnection, id: SessionID): Either<Throwable, Session>
this prevents grouping functions as an interface, even if using partial functions (i think) or using an interface OO-style:
Copy code
class RedisSessionManager(connection: RedisConnection): SessionManager {
fun saveSession(session: Session): Either<Throwable, Unit>
fun getSession(id: SessionID): Either<Throwable, Session>
}
side effects aside, no longer pure with the global external dependency Reader monad perhaps? not sure one ever gets away from the external implementation specific dependency (eg: connection), what approaches have you taken?
n
I like to use interfaces together with a reader monad based on a receiver parameter with generic bounds for something like this. For getting away from the specific types (e.x. RedisConnectiom) -- you can just add generic parameters to your interfaces, e.x.
interface SessionCtx<Conn,SId
,
Copy code
Sess> {
    fun getSession(connection: Conn, sessionId: SId): Either<Throwable, Sess>
}
p
nice thanks @Nathan Bedell let me explore that a bit
r
My preference is to use extension functions with dependencies as receivers. IMO There is no advantage in Kotlin using Reader or ReaderT when you already have extension functions, soon multiple contextual receivers and the ability to do these over suspend fun as well. Readers or similar double wrap and add complexity to stack traces through jumps over flatMap or similar. Nathan’s parametric solution looks good and if you separate each operation into
fun interfaces
instances can also be constructed much easier and independently.
p
soon
p
thank you @raulraja - insightful comments as always
do you happen to have any examples?
r
@Peter I created this alternative encoding showing more or less an alternative based on values and types that is not so focused on interfaces and impl
this approach starts with the premise that
Copy code
* 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>
}
then it offers a way to compose those services and defer their execution. Depends on the level you want to approach your architecture the concerns for readers or injection dissapears and you can just use plain extension functions. In this case I lifted your concerns as to how to define services to ignore subtyping and hierarchies not impose in the implementers the fact that all those methods you are implement like get, set, etc they all have the same shape.
Now is not clear what a
SessionManager
may be but you can create its operation from Get, Set ad-hoc and simply write the concrete ones that match the shape of a
Service
to work with the way you compose them.
p
thank you so much @raulraja! - very interesting approach 👀
🙏 played around with this a bit, i do like the dependency as receiver! keeps function parameters clean, especially in the case where the dependency is not always required (eg. mocking our Session operations, no persistence store involved) feeling a bit unsure about the operations all having the same shape vs each operation having it’s own shape, eg: • Get: (SessionID) -> Either<Failure, Session> • Save: (Session) -> Either<Failure, Unit> especially in a nested design (service -> service -> service), i think one would want the operations to have a well known shape independent of concrete implementation? not sure it’s possible to do that and retain the functional interfaces and if those aren’t possible, then the combinators might not be possible anymore either 😞 need to play a bit more with this
r
What does a
nested design (service -> service -> service)
mean? Is there a benefit for this kind of architecture? it still looks like the abstraction in that shape is 3 suspend functions that execute one after another consuming the result of the previous.
p
oh sorry I meant ServiceA depends on ServiceB depends on ServiceC
r
In this case
RedisManager
depends on default impls of your functions which you can replace in testing simply creating a new instance with an altered get or set.
Copy code
class RedisManager(
  val connection: RedisConnection,
  val save: (session: Session) -> Save<Unit> = connection::saveRedisSession,
  val get: (session: SessionID) -> Get<Session> = connection::getRedisSession
)
yet using agregration and still decoupled from any formal hierarchy. Of course you can introduce interfaces and other restrictions over types but at the end of the day is also possible to just use functions or fun interfaces to express any concern as a value that you can pass around and execute where needed.
But not trying to demonstrate this is superior or anything like that, I think the approach you had originally is great just wanted to show some alternatives ways to look at it and ultimately that most ReaderT etc are not worthy with contextual functions and this kind of lang support for them.
p
But not trying to demonstrate this is superior or anything like that
- absolutely! just trying learn about alternatives, great to be aware of various approaches to pick the best one for the situation at hand. this looks pretty good!
thank you again!
👍 1