How to use a case classes when hierarchy is needed? - scala

I know that you're not allowed to inherit from case classes but how would you do when you really need to? We have two classes in a hierarchy, both contain many fields, and we need to be able to create instances of both. Here's my options:
If I'd make the super class a usual class instead of a case class - I'd lose all the case class goodness such as toString, equals, hashCode methods etc.
If I keep it as a case class, I'd break the rule of not inheriting from case classes.
If I use composition in the child class - I'd have to write lots of methods and redirect them to the other class - which would mean lots of work and would feel non-Scalaish.
What should I do? Isn't it quite a common problem?

Yes this is quite a recurrent problem, what I would suggest is to create a trait with all parent properties, create a case class which just implements it and then another one which inherits of it with more properties.
sealed trait Parent {
/* implement all common properties */
}
case class A extends Parent
case class B extends Parent {
/*add all stuff you want*/
}
A good way of seeing it is a tree, traits are nodes and case classes are leaves.
You can use a trait or an abstract class depending on your needs for the parent. However, avoid using a class because you would be able to create instances of it, which would not be elegant.
EDIT: As suggested in comments, you can seal the trait in order to have exceptions at compilation if not all case classes are covered in a pattern matching. It is for example explained in chapter 15.5 of "Programming in Scala"

What about replacing inheritance with delegation?
If your two classes in the hierarchy have many shared fields, then delegation could reduce the amount of boilerplate code? Like so:
case class Something(aa: A, bb: B, cc: C, payload: Payload)
sealed abstract class Payload
case class PayloadX(xx: X) extends Payload
case class PayloadY(yy: Y) extends Payload
And then you would create Something instances like so:
val sth1 = Something('aa', 'bb', 'cc', PayloadX('xx'))
val sth2 = Something('aa', 'bb', 'cc', PayloadY('yy'))
And you could do pattern matching:
sth1 match {
case Something(_, _, _, PayloadX(_)) => ...
case Something(_, _, _, PayloadY(_)) => ...
}
Benefits: (?)
When you declare PayloadX and PayloadY, you don't have to repeat all fields in Something.
When you create instances of Something(... Payload(..)), you can reuse code that creates Something, both when you create a Something(... PayloadX(..)) and ...PayloadY.
Drawbacks: (?)
Perhaps PayloadX and Y in your case are actually true subclasses of Something, I mean, in your case, perhaps delegation is semantically wrong?
You would have to write something.payload.whatever instead of simply something.whatever (I suppose this might be good or bad depending on your particular case?)

I explored this issue as well, AFAIK, the best you're going to get is:
Have each case class extend from a common trait that defines abstract
properties each case class must implement
It doesn't remove the boilerplate (at all), but defines a contract your case classes must adhere to, while not losing case class feature set...

Related

Using Enumerations in Scala Best Practices

I have been using sealed traits and case objects to define enumerated types in Scala and I recently came across another approach to extend the Enumeration class in Scala like this below:
object CertificateStatusEnum extends Enumeration {
val Accepted, SignatureError, CertificateExpired, CertificateRevoked, NoCertificateAvailable, CertChainError, ContractCancelled = Value
}
against doing something like this:
sealed trait CertificateStatus
object CertificateStatus extends {
case object Accepted extends CertificateStatus
case object SignatureError extends CertificateStatus
case object CertificateExpired extends CertificateStatus
case object CertificateRevoked extends CertificateStatus
case object NoCertificateAvailable extends CertificateStatus
case object CertChainError extends CertificateStatus
case object ContractCancelled extends CertificateStatus
}
What is considered a good approach?
They both get the job done for simple purposes, but in terms of best practice, the use of sealed traits + case objects is more flexible.
The story behind is that since Scala came with everything Java had, so Java had enumerations and Scala had to put them there for interoperability reasons. But Scala does not need them, because it supports ADTs (algebraic data types) so it can generate enumeration in a functional way like the one you just saw.
You'll encounter certain limitations with the normal Enumeration class:
the inability of the compiler to detect pattern matches exhaustively
it's actually harder to extend the elements to hold more data besides the String name and the Int id, because Value is final.
at runtime, all enums have the same type because of type erasure, so limited type level programming - for example, you can't have overloaded methods.
when you did object CertificateStatusEnum extends Enumeration your enumerations will not be defined as CertificateStatusEnum type, but as CertificateStatusEnum.Value - so you have to use some type aliases to fix that. The problem with this is the type of your companion will still be CertificateStatusEnum.Value.type so you'll end up doing multiple aliases to fix that, and have a rather confusing enumeration.
On the other hand, the algebraic data type comes as a type-safe alternative where you specify the shape of each element and to encode the enumeration you just need sum types which are expressed exactly using sealed traits (or abstract classes) and case objects.
These solve the limitations of the Enumeration class, but you'll encounter some other (minor) drawbacks, though these are not that limiting:
case objects won't have a default order - so if you need one, you'll have to add your id as an attribute in the sealed trait and provide an ordering method.
a somewhat problematic issue is that even though case objects are serializable, if you need to deserialize your enumeration, there is no easy way to deserialize a case object from its enumeration name. You will most probably need to write a custom deserializer.
you can't iterate over them by default as you could using Enumeration. But it's not a very common use case. Nevertheless, it can be easily achieved, e.g. :
object CertificateStatus extends {
val values: Seq[CertificateStatus] = Seq(
Accepted,
SignatureError,
CertificateExpired,
CertificateRevoked,
NoCertificateAvailable,
CertChainError,
ContractCancelled
)
// rest of the code
}
In practice, there's nothing that you can do with Enumeration that you can't do with sealed trait + case objects. So the former went out of people's preferences, in favor of the latter.
This comparison only concerns Scala 2.
In Scala 3, they unified ADTs and their generalized versions (GADTs) with enums under a new powerful syntax, effectively giving you everything you need. So you'll have every reason to use them. As Gael mentioned, they became first-class entities.
It depends on what you want from enum.
In the first case, you implicitly have an order on items (accessed by id property). Reordering has consequences.
I'd prefer 'case object', in some cases enum item could have extra info in the constructor (like, Color with RGB, not just name).
Also, I'd recommend https://index.scala-lang.org/mrvisser/sealerate or similar libraries. That allows iterating over all elements.

