7

Given:

case object A

What's the difference, if any, between the @ and : in:

def f(a: A.type): Int = a match {
   case aaa @ A => 42
}

and

def f(a: A.type): Int = a match {
   case aaa : A.type => 42
}
Kevin Meredith
  • 41,036
  • 63
  • 209
  • 384

4 Answers4

6

The first one @ uses an extractor to do the pattern matching while the second one : requires the type - that's why you need to pass in A.type there.

There's actually no difference between them in terms of matching. To better illustrate the difference between @ and : we can look at a simple class, which doesn't provide an extractor out of the box.

class A

def f(a: A) = a match {
  case _ : A => // works fine
  case _ @ A => // doesn't compile because no extractor is found
}
Andrei T.
  • 2,455
  • 1
  • 13
  • 28
4

In this very specific case, almost nothing is different. They will both achieve the same results.

Semantically, case aaa @ A => 42 is usage of pattern binding where we're matching on the exact object A, and case aaa : A.type => 42 is a type pattern where we want a to have the type A.type. In short, type versus equality, which doesn't make a difference for a singleton.

The generated code is actually slightly different. Consider this code compiled with -Xprint:patmat:

def f(a: A.type): Int = a match {
  case aaa @ A => 42
  case aaa : A.type => 42
}

The relevant code for f shows that the two cases are slightly different, but will not produce different results:

def f(a: A.type): Int = {
  case <synthetic> val x1: A.type = a;
  case6(){
    if (A.==(x1))  // case aaa @ A
      matchEnd5(42)
    else
      case7()
  };
  case7(){
    if (x1.ne(null)) // case aaa: A.type
      matchEnd5(42)
    else
      case8()
  };
  case8(){
    matchEnd5(throw new MatchError(x1))
  };
  matchEnd5(x: Int){
    x
  }
}

The first case checks equality, where the second case only checks that the reference is not null (we already know the type matches since the method parameter is the singleton type).

Michael Zajac
  • 55,144
  • 7
  • 113
  • 138
2

Semantically, there is no difference in this case. We can have a look at the bytecode to see if there is a runtime difference:

> object A
defined object A
> object X { def f(a: A.type) = a match { case a @ A => 42 } }
defined object X
> :javap X
  ...
  public int f($line4.$read$$iw$$iw$$iw$$iw$$iw$$iw$A$);
    descriptor: (L$line4/$read$$iw$$iw$$iw$$iw$$iw$$iw$A$;)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: aload_1
         1: astore_3
         2: getstatic     #51                 // Field $line4/$read$$iw$$iw$$iw$$iw$$iw$$iw$A$.MODULE$:L$line4/$read$$iw$$iw$$iw$$iw$$iw$$iw$A$;
         5: aload_3
         6: invokevirtual #55                 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
         9: ifeq          18
        12: bipush        42
        14: istore_2
        15: goto          30
        18: goto          21
        21: new           #57                 // class scala/MatchError
        24: dup
        25: aload_3
        26: invokespecial #60                 // Method scala/MatchError."<init>":(Ljava/lang/Object;)V
        29: athrow
        30: iload_2
        31: ireturn

And the other case:

> object Y { def f(a: A.type) = a match { case a: A.type => 42 } }
defined object Y
> :javap Y
  ...
  public int f($line4.$read$$iw$$iw$$iw$$iw$$iw$$iw$A$);
    descriptor: (L$line4/$read$$iw$$iw$$iw$$iw$$iw$$iw$A$;)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: aload_1
         1: astore_3
         2: aload_3
         3: ifnull        12
         6: bipush        42
         8: istore_2
         9: goto          24
        12: goto          15
        15: new           #50                 // class scala/MatchError
        18: dup
        19: aload_3
        20: invokespecial #53                 // Method scala/MatchError."<init>":(Ljava/lang/Object;)V
        23: athrow
        24: iload_2
        25: ireturn

Indeed, there is a small difference. In the second case the compiler can see that a parameter of type A.type has only two values: A.type and null. Therefore at runtime there is only a check whether it is null because the other case is checked at compile time. In the first version of the code, the compiler doesn't do this optimization. Instead it is calling the equals method.

If we change the type of the parameter slightly, we get a different result:

> object Z { def f(a: AnyRef) = a match { case a: A.type => 42 } }
defined object Z
> :javap Z
  ...
  public int f(java.lang.Object);
    descriptor: (Ljava/lang/Object;)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: aload_1
         1: astore_3
         2: aload_3
         3: getstatic     #51                 // Field $line4/$read$$iw$$iw$$iw$$iw$$iw$$iw$A$.MODULE$:L$line4/$read$$iw$$iw$$iw$$iw$$iw$$iw$A$;
         6: if_acmpne     15
         9: bipush        42
        11: istore_2
        12: goto          27
        15: goto          18
        18: new           #53                 // class scala/MatchError
        21: dup
        22: aload_3
        23: invokespecial #56                 // Method scala/MatchError."<init>":(Ljava/lang/Object;)V
        26: athrow
        27: iload_2
        28: ireturn

In this version the compiler no longer knows what the parameter is, therefore it is doing a comparison of the types at runtime. We could now discuss whether the call of equals in the first version or the type comparison in the third call is more efficient but I guess the JIT of the JVM is optimizing away any overhead in both cases anyway, therefore we first would have to look at the machine code to tell which code is more efficient, if there is a difference at all.

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
1

Semantically there is no different in this particular example but in general we include keyword @ if we want to do something with the object itself. This thread explains the use of these extractors with a simple example.

Community
  • 1
  • 1
user4212639
  • 489
  • 6
  • 6