Updated Proposal: Revisiting Implicits

I spent some time attempting to convert the typeclasses in my project to the extension method style you suggested and I found the cases which caused the trouble.

The issue is that typeclasses like Monoid work really well with the style you’re recommending because they only have one type, and all methods are infix.

That style doesn’t work for typeclasses which involve multiple types (like Convertible), or have a postfix method that requires a type parameter (like Monad).

Interestingly, while a simplified version of the Monad definition in the “Implementing Typeclasses” docs did compile, the call site couldn’t resolve the extension method without an ops class. As the docs only show the definition, but no example of use, my guess is this is why it slipped through whatever tests you may have to ensure the docs compile.

So yes, there is currently boilerplate, and an import tax.

I’d prefer to minimize the boilerplate where feasible, and understand that it isn’t always going to be doable.

I’m fine with the import tax on syntax, managing the instances has always been the bigger headache.

I’m decidedly less fine with being told the import tax doesn’t exist when it’s fairly easy to show that it does.

Full example follows:

trait Convertible[A,B] {
  def (a: A) cast: B
}

object Convertible {
  def apply[A,B](given C: Convertible[A,B]): Convertible[A,B] = C

  given Convertible[Int, String] {
    def (a: Int) cast: String = s"<$a>"
  }
  
  object ops {
    given ops[A]: AnyRef {
      def[B](a: A) cast (given C: Convertible[A,B]): B = C.cast(a)
    }
  }
}

trait Monad[F[_]] {
  def [A, B](x: F[A]) bind(f: A => F[B]): F[B]
  def [A, B](x: F[A]) fmap(f: A => B): F[B] = x.bind(f `andThen` pure)

  def[A](x: A) pure: F[A]
}
object Monad {
  def apply[F[_]](given M: Monad[F]): Monad[F] = M
  
  given Monad[List] {
    def [A, B](xs: List[A]) bind(f: A => List[B]): List[B] = xs.flatMap(f)
    def[A](x: A) pure: List[A] = x :: Nil
  }
  object ops {
    given lifts[A]: AnyRef {
      def[F[_]] (a: A) pure (given M: Monad[F]): F[A] = M.pure(a)
    }
    given syntax[F[_], A]: AnyRef {
      def[B](fa: F[A]) fmap (f: A => B)(given M: Monad[F]): F[B] = M.fmap[A,B](fa)(f)
      def[B](fa: F[A]) bind (f: A => F[B])(given M: Monad[F]): F[B] = M.bind[A,B](fa)(f)
    }
  }
}

object Test extends App {
  def labeled(label: String, result: Any): Unit = {
    println(s"$label: $result")
  }
  {
    println("No Imports")
    println("----------")
    
    labeled("Direct call (Convertible)", Convertible[Int,String].cast(5))
    labeled("Direct call (Monad)", Monad[List].pure(5))
    
    labeled(
      "Call with type parameter",
      "error message is 'value cast is not a member of Int - did you mean (5 : Int).+?'"
      //5.cast[String]
    )
    labeled(
      "Postfix call",
      "error message is 'value pure is not a member of Int - did you mean (5 : Int).+?'"
      //5.pure[List]
    )
    labeled(
      "Infix call",
      "error message is 'value fmap is not a member of List[Int] - did you mean List[Int].map?'"
      //List(4).fmap(_.toString).bind(x => List(x, x))
    )
  }
  
  println(". ")
  
  {
    println("Extra Imports")
    println("-------------")
    import Convertible.ops.given
    import Monad.ops.given
    
    labeled("Call with type parameter", 5.cast[String])
    labeled("Postfix call", 5.pure[List])
    labeled(
      "Infix call",
      List(4).fmap(_.toString).bind(x => List(x, x))
    )
  }
}

Live Version on Scastie