-
Notifications
You must be signed in to change notification settings - Fork 1k
Reworked case class tutorial #547 #599
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
Changes from 1 commit
94fad22
2dfa9c4
6230bc9
87582d4
4c647e5
bc8cf17
aee907b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,80 +10,125 @@ tutorial-next: pattern-matching | |
tutorial-previous: currying | ||
--- | ||
|
||
Scala supports the notion of _case classes_. Case classes are regular classes which export their constructor parameters and which provide a recursive decomposition mechanism via [pattern matching](pattern-matching.html). | ||
Scala supports the notion of _case classes_. Case classes are just regular classes that are: | ||
|
||
Here is an example for a class hierarchy which consists of an abstract super class `Term` and three concrete case classes `Var`, `Fun`, and `App`. | ||
* Immutable by default | ||
* Decomposable through [Pattern Matching](pattern-matching.html) | ||
* Compared by structural equality instead of by reference | ||
* Succinct to instantiate and operate on | ||
|
||
This is achieved through auto-generated code by the Scala compiler. | ||
|
||
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`. | ||
|
||
```tut | ||
abstract class Term | ||
case class Var(name: String) extends Term | ||
case class Fun(arg: String, body: Term) extends Term | ||
case class App(f: Term, v: Term) extends Term | ||
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 | ||
``` | ||
|
||
This class hierarchy can be used to represent terms of the [untyped lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus). To facilitate the construction of case class instances, Scala does not require that the `new` primitive is used. One can simply use the class name as a function. | ||
Instantiating a Case class is easy: (Note that we don't need to use the `new` keyword) | ||
|
||
```tut | ||
val emailFromJohn = Email("[email protected]", "Greetings From John!", "Hello World!") | ||
``` | ||
|
||
Here is an example: | ||
The constructor parameters of case classes are treated as public values and can be accessed directly. | ||
|
||
```tut | ||
Fun("x", Fun("y", App(Var("x"), Var("y")))) | ||
val title = emailFromJohn.title | ||
println(title) // prints "Greetings From John!" | ||
``` | ||
|
||
The constructor parameters of case classes are treated as public values and can be accessed directly. | ||
With case classes, you cannot mutate their fields directly. Instead, you make a copy using the `copy` method. | ||
As seen below, you can replace just some of the fields: | ||
|
||
```tut | ||
val x = Var("x") | ||
println(x.name) | ||
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. | ||
|
||
val editedEmail = emailFromJohn.copy(title = "I am learning Scala!", body = "It's so cool!") | ||
|
||
println(emailFromJohn) // prints "Email([email protected],Greetings From John!,Hello World!)" | ||
println(editedEmail) // prints "Email([email protected],I am learning Scala,It's so cool!)" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this code block is designed to fail then https://github.com/tpolecat/tut Also check the Travis logs if they fail. They will show you where to look to fix the problem. :-) https://travis-ci.org/scala/scala.github.com/builds/166207587 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. I'll fix this up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jarrodwb I have fixed this a TravisCI run is passing. |
||
``` | ||
|
||
For every case class the Scala compiler generates an `equals` method which implements structural equality and a `toString` method. For instance: | ||
|
||
```tut | ||
val x1 = Var("x") | ||
val x2 = Var("x") | ||
val y1 = Var("y") | ||
println("" + x1 + " == " + x2 + " => " + (x1 == x2)) | ||
println("" + x1 + " == " + y1 + " => " + (x1 == y1)) | ||
val firstSms = SMS("12345", "Hello!") | ||
val secondSms = SMS("12345", "Hello!") | ||
|
||
if (firstSms == secondSms) { | ||
println("They are equal!") | ||
} | ||
|
||
println("SMS is: " + firstSms) | ||
``` | ||
|
||
will print | ||
|
||
``` | ||
Var(x) == Var(x) => true | ||
Var(x) == Var(y) => false | ||
They are equal! | ||
SMS is: SMS(12345, Hello!) | ||
``` | ||
|
||
It only makes sense to define case classes if pattern matching is used to decompose data structures. The following [object](singleton-objects.html) defines a pretty printer function for our lambda calculus representation: | ||
With Case Classes, all the necessary machinery required for using them in **Pattern Matching** is automatically generated by the compiler. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wondering; is it really necessary to point out that there's stuff generated by the compiler? Just thinking that mentions of this could be removed here, as this sort of information is generally not required to use/understand case classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think I put that in because it was really useful to know that pattern matching isn't some magical thing Scala did which only applies to case classes (coming from a Java-ish background). However you are correct - if they read this tutorial on case classes before pattern matching (which is the right order, imo), then the implementation detail of pattern matching doesn't need to be mentioned. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has been removed |
||
Here's a function that prints out different messages depending on what type of Notification is received: | ||
|
||
```tut | ||
object TermTest extends scala.App { | ||
def printTerm(term: Term) { | ||
term match { | ||
case Var(n) => | ||
print(n) | ||
case Fun(x, b) => | ||
print("^" + x + ".") | ||
printTerm(b) | ||
case App(f, v) => | ||
print("(") | ||
printTerm(f) | ||
print(" ") | ||
printTerm(v) | ||
print(")") | ||
} | ||
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 | ||
} | ||
def isIdentityFun(term: Term): Boolean = term match { | ||
case Fun(x, Var(y)) if x == y => true | ||
case _ => false | ||
} | ||
|
||
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 id = Fun("x", Var("x")) | ||
val t = Fun("x", Fun("y", App(Var("x"), Var("y")))) | ||
printTerm(t) | ||
println | ||
println(isIdentityFun(id)) | ||
println(isIdentityFun(t)) | ||
} | ||
|
||
val SPECIAL_NUMBER = "55555" | ||
val SPECIAL_EMAIL = "[email protected]" | ||
val someSms = SMS("12345", "Are you there?") | ||
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123") | ||
val specialEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!") | ||
val specialSms = SMS("55555", "I'm here! Where are you?") | ||
|
||
// 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! | ||
|
||
``` | ||
|
||
In our example, the function `printTerm` is expressed as a pattern matching statement starting with the `match` keyword and consisting of sequences of `case Pattern => Body` clauses. | ||
The program above also defines a function `isIdentityFun` which checks if a given term corresponds to a simple identity function. This example uses deep patterns and guards. After matching a pattern with a given value, the guard (defined after the keyword `if`) is evaluated. If it returns `true`, the match succeeds; otherwise, it fails and the next pattern will be tried. | ||
_Case Classes_ is one of the biggest reasons why Scala code can be both concise and readable. With immutability and structural equality, working with | ||
case classes are just like working with primative values (Int, Double, etc), which in turn makes writing programs in a functional style much simpler | ||
and intuitive. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about these last two sentences; not sure what they add, seems a bit fluffy. Maybe remove these? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that they are a little bit fluffy. They were put in because I wanted to stress that
If you think by this point a new reader will have these point driven home then I can remove them. Or perhaps reword them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Said this way, a much stronger point emerges. Care to try rewording? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Let me know what you think! |
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment below