Classes in OOP couple variables to methods via a scope, thus making refactors painful as we cannot easily move methods from one place into another. The entire dependency graph, with initialisation, of the class must be moved with the method somehow. A common "solution" in OOP is dependency injection frameworks.
E.g. in OOP
class Foo(client: Client) {
def dumbCouplingOfMethodToScope: Unit = client.send("hello world")
}
In Scala the solution to this is to use implicit parameters, usually called a "Context". E.g.
object Foo {
def methodNotDependOnScope(client: Client): Unit = client.send("hello world")
}
But a problem occurs when we want to do dynamic dispatch, i.e. be able to pass in multiple implementations of the Context (usually for testing), and when the Context itself depends on another Context. E.g.
trait MethodsNeedContext[Context] {
def method1(p1: Int)(implicit context: Context): Int
}
object ExampleUsage {
def callsMethod1(implicit contextWithContext: MethodsNeedContext[???], contextsContext: ???): Unit = {
contextWithContext.method1(10)(contextsContext)
}
}
We only know the type ???
at runtime, not at compile time. The OOP way would be to make MethodsNeedContext
an abstract class
, then we can use an existential type. E.g.
abstract class OOPWay[Context <: ContextTypeBound](context: Context) {
def method1(p1: Int): Int = context.send("hello world")
}
object ExampleUsage2 {
def callsMethod1(implicit contextWithContext: OOPWay[_ <: ContextTypeBound]): Unit = {
contextWithContext.method1(10)
}
}
But if we wrap the contextWithContext
and contextContext
into a case class, then add an implicit class
then we can solve the problem. Full example below:
trait ContextTypeBound
trait MethodsNeedContext[Context <: ContextTypeBound] {
def method1(p1: Int)(implicit ctx: Context): Int
def method2(p2: String)(implicit ctx: Context): String
}
case class MethodsWithContext[T <: ContextTypeBound](methods: MethodsNeedContext[T], context: T)
object Pimps {
implicit class PimpedMethodsWithContext[T <: ContextTypeBound](methods: MethodsWithContext[T]) {
def method1(p1: Int): Int = methods.methods.method1(p1)(methods.context)
}
}
import Pimps.PimpedMethodsWithContext
case class FooContext(foo: Int) extends ContextTypeBound
object FooExample extends MethodsNeedContext[FooContext] {
def method1(p1: Int)(implicit ctx: FooContext): Int = ???
def method2(p2: String)(implicit ctx: FooContext): String = ???
}
case class BobContext(foo: Int) extends ContextTypeBound
object BobExample extends MethodsNeedContext[BobContext] {
def method1(p1: Int)(implicit ctx: BobContext): Int = ???
def method2(p2: String)(implicit ctx: BobContext): String = ???
}
object ExampleUsage {
def callsMethod1(implicit contextWithContext: MethodsWithContext[_ <: ContextTypeBound]): Unit = {
contextWithContext.method1(10)
}
callsMethod1(MethodsWithContext(FooExample, FooContext(20)))
// Does not compile
callsMethod1(MethodsWithContext(BobExample, FooContext(20)))
}
My question(s) are:
- Does this pattern have a name? Is it a standard Scala practice?
- Is this a Scala only thing, or do other FP languages have similar mechanisms to do the same thing? If other FP languages have different mechanisms, what are they?
Aucun commentaire:
Enregistrer un commentaire