4

I am working on a piece of code that looks kind of like the following (Scastie for your convenience):

import scala.language.higherKinds

sealed trait Wrapping
sealed trait PinkWrap extends Wrapping
sealed trait GreenWrap extends Wrapping

sealed trait Foo[M[_], A] {}
case class MintFoo[M[_], A](a : A) extends Foo[M,A]
case class LiquoriceFoo[M[_], A](a : A) extends Foo[M,A]
sealed trait WrappedFoo[M[_], _, A]
case class FooInPinkWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, PinkWrap, A]
case class FooInGreenWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, GreenWrap, A]

object Utils {
   def analyzeFoo[M[_], S <: Wrapping, A](w: WrappedFoo[M, S, A]): String = {
    w match {
      case FooInPinkWrap(f: Foo[M,A]) => tasteFoo[M,A](f)+" in Pink wrapping"      
      case FooInGreenWrap(f: Foo[M,A]) => tasteFoo[M,A](f)+" in Green wrapping"
    }
  }

  def tasteFoo[M[_], A](f: Foo[M,A]) : String = 
   f match {
     case MintFoo (a) => "Mint"
     case LiquoriceFoo (a) => "Liquorice"
   }
}   

It used to compile perfectly fine with Scala 2.11.7.

Ever since the project has been bumped to Scala 2.12.4 (but in fact the issue is reproducible already with Scala 2.11.11) it fails to compile with the following message from scalac for the line case FooInPinkWrap(f: Foo[M,A]) => tasteFoo[M,A]:

M takes no type parameters, expected: one

This is reproducible right within Scastie by choosing Scala Version 2.12.4 under "Build Settings".

This is puzzling to me, since M is indeed 1-ary.

In fact, I run into the very same issue with the following, simpler MWE (Scastie):

import scala.language.higherKinds

sealed trait GiftWrap
sealed trait PinkWrap extends GiftWrap
sealed trait GreenWrap extends GiftWrap

sealed trait Foo[M[_], A] {}
sealed trait WrappedFoo[M[_], S, A]
case class FooInPinkWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, PinkWrap, A]
case class FooInGreenWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, GreenWrap, A]

object Utils {
   def tellColor[M[_], S <: GiftWrap, A](w: WrappedFoo[M, S, A]): String = {
    w match {
      case FooInPinkWrap(f: Foo[M,A]) => "Pink"
      case FooInGreenWrap(f: Foo[M,A]) => "Green"
    }
  }
}

Replacing M with ({type T[X] = M[X]})#T in the latter piece of code results in

pattern type is incompatible with expected type;
 found   : Playground.this.Foo[[X],A]
 required: Playground.this.Foo[Any,Any]
Note: [X] <: Any, but trait Foo is invariant in type M.
You may wish to define M as +M instead. (SLS 4.5)
Note: A <: Any, but trait Foo is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)

M does not take type parameters

Notice found : Playground.this.Foo[[X],A].

Why does this happen?

I don't find the changelog for 2.11.11 (a minor release) particularly enlighting.

More importantly, how do I make the old code compile

  1. The right way
  2. The minimum-amount-of-modifications way?

Thanks.

