This page is no longer maintained — Please continue to the home page at www.scala-lang.org

Feature request : implicit resolution disambiguation

No replies
Maxime Lévesque
Joined: 2009-08-18,
User offline. Last seen 42 years 45 weeks ago.

I'd like to make a case for an enhacement to implicit conversion resolution which
happened to be an unspecified behavior in 2.8.x.
I will describe a pattern that becomes possible with the 2.8.x behavior.

First some background on the problem that the pattern tries to solve :

SQL allows expressions like these :

  aNullableInt * 5

which simply returns null if aNullableInt happens to be null.

Contrast this with the Scala equivalent, where  aNullableInt is an Option[Int] :

  aNullableInt * 5

it will not compile in Scala.

Squeryl's DSL enforce the typing rules of SQL which differs from Scala's.
This pages explains it : http://squeryl.org/type-mapping.html

Further examples :

class User(val id: Int, val age: Int, val birthDate: Date)

//The return types of these queries :

//q1-
val avgAge: Option[Int] = from(users)(u => compute(max(age))) // select max(u.age) from users
//q2-
val avgAgeTimesUserCount: Option[Double] = from(users)(u => compute(avg(age) times aLong))
//q3-
val oldest: Option[Date] = from(users)(u => compute(max(birthDate)))


in q1 for example, compute(max(age)) causes the return type to be Option[], since the table
can potentially be empty.

Here is how the SQL like type arythmetic is implemented :

primitive types in SQL expressions are converted to either NumericExpression or NonNumericExpression :

 trait NumericExpression[Int]
 trait NumericExpression[Option[Float]]
 trait NonNumericExpression[Option[Date]]

SQL binary operations like Multiplication Addtion and Subtraction return an intermeriary type :

  class BinaryAMSOp[Long,Float]

for which conversion back to NumericalExpression exist, and that is where the SQL like type arythmetic is
specified, here is a sample :

  implicit def binaryOpConv23(op: BinaryAMSOp[LongType,LongType]) = new NumericalTypeConversion[LongType](op)
  implicit def binaryOpConv24(op: BinaryAMSOp[LongType,FloatType]) = new NumericalTypeConversion[DoubleType](op)
  implicit def binaryOpConv25(op: BinaryAMSOp[LongType,DoubleType]) = new NumericalTypeConversion[DoubleType](op)
  implicit def binaryOpConv25bd(op: BinaryAMSOp[LongType,BigDecimalType]) = new NumericalTypeConversion[BigDecimalType](op)
  implicit def binaryOpConv26(op: BinaryAMSOp[LongType,Option[ByteType]]) = new NumericalTypeConversion[Option[LongType]](op)
  implicit def binaryOpConv27(op: BinaryAMSOp[LongType,Option[IntType]]) = new NumericalTypeConversion[Option[LongType]](op)
  implicit def binaryOpConv28(op: BinaryAMSOp[LongType,Option[LongType]]) = new NumericalTypeConversion[Option[LongType]](op)
  implicit def binaryOpConv29(op: BinaryAMSOp[LongType,Option[FloatType]]) = new NumericalTypeConversion[Option[DoubleType]](op)

The division operation returns BinaryDivOp[A,B] which goes through implicits like : 
 
  implicit def binaryOpConv92(op: BinaryDivOp[Option[DoubleType],IntType]) = new NumericalTypeConversion[Option[DoubleType]](op)
  implicit def binaryOpConv102(op: BinaryDivOp[BigDecimalType,IntType]) = new NumericalTypeConversion[BigDecimalType](op)
  implicit def binaryOpConv113(op: BinaryDivOp[Option[BigDecimalType],ByteType]) = new NumericalTypeConversion[Option[BigDecimalType]](op)

in order to become a NumericalExpression[A]
 
All Squeryl sql functions expect a TypedExpression[A] an ancester of NumericalExpression[A] and NonNumericalExpression[A],
for example :

   def compute[A](e =>TypedExpression[A])

Now the problem :

