Could the compiler add .type for case object on its own?

Hello

One frequent issue when teaching Scala is the need for .type to have the type of a case object. It’s not consistent with the rest of the language and often one gets compilation errors due to forgetting this .type.

However, couldn’t the compiler guess on its own when the reference to the case object means a type?

It would make for a more consistent language, always a good thing IMHO.

Cheers!

2 Likes

I can see this could be convenient in a small number of cases, but it makes the language less consistent, rather than more consistent, to give object Foo type Foo only if it’s not a companion.

It’s relatively rare to have to refer to the type of an object at all.

5 Likes

I obsviously don’t know much ^^

Yet in our code base we often have code like Foo[Bar.type], with Bar being a case object, most likely some sealed hierarchy representing instances of some trait.

Actually i don’t remember having ever seen .type for anything else than a case object.

class X defines a type X without an instance. object X defines a value of it’s own type. That’s a crucial difference. object X has no type relation with class X unless object X extends X.

object X extends X means: value named X inherits from type X.

.type can be attached to any value (or more precisely to stable identifier) like val, lazy val or object. Such val, lazy val or object can be nested in another val, lazy val or object.

Usage examples of .type syntax:

object Main {
  trait MyTrait
  
  case object MyCaseObject extends MyTrait
   
  lazy val MyLazyVal = new MyTrait {}
  
  def main(args: Array[String]): Unit = {
    val myTraits: Seq[MyTrait] = List(new MyTrait {}, MyCaseObject, MyLazyVal)
    val myCaseObjects: Seq[MyCaseObject.type] = List(MyCaseObject) // other values won't fit
    val myLazyVals: Seq[MyLazyVal.type] = List(MyLazyVal) // other values won't fit
    
    printType(MyCaseObject)
    printType(MyLazyVal)
    printType(new MyTrait {})
    
    val a = 5
    val b = 5
    val c: a.type = a
    // val d: a.type = b // won't compile
  }
  
  def printType(value: MyTrait): Unit = {
    value match {
      case _: MyCaseObject.type => // equivalent to: case MyCaseObject =>
      	println("Got MyCaseObject")
      case _: MyLazyVal.type => // in this scenario (it's tricky) equivalent to: case MyLazyVal =>
        println("Got MyLazyVal")
      case _ =>
      	println(s"Got other subclass of MyTrait: ${value.getClass}")
    }
  }
}

https://scastie.scala-lang.org/QIBuZsRWSo6RMYJDGtefOA

2 Likes

That’s not the full story. You forgot:

case class X defines a type X AND a value X for convenience.

Now, why not also have:

case object X defines a value X AND a type X for convenience.

It only seems logical. I don’t think there is much code at all, out there, which defines both a case object and a companion class for it. It seems counter to the intuition that case objects are essentially for defining ADT cases with no parameters.

On the other hand, it’s frustrating and looks clunky having to write .type every time you refer to the type of an ADT case that just happens to be an object. To the point where I often find myself writing:

case object Foo extends MyADT
type Foo = Foo.type
4 Likes

I think this is probably somewhat orthogonal to your discussion here, but it’s worth pointing out …

Scala objects, case or otherwise, are the companion objects for their own singleton types. The practical upshot of this is that the implicit/given members of an object are in the implicit scope of the singleton type of that object, ie.,

trait F[T]

object O {
  implicit val fo: F[O] = ...
}

implicitly[F[O.type]] // yields O.fo without an import

shapeless exploits this in it’s encoding of polymorphic functions: in effect it allow us to use the singleton type of an object to represent the the collection of its members.

2 Likes

Haha, good to know! I had no idea.

It does seem orthogonal, though.

case class X(...) expands to class X(...) and object X { ... }, but that object doesn’t automatically extends X, so there’s still no type relationship. Following code doesn’t compile:

object Main {
  case class X()
  
  // type mismatch;
  // found   : Main.X.type
  // required: Main.X
  val x: X = X // this gives compilation error shown above
}

I do not see any type related convenience here, so there’s nothing to port from case classes to case objects.

I wouldn’t say that’s “for convenience”; it’s fundamental to how case classes work (you get the apply and unapply there).

I do think there’s a real chance for confusing beginners here: I can see myself wondering “why can I say X sometimes but when I add a companion class I now need to say X.type?” It’s similar to how, when you add a companion object to your case class, it doesn’t extend the relevant Function trait anymore.

Absolutely yes! Your question is about what the compiler may infer, whereas the answers are about conflating semantics.

It’s a floor wax and a dessert topping!

If I can say

Option.empty[1]

which is patently weird then why can’t I say

object X
Option.empty[X]

with the compiler inferring X.type?

Name binding is already more subtle than is exploited in the field. This would be a natural extension that feels natural to the touch. Precedence rules could prefer the type name X, so that a type name could shadow a term name in a type context, but not the other way around.

Unlike Woomba, which, once activated, can’t be turned off, there would be an -Xlint to let you know that a singleton was inferred, and -Wconf will let you either ignore it or escalate it. Or maybe -Woomba!

I feel that if Miles is free to use it's for the possessive its, then the future is open to a whole panoply of syntactic innovations that only appear to interact with semantics in ways formerly frowned upon, and which in today’s world receive not even a second glance.

1 Like

i may miss something obvious, but to me @LPTK was speaking of reusing the way of being able to write Foo[MyCaseClass] in Foo[MyCaseObject] instead of Foo[MyCaseObject.type], since a case object is also a case class.

