Overriding methods in case class

It is amazing to me how simply discussing a problem with peers can help to solve the problem. During this discussion I think I came up with a way to solve my problem in a much less invasive way. The motivation of my original change is that I have expression trees which at the leaves sometimes have atomic elements such as 0, 0.0, or 0L. I need all functions dealing with these to treat these leaves as different, including Set, Map, and other containers, and also functions such as contains, union, diff, sort, dedupe, etc.

What I’m going to try, as a result of this conversation, is to change my data structure so that the leaf is not 0.0, but rather is a tuple generated by something like (x:Any) =>(x.getClass(),x). I think all I need to do is massage my accessors, and constructors for the class representing those leaf nodes, and I won’t have to change the equal and hashCode methods.

Fingers crossed!

1 Like

OK, this is just my non-expert impression, take it for what it is worth.

The danger is that the language allows me to do lots of things with case classes which conventional wisdom, but not the compiler, discourage. This gives me the impression that case classes form a brittle abstraction. Some things are fine to do with case classes, and some things are dangerous, and nothing inherent in the system indicates which are which. For example, I’ve been overriding equal and hashCode in case classes for a while now, without ever encountering a problem. And lo and behold, it turns out to be a dubious behavior which is predicted to bite me at some unexpected point in the future.

Another danger with case classes is programmers can use them blindly without understanding how they work. For example, I have never written an unapply method and in particular never have I written a var-args unapply method and I don’t even know how to do it. The case class just magically does it for me. This is fine except that once the programmer accidentally or intentionally ventures beyond the standard/supported use case, things apparently break with no warning. The only solution seems to be that in order to use case class you must intimately understand how they work.

1 Like

The way case classes differ from conventional classes in this regard is that the case class writes the useful methods for me, and I, the programmer, need not understand how they work in order to use them.

1 Like

I think you’re probably right except that the lines (we are presumably supposed to stay inside of) do not seem to be well marked nor enforced, and as @chaotic3quilibrium pointed out, the jump from using a case class to writing your own conventional class is complicated by the lack of specification.

1 Like

And that’s fine. A language that would entirely shield you from doing stupid or risky things couldn’t possibly be turing complete. :slight_smile:

As the post @chaotic3quilibrium linked states, the main problem is that you’re likely breaking expectations about case classes, thus getting in the way of others (or your future self) reasoning about the code. If you don’t have these expectations to start with because you’re just (ab)using case classes as an unapply generator and override everything else, anyway, this is not a big issue - unless you want others to read your code. :slight_smile: (At some point you may still stumble over these inconsistencies, though.)

If you use them as intended (i.e. for plain “records”/DTOs and as ADT constructors, and without overriding stuff), a superficial understanding will get you a long way. But yes, it helps to understand the machinery underlying any convenience syntax you’re using.

The lang spec lists all the items that will be generated for a case class and there’s specification for all of these. What do you think is missing? There probably is no guide for reverting non-standard “contorted” case classes to their vanilla equivalents, because that’s a rather rare problem, but all the pieces should be there.

I’d think that part of the issue is that you have chosen not to “grow” into idiomatic Scala usage (of which there certainly is no universally accepted definition) but rather go with experience you have acquired with other languages and stacks. That’s fine, but it probably makes things harder as they’d need to be. My impression still is that a slim ADT based on a sealed trait and vanilla case classes with some core functionality built in via methods and additional functionality provided via external matching functions and type classes would be a perfect match for your domain - and circa 80% of the puzzlers you have come up with may never have surfaced. (There would be other questions, of course, but rather different ones.)

2 Likes

I admit I don’t understand the idea, but relying on #getClass is usually yet another telltale sign of non-idiomatic Scala code. (But if you want to discuss this, a dedicated thread with a self-contained, minimal code example probably would help keeping focus.)

2 Likes

Somewhere in my immense pile of buttons, I have one that reads, “There is not now, and never shall be, a programming language in which it is the least bit difficult to write bad code”. It’s not that case classes are brittle – it’s that they are sometimes not taught properly, and there is only so much the compiler can do to save you from mistakes.

Really, the only problem with case classes is that they never should have been called “class” in the first place. That made sense at the time (and it is long since water under the bridge), but in retrospect I think it would have been better if they had been called data, reflecting the fact that they work best when you think of them as data structures with a lot of built-in goodies, rather than a completely general-purpose tool like classes. I pretty much always teach them as such.

Granted, unapply is a bit weird, and it is nice that case classes do it for you. (I think it’s why they were created in the first place.) That said, it’s not rocket science: I’ve done it several times, and it’s generally a fairly modest amount of code. The varargs version is simply unapplySeq. (And apply is trivial.)

3 Likes

I disagree with this, you can easily find the documentation of what a case class do here, also when you need to do the jump you probably don’t need all the things the case class used to give you; especially in your case that you already override hashcode & equals, so you probably only need the pattern matching part.

This seems to work as you would expect:

final class Foo private (val data: Seq[Int])
object Foo {
  def apply(nums: Int*): Foo =
    new Foo(data = nums)
  
  def unapplySeq(foo: Foo): Some[Seq[Int]] =
    Some(foo.data)
}

Foo(1, 2, 3) match {
  case Foo(a, b, c) =>
    println(s"$a | $b | $c ")
}

(code running here)

I hope that helps you in your migration, you may read more about custom unapply methods, here.

1 Like

@BalmungSan the link you give is great information. You’re right in that it doesn’t look that difficult. But it doesn’t mention the unapplySeq method. Why’s that?

It does, at the very end (it is easy to miss actually, I searched for unapplySeq to notice it).

Sometimes, the number of values to extract isn’t fixed and we would like to return an arbitrary number of values, depending on the input. For this use case, you can define extractors with an unapplySeq method which returns an Option[Seq[T]] . Common examples of these patterns include deconstructing a List using case List(x, y, z) => and decomposing a String using a regular expression Regex , such as case r(name, remainingFields @ _*) => .

1 Like

The link about case classes doesn’t mention it because case classes don’t automatically have an unapplySeq method.