Tobia Tesan
  • 1,938
  • 17
  • 29
  • Any reason for using `List` and `Int` as separate types instead of `List[Int]`? – Yuval Itzchakov Mar 20 '18 at 10:12
  • @YuvalItzchakov not really, I just put the first types of arity 1 and 0 that came to my mind in order to make the final line compile in Scastie. I'm not particularly suggesting that I'm looking at a `M` of `A` in the "real" code, nor that `Int` or `List` are involved in any capacity. Tell you what, I removed that line from here for clarity (it's still in the Scastie paste). Thank you. – Tobia Tesan Mar 20 '18 at 10:17
  • I'm just having difficulty understanding something. I see you define `M` as a higher kinded type, but never use it as one. i.e. you never use `M[A]` anywhere, only `M` and `A` as separate type parameters? – Yuval Itzchakov Mar 20 '18 at 10:18
  • @YuvalItzchakov: yes, please assume `M` and `A` are orthogonal for the sake of this example. This piece of code is completely arbitrary and silly, of course (the "real" code - kind of long and not my design - has probably reasons for being what it is) - but still, *should* it typecheck or not? – Tobia Tesan Mar 20 '18 at 10:26

1 Answers1

3

Edit-notice

This posting now contains two parts:

  1. Several workarounds with a drastically reduced example
  2. Application of the workarounds to you code

Shorter example which produces same error

Those aren't exactly "minimal" examples, so I've attempted to boil it down a little further. The entire hierarchy of Foo and the entire hierarchy of the colored wrappings are not really relevant. This small example fails in essentially the same way:

sealed trait Foo[M[_], P]
case class Bar[M[_]]() extends Foo[M, Int]

def f[M[_], S](x: Foo[M, S]): Int = x match {
  case Bar() => g[M]
}

def g[M[_]] = 0

When compiled with 2.12.4 it gives:

error: M takes no type parameters, expected: one
  case Bar() => g[M]
                  ^
one error found

The only interesting thing here is the type parameter P of Foo that is replaced by a concrete Int in Bar but then again made into a generic S in the signature of f.

After reading the relevant parts of documentation and taking a look at similar, possibly related issues I decided that I probably shouldn't speculate too much about why this worked in 2.11, then stopped working, and now doesn't work in 2.12.

Instead, I will simply list several workarounds, hoping that you may find one of them useful.

1. You can convert the constructor into a type pattern:

sealed trait Foo[M[_], P]
case class Bar[M[_]]() extends Foo[M, Int]

def f[M[_], S](x: Foo[M, S]): Int = x match {
  case b: Bar[M] => g[M]
}

def g[M[_]] = 0

2. You can convert constructor-value match into a type pattern with variables (yes, Scala indeed supports type variables in match-case!), here is a quote that I haven't noticed for a long time [Odersky et al. "Programming in Scala", first edition, page 275]:

You could have also used (lowercase) type variables instead.

So, you can do this, and the compiler will actually try to infer quite a few things about m:

sealed trait Foo[M[_], P]
case class Bar[M[_]]() extends Foo[M, Int]

def f[M[_], S](x: Foo[M, S]): Int = x match {
  case b: Bar[m] => g[m]
}

def g[M[_]] = 0

}

3. You can move the RHS of the case into a method of Bar:

sealed trait Foo[M[_], P]
case class Bar[M[_]]() extends Foo[M, Int] {
  def g: Int = 0
}

def f[M[_], S](x: Foo[M, S]): Int = x match {
  case b @ Bar() => b.g
}

4. (Not really workaround, more an observation)

It works if you fix the type parameter P to Int instead of leaving it polymorphic:

sealed trait Foo[M[_], P]
case class Bar[M[_]]() extends Foo[M, Int]

def f[M[_]](x: Foo[M, Int]): Int = x match {
  case Bar() => g[M]
}

def g[M[_]] = 0

Back to your code

Here are the same work-arounds applied to your code:

1. Constructor pattern to type pattern:

import scala.language.higherKinds

sealed trait Wrapping
sealed trait PinkWrap extends Wrapping
sealed trait GreenWrap extends Wrapping

sealed trait Foo[M[_], A] {}
case class MintFoo[M[_], A](a : A) extends Foo[M, A]
case class LiquoriceFoo[M[_], A](a : A) extends Foo[M, A]
sealed trait WrappedFoo[M[_], _, A]
case class FooInPinkWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, PinkWrap, A]
case class FooInGreenWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, GreenWrap, A]

