From F# to Scala — case class/object (ADTs)

Read the whole series:

Part 1 — type infer­ence

Part 2 — traits

Part 3 — case class/object (ADTs) <- you’re here

Part 4 — apply & unap­ply func­tions

Part 5 — implic­its


Con­tin­u­ing on from where we left off with traits last time around, let’s look at Scala’s case class/object which can be used to cre­ate Alge­bra­ic Data Types (ADTs) in Scala.

 

Case Class

You can declare an ADT in F# using Dis­crim­i­nat­ed Unions (DUs). For exam­ple, a bina­ry tree might be rep­re­sent­ed as the fol­low­ing.

In Scala, you can declare this ADT with a pair of case class.

Here is how you con­struct and pat­tern match against F# DUs.

This looks very sim­i­lar in Scala (minus all the com­ments).

Also, one often use sin­gle case DUs in F# to mod­el a domain, for instance…

From what I can see, this is also com­mon prac­tice in Scala (at least in our code­base).

From this snip­pet, you can also see that case class­es do not have to be tied togeth­er to a top lev­el type (via inher­i­tance), but more on this short­ly.


UPDATE 16/01/2017: as @clementd point­ed out, you can turn case class­es with a sin­gle mem­ber into a val­ue class and avoid box­ing by extend­ing from any­Val. For more details, see here.


On the sur­face, case class­es in Scala looks almost iden­ti­cal to DUs in F#. But as we peek under the cov­er, there are some sub­tle dif­fer­ences which you ought to be aware of.

 

Case Object

In F#, if you have a union case that is not para­me­terised then the com­pil­er will opti­mise and com­pile it as a sin­gle­ton. For instance, the Not­Start­ed case is com­piled to a sin­gle­ton as it’s always the same.

You can declare this GameS­ta­tus with case class­es in Scala.

But ref­er­ence equal­i­ty check (which is done with .eq in Scala) reveals that:

  • Not­Start­ed() always cre­ate a new instance
  • but equals is over­rid­den to per­form struc­tur­al equal­i­ty com­par­i­son

If you want Not­Start­ed to be a sin­gle­ton then you need to say so explic­it­ly by using a case object instead.

Cou­ple of things to note here:

  • as men­tioned in my last post, object in Scala declares a sin­gle­ton, so does a case object
  • a case object can­not have con­struc­tor para­me­ters
  • a case object can­not be gener­ic (but a nor­mal object can)

When you pat­tern match against a class object you can also lose the paren­the­ses too (see the ear­li­er exam­ple in print[T]).

 

Cases as Types

For me, the biggest dif­fer­ence between DUs in F# and case class­es in Scala is that you declare an ADT in Scala using inher­i­tance, which has some inter­est­ing impli­ca­tions.

As we saw ear­li­er, each case class in Scala is its own type and you can define a func­tion that takes Node[T] or Empty[T].

This is not pos­si­ble in F#. Instead, you rely on pat­tern match­ing (yup, you can apply pat­tern match­ing in the func­tion params) to achieve a sim­i­lar result.

It’s also worth men­tion­ing that, case objects do not define their own types and would require pat­tern match­ing.

What this also means is that, each case class/object can define their own mem­bers! Oh, and what we have learnt about traits so far also holds true here (mul­ti­ple inher­i­tance, resolv­ing mem­ber clash­es, etc.).

In F#, all mem­bers are defined on the top lev­el DU type. So, a like-for-like imple­men­ta­tion of the above might look like this.

Whilst this is a friv­o­lous exam­ple, I think it is still a good demon­stra­tion of why the abil­i­ty to define mem­bers and inher­i­tance on a per-case basis can be quite pow­er­ful. Because we can’t do that with F#‘s union cas­es, we had to sac­ri­fice some com­pile-time safe­ty and resort to run­time excep­tions instead (and the imple­men­ta­tion became more ver­bose as a result).

The auto­Play func­tion also looks slight­ly more ver­bose than its Scala coun­ter­part, but it’s main­ly down to a F# quirk where you need to explic­it­ly cast sta­tus to the rel­e­vant inter­face type to access its mem­bers.

 

sealed and finally

make ille­gal states unpre­sentable” — Yaron Min­sky

Ever since Yaron Min­sky uttered these fab­u­lous words, it has been repeat­ed in many FP cir­cles and is often achieved in F# through a com­bi­na­tion of DUs and not hav­ing nulls in the lan­guage (apart from when inter-opt­ing with C#, but that’s where your anti-cor­rup­tion lay­er comes in).