overall i was just speaking of not having to add the .type when using the type of case object, just like we don’t use the .type for the type of case class and the like.

I wasn’t asking of removing the notion of .type.

The goal is just to make the use case of parametricity easier/more consistent, use case which is, i think, the most common when using types.

In the case of case object I think there’s something to be said that an explicit companion class would be a anti-pattern. I don’t remember ever seeing a companion case object where it made sense that is was a case object and not a regular object.

2 Likes

I’m failing to understand why you think this is relevant, or what you are trying to say with your example :confused:

@som-snytt letting type-land refer to value names unless they are shadowed by type names is actually an interesting idea. It could remove a lot of clutter in everyday code, and would not actually look outlandish IMHO, especially since variable names are usually not type names, so we’d stop having to write x.type everywhere but could simply write x instead, as in:

def foo(m: Module) = new Derived { val parent: m = m }
// instead of:
def foo(m: Module) = new Derived { val parent: m.type = m }

// and even:
def fluent(x: Int): this = { ... ; this }
// instead of:
def fluent(x: Int): this.type = { ... ; this }
1 Like

This doesn’t compile in Scastie:

trait Bar

case object Foo extends Bar

object Foo

The error being “Foo is already defined as case class Foo”.

So it seems that a case object can’t have a companion object, is this right?

The error message seems to be a bug – Foo is a case object, not a case class.

It’s also not that case object can have companion objects (of course they can’t, they’re not classes), but be companion objects:

I meant you can have a case object with a companion class, i.e. a class with companion object that’s a case object.

class Foo {
  // some class members
}
case object Foo {
  // some static methods
}

But I don’t see why someone would ever do this other than copy pasting some keywords together without really knowing what they’re doing.

So desugaring case object Foo to

sealed abstract class Foo
case object Foo extends Foo

seems relatively safe to me. And then you could even warn or error when somebody wants to define their own class Foo.

1 Like

Agreed; though I’d prefer using a type synonym here, to avoid creating two classes instead of one. This will be generally possible with top-level definitions.

case object Foo extends Foo
type Foo = Foo.type
1 Like
  1. Option.empty[1] looks weird, indeed. I would choose Option.empty[1.type] syntax, but it’s too late to argue.
  2. If I have object X then X is a name of value, not a literal value itself.

I don’t see what case has to do here. class X and object X compile to different classes under the hood. object X is of type X$ in fact. To a great extent object X is similar to lazy val X = new {}. I can change:

object Main {
  object Object1

  class Class1
  object Object2 extends Class1

  trait Trait1
  object Object3 extends Class1 with Trait1
}

to:

object Main {
  lazy val Object1 = new {}

  class Class1
  lazy val Object2 = new Class1 {}

  trait Trait1
  lazy val Object3 = new Class1 with Trait1 {}
}

If I have code like:

class X
object X

Then classOf[X] returns class X, while classOf[X.type] returns class X$, a different and unrelated class, so conflating them would be confusing and error prone.

Additionally I fail to see how implicit .type would increase consistency. It is a outright inconsistency. What would be the extra rule for implying .type?

something[X] expands to something[X.type] if both conditions apply:

  • X is a name of case object (but not ordinary object)
  • X is not a name of a class or trait

?

It doesn’t look like simplification of syntax rules. Bear in mind that class X and object X can be defined in different e.g. packages so you can’t avoid the problem by disallowing a situation where case object X is next to class X.

Companion objects for case classes have a convenient property that if you can provide an explicit companion object and Scala compiler will merge the autogenerated one with your explicit one, giving your definitions a higher priority. So:

case class X(a: Int)

object X { // and explicit companion object, but without apply, unapply, etc
  def makeSpecial(b: Int): X = X(b * 3)
}

is compiled into:

class X(val a: Int) {
  // implemented hashCode, equals, toString, product methods, etc
}
object X {
  def makeSpecial(b: Int): X = X(b * 3) // it's here because it was in the original code

  // implemented apply, unapply, etc because we haven't provided one
}

Such merging couldn’t work in reverse. Consider following code:

class X(a: Listener) {
  a.listen()
}

case object X

What would it be translated to? This?

class X(a: Listener) {
  a.listen()
}

object X extends X(what?)

In the end, what I’m trying to say, is that implying .type based on some extra rule is definitely not adding any consistency, but rather reducing it. OTOH implying .type can be convenient to some, but convenience != consistency.

The automatic type Foo = Foo.type would be least problematic IMO as it doesn’t bend the syntax. But currently there is a problem with it: you can’t have top level type aliases (at least in Scala 2.12, I’m not sure if it changed in later versions).

case object X creates two classes anyway: X.class and X$.class. X is an instance of X$.class, whereas X.class contains static forwarders for Java interoperability. It works that way with any kind of objects, case or not.

3 Likes

hello @tarsa and thanks for the long answer

Obviously i’m not an expert regarding scala’s syntax rules and the reasons behind .type.

I’m just a medium level scala dev often tumbling on this .type for case object and only them: i never use it anywhere else. And quite often i forget the .type for my only use case & i often see coworkers stumbling upon it as well. With my limited knowledge it feels a pretty irregular syntax for a corner case usage. So i was hoping it could be more in line with the syntax i use for all the other types’ parametricity, making the whole thing uniform and thus easier.

If not possible, so be it, but this apparent/perceived irregularity is a pain for me and the coders i work with.

cheers