:wave:, i bring you another doubt, always thinking...
# arrow
c
👋, i bring you another doubt, always thinking from the FP point of view, purity above all hehe
message has been deleted
with the Service1 one, I have to provide the dependencies each time that i call his functions
with Service2 it would already be provided, but when i call its functions i am hiding its dependencies
Copy code
class App {
    companion object {
        val service2: Service2 by lazy { with(Repository) { createService2() } }
        val service1: Service1 by lazy { Service1 }
    }
}

class Activity() {

    fun onCreate() {
        runBlocking {
            with(Repository) { service1.doSomething1() }
            service2.doSomething1()
        }
    }

    fun onResume() {
        runBlocking {
            with(Repository) { service1.doSomething2() }
            service2.doSomething2()
        }
    }
}

interface Repository {
    suspend fun doSomething(): Either<String, String> =
        "ok".right()
}

interface Service1 {
    context(Repository)
    suspend fun doSomething1(): Either<String, String>

    context(Repository)
    suspend fun doSomething2(): Either<String, String>
}

interface Service2 {
    suspend fun doSomething1(): Either<String, String>
    suspend fun doSomething2(): Either<String, String>
}
s
Hey @carbaj0, This is a bit of a complex question 😄 It's IMO unrelated to FP, or OOP so let me answer the question outside of the domain of paradigms. When we want to model our dependencies explicitely we typically care about our dependencies, and we do not want to expose/leak/model any downstream dependencies since those leak implementation details.
For example, when working with a database in
ServiceX
you probably don't want to expose
SqlDelight
,
Exposed
or
Room
. That is typically also considered counter-productive for testing and is a concern for coupling. So for this reason people typically model their logic as an interface and hide the fact that they're using one of the above database implementations. So in those cases, it's good to hide the dependency.
To come back to your example, both implementations are valid of course but in the case of
Service1
I don't see the need anymore to model it as an interface. It can just be top-level functions without losing any powers, but that is perhaps because this example is a bit too simple.
Copy code
context(Repository)
suspend fun doSomething1(): Either<String, String>
A good example could be that in repo/service architectures we typically define all logic in
Repo
hiding
DB
&
Network
related dependencies, and we use the service layer to compose the repo specific interfaces/algebras. I think with Context Receivers we'll replace the service layer with just top-level functions.
Copy code
context(UserRepo, OrderRepo, EffectScope<ErrorType>)
suspend fun User.getAllOrders(): List<Order> {
  val orderIds = selectOrderIds(userId).bind()
  orderIds.map { orderId -> selectOrder(orderId).bind() }
}
❤️ 2
This way you explicitly model everything you care about. One downside of this approach would be though that you lose the ability to easily stub the service method, which is not the case for
Service1
or
Service2
. Gaining more experience with the language feature, and personal preference or project needs might change how we think and feel about this though.
c
Thanks Simon, I couldn’t have answered my question better hehe ❤️
I am normally used to using Clean Architecture differentiating the layers with interfaces as you say. But when it comes to working with concretions, or domain models, we usually use classes bringing together state and behavior. Lately I’ve been trying to use only functions and ADTs to model an application. (to decouple state and behavior) The hardest part is dependency injection. First experiment by passing them by parameter (certainly unfeasible). Then experiment with partial application. Then I was going to use Reader monad. But the context receivers arrived and I decided to try them. But as you say, I don’t want to expose/leak all these dependencies. Now is when I am trying to find the balance between the purist and what is really useful (in Kotlin).
s
I think I'm following the same approach as you, and I can say I also went through the same stages of learning 😄 dagger -> partial application -> reader monad -> Receiver DI and now I think instead of using classes as you say I redeclare an interface and consume the other interfaces.
Copy code
interface APersistence { }
interface BNetwork { }

interface UseCase {
  suspend fun program(): Either<E, A>
}

fun useCase(persistence: APersistence, network: BNetwork) = object : UseCase { }
The reason I take this approach over classes is that now you can easily make the constructor
suspend
, which gives me the ability to do complex things upon construction in a safe way. Additionally, I never create a new type that can potentially expose additional states or functionality by accident (or by abuse). But it's perhaps just a matter of style. I'm curious to see how with context receivers we can clean this up by replacing
UseCase
with just a top-level function with
context(APersistence, BNetwork)
.
j
the only advantage of using interfaces + implementation over just simple top level functions is parallelizing in multimodule Gradle projects
if a function could be empty in the module A, and being implemented in module B, you even don't need this interface + implementation, but at this moment the only way "to have" functions with no implementation are
fun interface
the reason to depends on interface is, this interface should not change a lot, so you depends on a module which should not be changed a lot, so you dont rebuild constantly. Only the implementation module and the DI module will be rebuilded. So you get a lot of parallelization and even
up to date
or
from cache
tasks.
but IMO you only need this interface in one point, the business rules, you obviously can abstract more and create more layers, but it should be for improving testing or reusability more than parallelizing