Difference between case class and case object?

I am learning Scala and Akka and in my recent lookup for a solution, I found something like
case class TotalTaxResult(taxAmount:Double)
case object TaxCalculationTimeout
What is the difference between the two?
When should I use one over the other?
A case class can take arguments, so each instance of that case class can be different based on the values of it's arguments. A case object on the other hand does not take args in the constructor, so there can only be one instance of it (a singleton, like a regular scala object is).
If your message to your actor does not need any value differentiation, use a case object. For instance, if you had an actor that did some work, and you, from the outside, wanted to tell it to do work, then maybe you'd do something like this:
case object DoWork
...
def receive = {
case DoWork =>
//do some work here
}
But if you wanted some variation in how the work is done, you might need to redefine your message like so:
case class DoWorkAfter(waitTime:Long)
...
def receive = {
case class DoWorkAfter(time) =>
context.system.scheduler.scheduleOnce(time.milliseconds, self, DoWork)
case DoWork =>
//do some work here
}
A case object is a singleton case class. They are used kind of like enumeration values. It can be used in pattern matching just like any other value:
TaxCalculationTimeout match {
case TaxCalculationTimeout => println("hello")
}
When you define a case class, you are creating a template for instances of that class. TotalTaxResult(1.0) and TotalTaxResult(2.0) are two different values of the same type. Whereas there is exactly one TaxCalculationTimeout value.
In simple words, Scala is a Object Oriented and Functional programming language. It have features of functional programming like pattern matching with pure object oriented methodology.
Some times, we need to create singleton object without any value like passing some signal for pattern matching. If scala have not concept of case object we just to need to use enum or equals some string value in matching. But this is not a readability in pure Object Oriented language.. In that scenario we are using Case Object
Case classes are used when we need to create multiple objects with different values.
You can think of these two to be just like a class and an object in general.
When you do a case class ClassName(params list) it creates a blueprint for making objects and case object defines a singleton object in the scope in which it is declared.
Starting with Scala 2.10, you should always use case objects instead of case classes with no arguments.
So when you want to do a pattern match on some values which need arguments you should go for a case class, but if your values don't take arguments then you should use a case object.

What is a sealed trait?

