Skip to content

Rewrote case classes tour #715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 12, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 29 additions & 115 deletions tutorials/tour/_posts/2017-02-13-case-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,135 +9,49 @@ categories: tour
num: 11
next-page: pattern-matching
previous-page: currying
prerequisite-knowledge: classes, basics, mutability
---

Scala supports the notion of _case classes_. Case classes are just regular classes that are:

* Immutable by default
* Decomposable through [pattern matching](pattern-matching.html)
* Compared by structural equality instead of by reference
* Succinct to instantiate and operate on

Here is an example for a Notification type hierarchy which consists of an abstract super class `Notification` and three concrete Notification types implemented with case classes `Email`, `SMS`, and `VoiceRecording`.
Case classes are like regular classes with a few key differences which we will go over. Case classes are good for modeling immutable data. In the next step of the tour, we'll see how they are useful in [pattern matching](pattern-matching.html).

## Defining a case class
A minimal case class requires the keywords `case class`, an identifier, and a parameter list (which may be empty):
```tut
abstract class Notification
case class Email(sourceEmail: String, title: String, body: String) extends Notification
case class SMS(sourceNumber: String, message: String) extends Notification
case class VoiceRecording(contactName: String, link: String) extends Notification
```
case class Book(isbn: String)

Instantiating a case class is easy: (Note that we don't need to use the `new` keyword)

```tut
val emailFromJohn = Email("john.doe@mail.com", "Greetings From John!", "Hello World!")
val frankenstein = Book("978-0486282114")
```
Notice how the keyword `new` was not used to instantiate the `Message` case class. This is because case classes have an `apply` method by default which takes care of object construction.

The constructor parameters of case classes are treated as public values and can be accessed directly.

```tut
val title = emailFromJohn.title
println(title) // prints "Greetings From John!"
When you create a case class with parameters, the parameters are public `val`s.
```
case class Message(sender: String, recipient: String, body: String)
val message1 = Message("guillaume@quebec.ca", "jorge@catalonia.es", "Ça va ?")

With case classes, you cannot mutate their fields directly. (unless you insert `var` before a field, but doing so is generally discouraged).

```tut:fail
emailFromJohn.title = "Goodbye From John!" // This is a compilation error. We cannot assign another value to val fields, which all case classes fields are by default.
println(message1.sender) // prints guillaume@quebec.ca
message1.sender = "travis@washington.us" // this line does not compile
```
You can't reassign `message1.sender` because it is a `val` (i.e. immutable). It is possible to use `var`s in case classes but this is discouraged.

Instead, you make a copy using the `copy` method. As seen below, you can replace just some of the fields:

```tut
val editedEmail = emailFromJohn.copy(title = "I am learning Scala!", body = "It's so cool!")

println(emailFromJohn) // prints "Email(john.doe@mail.com,Greetings From John!,Hello World!)"
println(editedEmail) // prints "Email(john.doe@mail.com,I am learning Scala,It's so cool!)"
## Comparison
Case classes are compared by structure and not by reference:
```
case class Message(sender: String, recipient: String, body: String)

For every case class the Scala compiler generates an `equals` method which implements structural equality and a `toString` method. For instance:

```tut
val firstSms = SMS("12345", "Hello!")
val secondSms = SMS("12345", "Hello!")

if (firstSms == secondSms) {
println("They are equal!")
}

println("SMS is: " + firstSms)
val message2 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
val message3 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These examples are excellent!

val messagesAreTheSame = message2 == message3 // true
```
Even though `message2` and `message3` refer to different objects, the value of each object is equal.

will print

## Copying
You can create a deep copy of an instance of a case class simply by using the `copy` method. You can optionally change the constructor arguments.
```
They are equal!
SMS is: SMS(12345, Hello!)
case class Message(sender: String, recipient: String, body: String)
val message4 = Message("julien@bretagne.fr", "travis@washington.us", "Me zo o komz gant ma amezeg")
val message5 = message4.copy(sender = message4.recipient, recipient = "claire@bourgogne.fr")
message5.sender // travis@washington.us
message5.recipient // claire@bourgogne.fr
message5.body // "Me zo o komz gant ma amezeg"
```

With case classes, you can utilize **pattern matching** to work with your data. Here's a function that prints out different messages depending on what type of Notification is received:

```tut
def showNotification(notification: Notification): String = {
notification match {
case Email(email, title, _) =>
"You got an email from " + email + " with title: " + title
case SMS(number, message) =>
"You got an SMS from " + number + "! Message: " + message
case VoiceRecording(name, link) =>
"you received a Voice Recording from " + name + "! Click the link to hear it: " + link
}
}

val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

println(showNotification(someSms))
println(showNotification(someVoiceRecording))

// prints:
// You got an SMS from 12345! Message: Are you there?
// you received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
```

Here's a more involved example using `if` guards. With the `if` guard, the pattern match branch will fail if the condition in the guard returns false.

```tut
def showNotificationSpecial(notification: Notification, specialEmail: String, specialNumber: String): String = {
notification match {
case Email(email, _, _) if email == specialEmail =>
"You got an email from special someone!"
case SMS(number, _) if number == specialNumber =>
"You got an SMS from special someone!"
case other =>
showNotification(other) // nothing special, delegate to our original showNotification function
}
}

val SPECIAL_NUMBER = "55555"
val SPECIAL_EMAIL = "jane@mail.com"
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val specialEmail = Email("jane@mail.com", "Drinks tonight?", "I'm free after 5!")
val specialSms = SMS("55555", "I'm here! Where are you?")

println(showNotificationSpecial(someSms, SPECIAL_EMAIL, SPECIAL_NUMBER))
println(showNotificationSpecial(someVoiceRecording, SPECIAL_EMAIL, SPECIAL_NUMBER))
println(showNotificationSpecial(specialEmail, SPECIAL_EMAIL, SPECIAL_NUMBER))
println(showNotificationSpecial(specialSms, SPECIAL_EMAIL, SPECIAL_NUMBER))

// prints:
// You got an SMS from 12345! Message: Are you there?
// you received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
// You got an email from special someone!
// You got an SMS from special someone!

```

When programming in Scala, it is recommended that you use case classes pervasively to model/group data as they help you to write more expressive and maintainable code:

* Immutability frees you from needing to keep track of where and when things are mutated
* Comparison-by-value allows you compare instances as if they are primitive values - no more uncertainty regarding whether instances of a class is compared by value or reference
* Pattern matching simplifies branching logic, which leads to less bugs and more readable code.


The recipient of `message4` is used as the sender of `message5` but the `body` of `message4` was copied directly.