views:

101

answers:

1

Hi, all,

In Scala, the interaction of overloading and implicit argument resolution seem to make it impossible to make the following code usable.

trait Bijection[A, B] extends Function1[A, B] with Unapply[A, B] { self =>
  def apply(a: A): B
  def unapply(b: B): A
}

sealed trait Unapply[A, B] {
  def unapply(b: B): A
}

object Bijection {
  implicit def biject[A](a: A): Biject[A] = new Biject(a)

  implicit object IntStringBijection extends Bijection[Int, String] {
    override def apply(a: Int): String = a.toString
    override def unapply(b: String): Int = b.toInt
  }
}

sealed class Biject[A](a: A) {
  def as[B](implicit f: Function1[A, B]): B = f(a)
  def as[B](implicit f: Unapply[B, A]): B = f unapply a
}

The goal here is for a.as[B] to perform a typesafe conversion irrespective of whether a Bijection[A,B] or a Bijection[B,A] is available in implicit scope.

The reason that this doesn't work is that implicit resolution appears to take place after overload disambiguation in the compiler, and since both implementations of 'as' have the same result type, the compiler doesn't even get around to attempting to find out whether an appropriate implicit is in scope that can perform the conversion. In short, implicit resolution is not used in overload disambiguation.

The reason I want to have 'as' overloaded is to avoid the need for the user of this library to need to encode the "direction" of the bijection at the call site; obviously one could implement Biject as this:

sealed class Biject[A](a: A) {
  def viaForward[B](implicit f: Function1[A, B]): B = f(a)
  def viaReverse[B](implicit f: Unapply[B, A]): B = f unapply a
}

but this is really unappealing because it essentially makes the pimp superfluous; one might as well explicitly pass the bijection, but then of course you lose the ability to have the bijection that is used vary based upon the scope.

Is there any good solution to this problem?

+4  A: 

How's this?

trait Bijection[A, B] extends Function1[A, B] with Unapply[A, B] {
  self =>
  def apply(a: A): B

  def unapply(b: B): A
}

sealed trait Unapply[A, B] {
  def unapply(b: B): A
}

object Bijection {
  implicit def biject[A](a: A): Biject[A] = new Biject(a)

  implicit object IntStringBijection extends Bijection[Int, String] {
    override def apply(a: Int): String = a.toString

    override def unapply(b: String): Int = b.toInt
  }
}

sealed class Biject[A](a: A) {
  def as[B](implicit f: Either[Bijection[A, B], Bijection[B, A]]): B = f.fold(_ apply a, _ unapply a)
}

trait EitherLow {
  implicit def left[A, B](implicit a: A): Either[A, B] = Left(a)
}

object Either extends EitherLow {
  implicit def right[A, B](implicit b: B): Either[A, B] = Right(b)
}

import Bijection._
import Either._

1.as[String]
"1".as[Int]
retronym
That's awesome, retronym, thanks! It had occurred to me that Either might need to play a role, but it never occurred to me to project the bijection itself into Either space. If I understand this correctly, the reason that this works is the very reason that my initial attempt didn't: the initial implicit conversion to Biject is "complete" before implicit resolution for the parameter to 'as', which enables the left and right implicit conversions to apply.
Kris Nuttycombe
The key to this is using implicit prioritization to avoid ambiguity in the case of `1.as[Int]`.
retronym
It appears that this works even without the low priority implicits trick. Both left() and right() can be declared in the Bijection object, (and they can be specialized so that they only apply to Bijection instances) and it still works.
Kris Nuttycombe
Here's a version with the implicit conversions tightened up: http://paste.pocoo.org/show/270683/
Kris Nuttycombe