Functions like the SQL 'max()' need to work on both NumericalExpression[A] and NonNumericalExpression[A] :

  def max[A](e: NumericalExpression[A])      = new  UnaryAgregateLengthNeutralOp[A](e, "max")
  def max[A](e: NonNumericalExpression[A])      = new  UnaryAgregateLengthNeutralOp[A](e, "max") 

and UnaryAgregateLengthNeutralOp[A] can only become NumericalExpression[A] and NonNumericalExpression[A] via implicits
that will ensure that NumericalExpression[A] become NumericalExpression[Option[A]] (the same for NonNumericalExpression[A])
since that SQL aggregate functions turn everything into a nullable result

  implicit def unaryOpConv1(op: UnaryAgregateLengthNeutralOp[ByteType]) = new NumericalTypeConversion[Option[ByteType]](op)
  implicit def unaryOpConv2(op: UnaryAgregateLengthNeutralOp[IntType]) = new NumericalTypeConversion[Option[IntType]](op)
  implicit def unaryOpConv5bd(op: UnaryAgregateLengthNeutralOp[BigDecimalType]) = new NumericalTypeConversion[Option[BigDecimalType]](op)
  implicit def unaryOpConv7(op: UnaryAgregateLengthNeutralOp[Option[IntType]]) = new NumericalTypeConversion[Option[IntType]](op)

  implicit def unaryOpConv11(op: UnaryAgregateLengthNeutralOp[DateType]) = new DateTypeConversion[Option[DateType]](op)
  implicit def unaryOpConv15(op: UnaryAgregateLengthNeutralOp[BooleanType]) = new BooleanTypeConversion[Option[BooleanType]](op)
  implicit def unaryOpConv16(op: UnaryAgregateLengthNeutralOp[Option[BooleanType]]) = new BooleanTypeConversion[Option[BooleanType]](op)

Since 2.9.0-RC1, these functions :

  def max[A](e: NumericalExpression[A])      = new  UnaryAgregateLengthNeutralOp[A](e, "max")
  def max[A](e: NonNumericalExpression[A])      = new  UnaryAgregateLengthNeutralOp[A](e, "max") 

cannot coexist with the same name, the same happened with min() and nvl().
I haven't found a better workaround than removing the overload, i.e. renaming
max[A](e: NonNumericalExpression[A]) to maxNN[A](e: NonNumericalExpression[A]), one has to do :

  from(users)(u => compute(maxNN(birthDate)))

//instead of :

  from(users)(u => compute(max(birthDate)))

Perhaps the whole approach for implementing SQL's typing rules was wrong in the first place, I haven't found a better one,
but it did work rather well until now, and almost everything else (98%) still works.
Suggestions on a radically different approach are welcome, but I would hope that this other approach would have other
benefit that simply fixing this corner case.

So Informally the enhancement would be :

  bFunc(
    // the 2.9.0-RC1 behavior is to look for conversions for :
    // AA[Z] --> N[Z]
    // BB[Z] --> N[Z] // this conversion could be excluded in the search for implicits since the signature of aFunc(e: NN[Z]): BB[Z] is wrong at this point
    aFunc(4F)
  ) 

I will not try to formally specify the bahavior, I know it's not easy to specify this to have the least impact possible,
but intuitively it seems possible, since sugarring the expression forces the exclusion of aFunc(e: NN[Z]): BB[Z] :

  bFunc({val z = aFunc(4F);z})


object ImplicitConvAmbiguity2 {

  class N[T]
  class NE[T] extends N[T]
  class NN[T] extends N[T]
  class AA[A]
  class BB[A]

  implicit def conv1(i: Float) = new NE[Float]
  implicit def conv3(op: AA[java.util.TooManyListenersException]) = new N[java.util.TooManyListenersException]
  implicit def conv4(op: AA[Float]) = new N[Float]
  implicit def conv5(e: BB[java.util.GregorianCalendar]) = new N[java.util.GregorianCalendar]

  def aFunc[A](a: NE[A]) = new AA[A]

  def aFunc[A](a: NN[A]) = new BB[A]

  def bFunc[T](e1: N[T]) = {}

  bFunc(aFunc(4F))
}

Copyright © 2012 École Polytechnique Fédérale de Lausanne (EPFL), Lausanne, Switzerland