Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid code duplication using duck typing in scala

I am currently coding in scala and I consider my self a newbie. I have 3 classes that are out of my control, that means that I can't change them.

class P

class A {
  def m(p: P): A = { println("A.m"); this }
}

class B {
  def m(p: P): B = { println("B.m"); this }
}

This is a simplified example the actual code is more complicated and classes A, B have many other similar methods.

I need to call method m for instances of classes A, B

The obvious solution is:

def fill(ab: AnyRef, p: P): Unit = {
  ab match {
    case a: A => a.m(p)
    case b: B => b.m(p)
  }
}

but that involves code duplication. I tried to solve it with duck typing and so far my best take on the subject is this:

type WithM[T] = { def m(p: P): T }

def fill[S, T <: WithM[S]](ab: T, p: P): S =
  ab.m(p)

fill(new A, new P)

but I get type inference errors like:

Error:(18, 5) inferred type arguments [Nothing,A] do not conform to method fill's type parameter bounds [S,T <: Test.WithM[S]]
fill(new A, new P)
^

Can this problem be solved in an elegant way with minimal magic?

like image 658
fusion Avatar asked Aug 19 '15 18:08

fusion


Video Answer


1 Answers

You've got a few options. One is to provide the type parameters explicitly:

scala> fill[A, A](new A, new P)
A.m
res1: A = A@4beb8b21

If the m method always returns a value of the type that it's defined on, you can help out the type inference by encoding that fact in your fill:

scala> def fill[T <: WithM[T]](o: T, p: P): T = o.m(p)
fill: [T <: WithM[T]](o: T, p: P)T

scala> fill(new A, new P)
A.m
res2: A = A@5f9940d4

You can also skip the type alias:

scala> def fill[S](o: { def m(o: P): S }, p: P): S = o.m(p)
fill: [S](o: AnyRef{def m(o: P): S}, p: P)S

scala> fill(new A, new P)
A.m
res3: A = A@3388156e

I'd strongly suggest using a type class, though—it's a little bit of syntactic overhead but much cleaner:

trait HasM[T] {
  type Out

  def apply(t: T, p: P): Out
}

object HasM {
  type Aux[T, Out0] = HasM[T] { type Out = Out0 }

  implicit def AHasM: Aux[A, A] = new HasM[A] {
    type Out = A
    def apply(t: A, p: P): A = t.m(p)
  }

  implicit def BHasM: Aux[B, B] = new HasM[B] {
    type Out = B
    def apply(t: B, p: P): B = t.m(p)
  }
}

def fill[T](t: T, p: P)(implicit hm: HasM[T]): hm.Out = hm(t, p)

And then:

scala> fill(new A, new P)
A.m
res4: A = A@74e92aa9

scala> fill(new B, new P)
B.m
res5: B = B@1ea35068

No reflective access and you're using a widely-understood idiom.

like image 108
Travis Brown Avatar answered Sep 27 '22 19:09

Travis Brown