Having made the switch to extensive use of extensi...
# arrow
j
Having made the switch to extensive use of extension functions, I'm struggling with how to organize them. OO was simpler in this way, because you simply added methods to the objects on whose data the methods operated, mostly. What general strategies do others apply in the context of FP? What's the impact of these strategies on unit testing the extension functions? For example, when extension functions on different receivers are all top-level and so share the same scope, isn't it more tricky to tease them apart in order to unit test each function in isolation from the others it may depend on in that scope? Or, in this scenario, does the "unit" in unit testing become the function under test AND all the other functions that it calls? If so, doesn't that become unwieldy as functions stack up on top of each other?
r
Do you have an example of code where organization with ext funs is an issue?
They should be fine you can even have generic args as receivers.
j
Thanks @raulraja. If you don't mind, I'm going to take my question in a slightly different direction in order to get to a more fundamental question I have. Suppose we have this: #1
Copy code
class A
class B
class C

fun A.f(): C = B().f()
fun B.f(): C = C()
Above, the extension function
B.f()
is an implicit dependency of
A.f()
. When
A.f()
is a large function with many such dependencies on other extension functions that are available in declaration scope, it makes testing more difficult. Because one can't simply unit test
A.f()
by providing different arguments to
A.f()
.
A.f()
has no declared arguments (besides the implicit receiver). #2
Copy code
fun A.f2(bf: B.() -> C): C = B().bf()
Here,
A.f2()
is a higher order function whose function dependencies are explicitly defined. These dependencies are immediately and easily identifiable when writing unit tests. We can provide an implementation of
B.() -> C
that's different from what we use in production, in order to setup the specific conditions under which we want to test
A.f2()
. I see a lot of #1 in Kotlin code, but never #2. #2 is noisy and becomes a nuisance because we must provide all these function arguments whenever we make a call. Perhaps in that case they can be grouped together, as in #3. #3
Copy code
interface K {
    fun B.f(): C = TODO()
    fun C.g(): D = TODO()
}

fun A.f3(k: K): C = with (k) { 
    B().f()
    C().g()
    C()
}
#1 and its testability can be improved by introducing interfaces as scopes that can be depended on and overridden, as in #4. #4
Copy code
interface J {
    fun B.f(): C = C()
}

interface H : J {
    fun A.f(): C = B().f()
}

// In our tests of A.f() we can override B.f() to provide some alternative C that's specific to the use case we want to verify.

val h = object : H {
    override fun B.f(): C = TODO()
}

// Uses our override
fun test(): C = with (h) {
    A().f()
    B().f() 
}
An alternative to #3 might be #5. It partially applies extension function dependencies to
A.f()
and we end up with an
A.(E) -> D
that's easier to use and pass around. #5
Copy code
class A
class B
class C
class D
class E

fun B.f(): C = TODO()
fun C.f(): D = TODO()
fun A.f(bf: B.() -> C, cf: C.() -> D, e: E): D = TODO()
val af: A.(E) -> D = A::f.partially2(B::f).partially2(C::f)
val d = A().af(E())
If anything, I have too many options.
I think it all boils down to: In pragmatic FP in Kotlin, should higher order functions be explicit in their function-type parameters, or is it OK to have them depend implicitly on other functions that are declared in the same scope?
r
I think the question here is regarding Kotlin support for native DI
which is what you need for tests etc.
The answer. it already does but you have to Follow the Interface, ext fun, module approach. that is, use type args and type bounds and the compiler can give you a full polymorphic graph verified at compile time you can override at will
you can do the same with top level functions with type args constrained by interface
then use the interface inside
at the edge of the app make a module
instead people prefer annotation processors than a much faster type system that can makes it all concrete at the edge since it already supports implicits
you just need to make what you want to override an interface and use always a type arg as receiver constrained by what you want it to support at the edge.
Most people in Kotlin are unaware that this can be done
My thoughts are that type arguments are for those cases where we don’t know something. In this case if you don’t know how your things are injected you can use type arguments over your unknown receiver. This is a most ergonomic and performant approach than reader since type args are erased and since interfaces let you define val and fun you have singleton and prototype strategies for DI covered.
Kotlin as Scala, having implicits has already resolved the DI problem, you just need to write a proof at the end
Is this popular?. Nope xD
j
Thank you. The gist you linked to is quite useful. But I'm not sure it completely addresses the question of dependency that I'm trying to ask. The code in the gist addresses the issue of dependency between interfaces and their concrete implementations. For example,
DefaultUI
doesn't depend on
InMemoryData
. Rather it depends on
Data
. A
Data
implementation of
InMemoryData
is provided at the edge of the program. So this is dependency between interfaces/classes. Whereas I'm interested in dependency within a single interface/class. In the gist the unit of dependency is the interface/class. The unit of dependency I'm asking about is the function. Though it's all on the topic of dependency, the scale of what I'm asking about is more granular. I'll use the gist to try to express myself more clearly. Suppose
DefaultDomain
looked like this:
Copy code
class DefaultDomain : Domain {
  fun Int.roundUpOrDown() = TODO()
  override fun <R> R.getProcessedAccount(): IO<Account> where R: Data =
    fetchAccount().map { Account(it.balance.roundUpOrDown()) }
}
Now, you want to unit test
DefaultDomain R.getProcessedAccount()
. But you want to avoid actually calling the real implementation of
DefaultDomain Int.roundUpOrDown()
. Maybe because of overhead. Maybe because you want to test
roundUpOrDown
separately. You will write two tests of
getProcessedAccount()
, one where the balance is rounded up. And another when the balance is rounded down. To do this you need to provide an alternate implementation of
roundUpOrDown
. This implementation would return a hardcoded rounded up number in the first case, and a hardcoded rounded down number in the second case. What's the best way to do this?