Sealed classes are described in 'Programming in Scala', but sealed traits are not.
Where can I find more information about a sealed trait?
I would like to know, if a sealed trait is the same as a sealed class?
Or, if not, what are the differences?
When is it a good idea to use a sealed trait (and when not)?
A sealed trait can be extended only in the same file as its declaration.
They are often used to provide an alternative to enums. Since they can be only extended in a single file, the compiler knows every possible subtypes and can reason about it.
For instance with the declaration:
sealed trait Answer
case object Yes extends Answer
case object No extends Answer
The compiler will emit a warning if a match is not exhaustive:
scala> val x: Answer = Yes
x: Answer = Yes
scala> x match {
| case No => println("No")
| }
<console>:12: warning: match is not exhaustive!
missing combination Yes
So you should use sealed traits (or sealed abstract class) if the number of possible subtypes is finite and known in advance. For more examples you can have a look at list and option implementations.
a sealed trait is the same as a sealed class ?
As far as sealed goes, yes. They share the normal differences between trait and class, of course.
Or, if not, what are the differences ?
Moot.
When is it a good idea to use a sealed trait (and when not) ?
If you have a sealed class X, then you have to check for X as well as any subclasses. The same is not true of sealed abstract class X or sealed trait X. So you could do sealed abstract class X, but that's way more verbose than just trait and for little advantage.
The main advantage of using an abstract class over a trait is that it can receive parameters. That advantage is particularly relevant when using type classes. Let's say you want to build a sorted tree, for instance. You can write this:
sealed abstract class Tree[T : Ordering]
but you cannot do this:
sealed trait Tree[T : Ordering]
since context bounds (and view bounds) are implemented with implicit parameters. Given that traits can't receive parameters, you can't do that.
Personally, I prefer sealed trait and use it unless some particular reason makes me use a sealed abstract class. And I'm not talking about subtle reasons, but in-your-face reasons you cannot ignore, such as using type classes.
From the daily-scala blog:
When a trait is "sealed" all of its subclasses are declared within the
same file and that makes the set of subclasses finite which allows
certain compiler checks.
Also I feel the need to point you to the specifications:
The sealed modifier applies to class definitions. A sealed class may not be directly inherited, except if the inheriting template is defined in the same source
file as the inherited class. However, subclasses of a sealed class can be inherited anywhere.
— M. Odersky. The Scala language specification, version 2.8. online, Sept., 2013.
‌‌Briefly:
Sealed traits can only be extended in the same file
List this lets the compiler easily know all possible subtypes
Use sealed traits when the number of possibly subtypes is finite and known in advance
A way of creating something like enum in Java
Help to define algebraic data types (ADTs)
and for more details
Everything about sealed traits in Scala
Traits can also be defined sealed, and only extended by a fixed set of case classes.
The core difference between normal traits and sealed traits can be summarized as follows:
Normal traits are open, so any number of classes can inherit from the trait as long as they provide
all the required methods, and instances of those classes can be used interchangeably via the trait's
required methods.
A normal trait hierarchy makes it easy to add additional sub-classes: just define your class and
implement the necessary methods. However, it makes it difficult to add new methods: a new
method needs to be added to all existing subclasses, of which there may be many.
Sealed traits are closed: they only allow a fixed set of classes to inherit from them, and all
inheriting classes must be defined together with the trait itself in the same file or REPL command.
A sealed trait hierarchy is the opposite: it is easy to add new methods, since a new method can
simply pattern match on each sub-class and decide what it wants to do for each. However, adding
new sub-classes is difficult, as you need to go to all existing pattern matches and add the case to
handle your new sub-class.
As an example
object SealedTraits extends App{
sealed trait Point
case class Point2D(x: Double, y: Double) extends Point
case class Point3D(x: Double, y: Double, z: Double) extends Point
def hypotenuse(p: Point) = p match {
case Point2D(x, y) => math.sqrt(x x + y y)
case Point3D(x, y, z) => math.sqrt(x x + y y + z z)
}
val points: Array[Point] = Array(Point2D(1, 2), Point3D(4, 5, 6))
for (p <- points) println(hypotenuse(p))
// 2.23606797749979
// 8.774964387392123
In general, sealed traits are good for modelling hierarchies where you expect the number of sub-classes
to change very little or not-at-all. A good example of something that can be modeled using sealed trait is
JSON.
A JSON value can only be JSON null, boolean, number, string, array, or dictionary.
JSON has not changed in 20 years, so it is unlikely that anyone will need to extend our JSON
with additional subclasses.
While the set of sub-classes is fixed, the range of operations we may want to do on a JSON blob is
unbounded: parse it, serialize it, pretty-print it, minify it, sanitize it, etc.
Thus it makes sense to model a JSON data structure as a closed sealed trait
hierarchy rather than a normal open trait hierarchy.
sealed trait Json
case class Null() extends Json
case class Bool(value: Boolean) extends Json
case class Str(value: String) extends Json
case class Num(value: Double) extends Json
case class Arr(value: Seq[Json]) extends Json
case class Dict(value: Map[String, Json]) extends Json

Recommended code style: case object Foo or object Foo extends Serializable?

If I want a serialization-safe singleton, should I prefer
case object Foo
or
object Foo extends Serializable
?
I think this depends on how you plan to use this object. Case objects are generally used with case classes to represent some kind of initial or terminal object in an algebraic data type, eg Nil or None. Regular objects usually are companions for classes to hold static methods like singleton and factory methods.
If you're planning using this object with other classes, serializing it, and maybe using it in pattern matching, defining it as a case object seems more natural to me.

Is it appropriate to define a non-trivial Scala case class?

I'm defining a Scala class today, and I think "I need an equals method and a hashCode method; and a copy method would be handy too. I'll turn this into a case class." My class already has a bunch of other code, and is in no way trivial.
So fine, it all works and everything, but when the text books deal with case classes, all of the examples define them for use as value classes or 'data transfer objects'. Is it appropriate to define a non-trivial case class? Is the thought process described above OK, or do I need to think of case classes differently?
A case class provides, equals, hashCode and toString methods based on the main constructor parameters, all of which are turned into val too. In addition, the object companion gets an apply and an unapply methods, again based on the main constructor parameters.
Also, a case class inherits from Serializable and from Product, and should not be extended by other classes.
If all of these things are appropriate for your class, then feel free to declare it as a `case class'.
Feel free, provided it doesn't have descendants. Extending case classes is a bad idea.