Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange type mismatch when using member access instead of extractor

Given a tuple with elements of type A and another type parametrised in A:

trait Writer[-A] { def write(a: A): Unit }
case class Write[A](value: A, writer: Writer[A])

And a use site:

trait Cache { def store[A](value: A, writer: Writer[A]): Unit }

Why does the following work as expected, using the tuple's extractor:

def test1(set: Set[Write[_]], cache: Cache): Unit =
  set.foreach {
    case Write(value, writer) => cache.store(value, writer)
  }

But the following fails:

def test2(set: Set[Write[_]], cache: Cache ): Unit =
  set.foreach { write =>
    cache.store(write.value, write.writer)
  }

with error message

 found   : Writer[_$1] where type _$1
 required: Writer[Any]
             cache.store(write.value, write.writer)
                                        ^

Can I fix the second form (test2) to compile properly?

EDIT

Departing from the ideas by Owen I tried out if I can make it work without pattern matching at all (which is what I wanted in the first place). Here are two more strange cases, one working, the other not:

// does not work
def test3(set: Set[Write[_]], cache: Cache): Unit = {
  def process[A](write: Write[A]): Unit =
    cache.store(write.value, write.writer)

  set.foreach(process)
}

// _does work_
def test4(set: Set[Write[_]], cache: Cache): Unit = {
  def process[A](write: Write[A]): Unit =
    cache.store(write.value, write.writer)

  set.foreach(w => process(w))
}

Still pretty obscure to me...

like image 571
0__ Avatar asked Mar 07 '12 23:03

0__


1 Answers

Running with -Xprint:typer is illuminating here. The problem with test2 is that there is an existential type, which appears in two separate places: both write.value and write.writer have an existential type, but, crucially, the compiler has no way of knowing that they have the same existentially quantified type variable. Even though you access them from the same object, the compiler forgets they came from the same place.

When test1 is fully typed, you see:

def test1(set: Set[Write[_]], cache: Cache) =
    set.foreach(((x0$1: Write[_]) => x0$1 match {
      case (value: _$1, writer: Writer[_$1])Write[_$1]((value @ _), (writer @ _)) =>
          cache.store[_$1](value, writer)
    }));

The type variable _$1 is matched along with the values. Matching the type variable _$1 binds it in the scope of the case, so it's not existential anymore, and Scala can tell that value and writer have the same type parameter.

The solution for test2 is to not use existentials:

def test2[A]( set: Set[ Write[ A ]], cache: Cache ) {
   set.foreach { write =>
      cache.store( write.value, write.writer )
   }
}

or to bind the type variable with a match:

def test2( set: Set[ Write[ _ ]], cache: Cache ) {
   set.foreach { case write: Write[a] =>
      cache.store( write.value, write.writer )
   }
}

edit

Let me endeavor to answer the new questions you brought up.

The reason test3 does not work, is that when you write:

set.foreach( process )

process, which is polymorphic, has to be made monomorphic, for two reasons (that I know of):

  1. Functions in Scala cannot be polymorphic; only methods can be. process as defined as a method; when used as a first-class function, it is a function.

  2. The way the compiler does type inference is mostly by taking polymorphic values and unifying them together to make them less polymorphic. Passing an actual polymorphic value as a method argument would require higher-rank types.

The reason that test4 does work is that the function literal:

set.foreach( w => process( w ))

is actually not a polymorphic function! It takes as its argument an exestentially qualified type; but not a polymorphic type. It then calls the method (not the function) process, and matches the existential type variable to process's type parameter. Pretty wild, eh?

You could also write:

set.foreach( process(_) )

which, creating an anonymous function, means the same thing.

Another route you may or may not find appropriate would be to discard existential types and use type members:

trait Writable {
    type A
    val value: A
    val writer: Writer[A]
}

case class Write[T]( value: T, writer: Writer[ T ]) extends Writable {
    type A = T
}

def test2( set: Set[Writable], cache: Cache ) {
    set.foreach { write =>
        cache.store( write.value, write.writer )
    }
}

Here Scala is able to see that write.value and write.writer have the same type parameter because they have the same path dependent type.

like image 80
Owen Avatar answered Sep 28 '22 11:09

Owen