Alias rejected in return type


#1

Not sure if this is a shapeless question or a general Scala question. The code below uses shapeless.tag and works fine (imports omitted):

object Test {
  trait MoneyTag
  type Money = Int @@ MoneyTag
}

class Test {
  def deposit(account: Long, cents: Money) = ()
  def cash(cents: Int): Int @@ MoneyTag = tag[MoneyTag](cents)
  def add100(): Unit = deposit(1L, cash(100))
}

However, this method is rejected by the compiler with a type mismatch:

  def cash(cents: Int): Money = tag[MoneyTag](cents)

Since Money is an alias for Int @@ MoneyTag, what’s the difference with the previous one?

MC


#2

Can you paste the error here?


#3
Test.scala:18:48: type mismatch;
[error]  found   : cents.type (with underlying type Int)
[error]  required: shapeless.tag.Tagged[Test.MoneyTag] with Int
[error]   def cash2(cents: Int): Money = tag[MoneyTag](cents)
[error]                                                ^
[error] one error found

#4

The weird thing about this error message is the ‘required’ type. From the docs ( https://oss.sonatype.org/service/local/repositories/releases/archive/com/github/japgolly/fork/shapeless/shapeless_sjs0.6_2.11/2.1.0-2/shapeless_sjs0.6_2.11-2.1.0-2-javadoc.jar/!/index.html#shapeless.tag$ ), this expression:

tag[MoneyTag](cents)

Is equivalent to:

new Tagger[MoneyTag].apply[Int](cents)

Which has a return type:

Int @@ MoneyTag

Which is equivalent to:

Int with Tagged[MoneyTag]

This type is different from Tagged[MoneyTag] with Int, which the compiler is asking for. Not sure what’s going on, I would probably ask on Shapeless’ Gitter to see if anyone can explain.

In the meanwhile, if you’re open to an alternative, my lightweight newtype code ( https://gist.github.com/yawaramin/ae4fbaa67ac50963c8743abea8709857 ) seems to work:

object Test {
  val Money = Newtype[Int]()
  type Money = Money.T
}

class Test {
  import Test.Money

  def deposit(account: Long, cents: Money) = ()
  def cash(cents: Int): Money = Money(cents)
  def add100(): Unit = deposit(1L, cash(100))
}

#5

Thanks for the link to an alternative. Looks neat.

What I don’t understand is that the call to add100 is accepted, so cash has to produce a subtype of Money, whatever that type is. If so, why can’t it have return type Money? Whatever it’s doing, shapeless.tag doesn’t seem quite ready for prime time. I was just looking for an alternative to value classes. I’ll have a look at your newtype setup.

MC


#6

I thought A with B is always the same type as B with A, even though the implementation can be different?


#7

Agreed, the implementation in Shapeless seems to have been thrown in there after a successful Stack Overflow answer showed off the neat trick. Anyway, I learned today about https://github.com/estatico/scala-newtype , which might also work for you (I haven’t tried it).


#8

I remember Martin Odersky saying he wanted Dotty to have intersection types (A & B) because one of the problems with A with B is that it’s not commutative. We can demonstrate this:

scala> trait A { def x = 1 }
defined trait A

scala> trait B { def x = 2 }
defined trait B

scala> object AwithB extends A with B { override def x = super.x }
defined object AwithB

scala> AwithB.x
res4: Int = 2

scala> object BwithA extends B with A { override def x = super.x }
defined object BwithA

scala> BwithA.x
res5: Int = 1

#9

**As I said, the implementation may change, but the type is the same:

Welcome to Scala 2.12.4 (OpenJDK 64-Bit Server VM, Java 1.8.0_171).
Type in expressions for evaluation. Or try :help.

trait A; trait B
defined trait A
defined trait B

def aWithB: A with B = ???
aWithB: A with B

def bWithA: B with A = aWithB
bWithA: B with A**

Best, Oliver


#10

If the implementation is different, the type is ipso facto different:

trait A { def x = 1 }
trait B { def x = 2 }
class AwithB extends A with B { override def x = super.x }
class BwithA extends B with A { override def x = super.x }

val aWithB: AwithB = new BwithA // error: type mismatch
val bWithA: BwithA = new AwithB // error: type mismatch

#11

You are merely demonstrating that a class that extends A with B is not the same type as a class that extends B with A. Two classes are always different types, even if they extend the exact same type. Therefore, this does not demonstrate that A with B is distinct from B with A.

What I meant to say is that in terms of type inference and type checking, there is no difference between A with B and B with A. Each is assignable to the other. You cannot write a method that accepts one but not the other as an argument. A type error cannot be caused by the difference between the two, unless the compiler has a bug.

But you are also right by pointing out that at the same time, instances of A with B are distinct from instances of B with A.

I guess the correct way of talking about this is that A with B and B with A are the same reference type, but distinct object types.

Best, Oliver


#12

aWithB and bWithA are distinct (and incompatible) types, but I still think A with B and B with A are the same type. I don’t think you can find a value that would have type A with B but not B with A (and vice-versa).

trait A
trait B

type AB = A with B
type BA = B with A

def ab: AB = null
def ba: BA = null

def f(x: AB) = ()
def g(x: BA) = ()

f(ba)
g(ab)

val x = new A with B
val y = new B with A

f(x)
f(y)
g(x)
g(y)

No type error anywhere here.


#13

I’m not an expert in this subject, but don’t they linearize differently? Yes, A with B and B with A are very, very similar, but wouldn’t they behave differently in terms of diamond inheritance? I’m actually a bit surprised there aren’t type errors here…


#14
scala> trait A ; trait B
defined trait A
defined trait B

scala> typeOf[A with B]
res1: $r.intp.global.Type = A with B

scala> typeOf[B with A]
res2: $r.intp.global.Type = B with A

scala> res1 <:< res2
res3: Boolean = true

scala> res2 <:< res1
res4: Boolean = true

scala> res2 =:= res1
res5: Boolean = false

scala> res1 =:= res2      // fool me twice, shame on me
res6: Boolean = false