Search code examples
scalaimplicit

Scala: how to use an implicit class extension's implementation of a trait


I am trying to provide an extension to a class I can't modify using an implicit class. I have a trait HasFoo[A] that takes a type parameter. I then have a class (Processor) that expects an A that implements HasFoo[A]. The issue is that the compiler doesn't recognize that the Bar class is implementing HasFoo[Bar] via the implicit class extension.

The error is Type argument Bar does not conform to upper bound Playground.HasFoo[Bar]

Is there a way to get the compiler to recognize that the trait HasFoo[Bar] is implemented by the implicit class, or is there a better way to do this?

// Bar is an autogenerated class that I can't modify directly
final case class Bar(baz: String)

trait HasFoo[A] {
  def foo: A
}

implicit class Ext(val bar: Bar) extends HasFoo[Bar] {
  def foo: Bar = bar.copy(baz = s"${bar.baz} else")
}

class Processor[A <: HasFoo[A]]() {}

// This errors out because type `Bar` doesn't implement `foo`,
// even though the implicit extension does.
val process = new Processor[Bar]()

Solution

  • The class Bar implementing the method def foo from the trait HasFoo doesn't make Bar a subtype of HasFoo.

    You can try to make HasFoo a type class rather than just OOP trait (replacing subtype polymorphism and F-bounded polymorphism with ad hoc polymorphism). Third-party classes like autogenerated Bar that can't be modified is exactly a use case for type classes.

    // autogenerated class
    final case class Bar(baz: String)
    
    // type class
    trait HasFoo[A] {
      def foo(a: A): A
    }
    
    // extension method
    implicit class Ext[A: HasFoo](a: A) {
      def foo: A = implicitly[HasFoo[A]].foo(a)
    }
    
    // Bar is an instance of the type class
    implicit val barHasFoo: HasFoo[Bar] = new HasFoo[Bar] {
      override def foo(bar: Bar): Bar = bar.copy(baz = s"${bar.baz} else")
    }
    
    Bar("baz").foo
    
    // replacing F-bound with context bound
    class Processor[A: HasFoo]
    
    val process = new Processor[Bar]
    

    Some intros to type classes:

    What are type classes in Scala useful for?

    https://kubuszok.com/2018/implicits-type-classes-and-extension-methods-part-1/

    https://tpolecat.github.io/2013/10/12/typeclass.html https://tpolecat.github.io/2015/04/29/f-bounds.html

    https://books.underscore.io/shapeless-guide/shapeless-guide.html#sec:generic:type-classes (chapter 3.1)

    https://www.baeldung.com/scala/type-classes

    https://docs.scala-lang.org/scala3/book/types-type-classes.html https://docs.scala-lang.org/scala3/reference/contextual/type-classes.html

    https://gist.github.com/BalmungSan/c19557030181c0dc36533f3de7d7abf4#typeclasses


    In principle you can have both type class and OOP trait (for example if you already have many implementations of the OOP trait and you don't want to modify them) although this seems to be overengineering

    // autogenerated class
    final case class Bar(baz: String)
    
    // OOP trait
    trait HasFoo[A] {
      def foo: A
    }
    
    class HasFooImpl extends HasFoo[HasFooImpl] {
      override def foo: HasFooImpl = new HasFooImpl
    }
    
    // type class
    trait HasFooTC[A] {
      def foo(a: A): A
    }
    
    implicit class Ext[A: HasFooTC](a: A) extends HasFoo[A] {
      override def foo: A = implicitly[HasFooTC[A]].foo(a)
    }
    
    // all (F-bounded) implementations of the OOP trait are instances of the type class
    implicit def hasFooSubtypes[A <: HasFoo[A]]: HasFooTC[A] = new HasFooTC[A] {
      override def foo(a: A): A = a.foo
    }
    
    implicit val barHasFoo: HasFooTC[Bar] = new HasFooTC[Bar] {
      override def foo(bar: Bar): Bar = bar.copy(baz = s"${bar.baz} else")
    }
    
    Bar("baz").foo // extension method
    new HasFooImpl().foo // OOP method
    
    class Processor[A: HasFooTC]
    
    val process = new Processor[Bar]
    val process1 = new Processor[HasFooImpl]
    

    An alternative to the type class is another pattern, the magnet. Magnets are much less popular than type classes (implicit conversions should be used with caution although the above implicit class actually defined an implicit conversion too)

    import scala.language.implicitConversions
    
    // autogenerated class
    final case class Bar(baz: String)
    
    // magnet
    trait HasFoo[A] {
      def foo: A
    }
    
    // implicit conversion from Bar to the magnet
    implicit def fromBar(bar: Bar): HasFoo[Bar] = new HasFoo[Bar] {
      override def foo: Bar = bar.copy(baz = s"${bar.baz} else")
    }
    
    Bar("baz").foo
    
    // replacing F-bound with view bound
    class Processor[A](implicit ev: A => HasFoo[A])
    
    val process = new Processor[Bar]
    

    Group TypeClass instances by type parameter

    Overloading methods based on generics

    Type erasure problem in method overloading

    How to make a typeclass works with an heterogenous List in scala

    Generic function where the return type depends on the input type in Scala?

    Problem with bringing into scope scala implicit conversions

    https://kubuszok.com/2018/implicits-type-classes-and-extension-methods-part-3/#magnet-pattern

    Scala generic method - No ClassTag available for T - when using Collection

    Trying to extract the TypeTag of a Sequence of classes that extend a trait with different generic type parameters


    As @LuisMiguelMejíaSuárez advices in comments, alternatively a wrapper can be used. This would also work if F-bounded Processor couldn't be modified.

    // autogenerated class
    final case class Bar(baz: String)
    
    trait HasFoo[A] {
      def foo: A
    }
    
    final case class BarWrapper(bar: Bar) extends HasFoo[BarWrapper] {
      override def foo: BarWrapper = copy(bar = bar.copy(baz = s"${bar.baz} else"))
    }
    
    class Processor[A <: HasFoo[A]]
    
    val process = new Processor[BarWrapper]