This works because DU defines a finite and closed set of pos­si­ble states that can be rep­re­sent­ed, and can­not be extend­ed with­out direct­ly mod­i­fy­ing the DU. The com­pil­er per­forms exhaus­tive checks for pat­tern match­es and will warn you if you do not cov­er all pos­si­ble cas­es. So if a new state is intro­duced into the sys­tem, you will quick­ly find out which parts of your code will need to be updat­ed to han­dle this new state.

For instance, using the GameS­ta­tus type we defined in the pre­vi­ous sec­tion…

the com­pil­er will issue the fol­low­ing warn­ing:

warn­ing FS0025: Incom­plete pat­tern match­es on this expres­sion. For exam­ple, the val­ue ‘GameOver (_)’ may indi­cate a case not cov­ered by the pattern(s).

You can also upgrade this par­tic­u­lar warn­ing — FS0025 — to error to make it much more promi­nent.

 

In Scala, because case classes/objects are loose­ly grouped togeth­er via inher­i­tance, the set of pos­si­ble states rep­re­sent­ed by these case classes/objects is not closed by default. This means new states (poten­tial­ly invalid states intro­duced either inten­tion­al­ly or mali­cious­ly) can be added to the sys­tem and it’s not pos­si­ble for you or the com­pil­er to know all pos­si­ble states when you’re inter­act­ing with the top lev­el trait.

There’s a way you can help the com­pil­er (and your­self!) in this case is to mark the top lev­el trait as sealed.

A sealed trait can only be extend­ed inside the file it’s declared in. It also enables the com­pil­er to per­form exhaus­tive checks against pat­tern match­es to warn you about any missed pos­si­ble input (which you can also upgrade to an error to make them more promi­nent).

Since case objects can­not be extend­ed fur­ther we don’t have to wor­ry about it in this case. But case class­es can be extend­ed by a reg­u­lar class (case class-to-case class inher­i­tance is pro­hib­it­ed), which presents anoth­er angle for poten­tial new states to creep in unde­tect­ed.

So the con­ven­tion in Scala is to mark case class­es as final (which says it can­not be extend­ed any­where) as well as mark­ing the top lev­el trait as sealed.

Voila! And, it works on abstract class­es too.

 

But wait, turns out sealed is not tran­si­tive.

If your func­tion takes a case class then you won’t get com­pil­er warn­ings when you miss a case in your pat­tern match­ing.

You could, make the case class sealed instead, which will allow the com­pil­er to per­form exhaus­tive checks against it, but also opens up the pos­si­bil­i­ty that the case class might be extend­ed in the same file.

Unfor­tu­nate­ly you can’t mark a case class as both final and sealed, so you’d have to choose based on your sit­u­a­tion I sup­pose.

 

Reuse through Inheritance

Because case class­es are their own types and they can inher­it mul­ti­ple traits, it opens up the pos­si­bil­i­ty for you to share case class­es across mul­ti­ple ADTs.

For instance, many col­lec­tion types have the notion of an emp­ty case. It’s pos­si­ble to share the def­i­n­i­tion of the Emp­ty case class.

I think it’s inter­est­ing you can do this in Scala, although I’m not sure that’s such a good thing. It allows for tight cou­pling between unre­lat­ed ADTs, all in the name of code reuse.

Sure, “no one would actu­al­ly do this”, but one thing I learnt in the last decade is that if some­thing can be done then soon­er or lat­er you’ll find it has been done 

 

Summary

To wrap up this fair­ly long post, here are the main points we cov­ered:

  • you can declare ADTs in Scala using case class and/or case object
  • case classes/objects are loose­ly grouped togeth­er through inher­i­tance
  • a case class defines its own type, unlike dis­crim­i­nat­ed unions in F#
  • a case object cre­ates a sin­gle­ton
  • case class­es/objects can define their own mem­bers
  • case class­es/objects sup­port mul­ti­ple inher­i­tance
  • mark­ing the top lev­el trait as sealed allows com­pil­er to per­form exhaus­tive checks when you pat­tern match against it
  • Scala con­ven­tion is to seal the top lev­el trait and mark case class­es as final
  • sealed is not tran­si­tive, you lose the com­pil­er warn­ings when pat­tern match­ing against case class­es direct­ly
  • you can mark case class­es as final or sealed, but not both
  • mul­ti­ple inher­i­tance allows you to share case class­es across dif­fer­ent ADTs, but you prob­a­bly shouldn’t 

 

Links