object Utils {
  def analyzeFoo[M[_], S <: Wrapping, A](w: WrappedFoo[M, S, A]): String = {
    w match {
      case f: FooInPinkWrap[M, A] => tasteFoo[M, A](f.m) + " in Pink wrapping"      
      case f: FooInGreenWrap[M, A] => tasteFoo[M, A](f.m) + " in Green wrapping"
    }
  }

  def tasteFoo[M[_], A](f: Foo[M,A]) : String = {
    f match {
      case MintFoo(a) => "Mint"
      case LiquoriceFoo(a) => "Liquorice"
    }
  }
}

2. Type pattern with variables:

sealed trait Wrapping
sealed trait PinkWrap extends Wrapping
sealed trait GreenWrap extends Wrapping

sealed trait Foo[M[_], A] {}
case class MintFoo[M[_], A](a : A) extends Foo[M, A]
case class LiquoriceFoo[M[_], A](a : A) extends Foo[M, A]
sealed trait WrappedFoo[M[_], _, A]
case class FooInPinkWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, PinkWrap, A]
case class FooInGreenWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, GreenWrap, A]

object Utils {
  def analyzeFoo[M[_], S <: Wrapping, A](w: WrappedFoo[M, S, A]): String = {
    w match {
      case f: FooInPinkWrap[m, a] => tasteFoo[m, a](f.m) + " in Pink wrapping"      
      case f: FooInGreenWrap[m, a] => tasteFoo[m, a](f.m) + " in Green wrapping"
    }
  }

  def tasteFoo[M[_], A](f: Foo[M,A]) : String = {
    f match {
      case MintFoo(a) => "Mint"
      case LiquoriceFoo(a) => "Liquorice"
    }
  }
}

3: Move tasteBlah into WrappedFoo:

sealed trait Wrapping
sealed trait PinkWrap extends Wrapping
sealed trait GreenWrap extends Wrapping

sealed trait Foo[M[_], A] {}
case class MintFoo[M[_], A](a : A) extends Foo[M, A]
case class LiquoriceFoo[M[_], A](a : A) extends Foo[M, A]
sealed trait WrappedFoo[M[_], _, A] {
  def m: Foo[M, A]
  def tasteFoo: String = {
    m match {
      case MintFoo(a) => "Mint"
      case LiquoriceFoo(a) => "Liquorice"
    }
  }
}
case class FooInPinkWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, PinkWrap, A]
case class FooInGreenWrap[M[_], A](m: Foo[M, A]) extends WrappedFoo[M, GreenWrap, A]

object Utils {
  def analyzeFoo[M[_], S <: Wrapping, A](w: WrappedFoo[M, S, A]): String = {
    w match {
      case f: FooInPinkWrap[M, A] => f.tasteFoo + " in Pink wrapping"      
      case f: FooInGreenWrap[M, A] => f.tasteFoo + " in Green wrapping"
    }
  }
}

4 (Only for minimized example, not applicable).


(Purely my personal opinion): I don't find this behavior very intuitive, maybe you could try to submit it as an issue. Even if someone can find a very good reason why this shouldn't compile after all, the error message is still very confusing.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • `The problem is that unless P is set to the concrete type Int, it doesn't make any sense at all to pattern match Baz[M[_]] against a Bar[M[_], P] for a general parameter P.` which is to say, this is either incorrect per the spec or undefined behaviour that worked in 2.11 by sheer luck? – Tobia Tesan Mar 20 '18 at 18:45
  • @TobiaTesan I've updated my answer. I've skimmed over the relevant parts of the spec, I couldn't come up with some precise quote that would definitely settle the question whether this should or shouldn't compile. Seems rather like a somewhat underspecified corner case to me. I suspect that it turned out that the behavior in 2.11 (when it worked) led to some hard compiler crashes, so they've built in too conservative defence mechanisms to fail faster, and thereby inadvertently also moved your (seemingly valid) code into the set of incompilable programs. I don't know. I would submit a bug. – Andrey Tyukin Mar 20 '18 at 22:26