Skip to content

Commit 6182139

Browse files
committed
Add StructuringInformation section
1 parent 8c2e374 commit 6182139

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed
12.2 KB
Loading

src/main/scala/scalatutorial/sections/StructuringInformation.scala

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,224 @@ package scalatutorial.sections
33
/** @param name structuring_information */
44
object StructuringInformation extends ScalaTutorialSection {
55

6+
/**
7+
* = Introduction =
8+
*
9+
* Programs are systems that process information. Therefore, programming
10+
* languages provide ways to model the domain of a program.
11+
*
12+
* This section introduces the ways you can structure information in Scala.
13+
* We will base our examples on the following domain, a ''music sheet'':
14+
*
15+
* <img src="/assets/scala_tutorial/music_sheet.png" />
16+
*
17+
* = Aggregating Information With Case Classes =
18+
*
19+
* First, let’s focus on ''notes''. Suppose that, in our program, we are
20+
* interested in the following properties of notes: their
21+
* [[https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale name]]
22+
* (A, B, C, etc.), their
23+
* [[https://en.wikipedia.org/wiki/Note_value duration]] (whole, half,
24+
* quarter, etc.) and their octave number.
25+
*
26+
* In summary, our note model ''aggregates'' several data (name,
27+
* duration and octave). We express this in Scala by using a ''case class''
28+
* definition:
29+
*
30+
* {{{
31+
* case class Note(
32+
* name: String,
33+
* duration: String,
34+
* octave: Int
35+
* )
36+
* }}}
37+
*
38+
* This definition introduces a new type, `Note`. You can create values
39+
* of this type by calling its ''constructor'':
40+
*
41+
* {{{
42+
* val c3 = Note("C", "Quarter", 3)
43+
* }}}
44+
*
45+
* Then, you can retrieve the information carried by each ''member'' (`name`,
46+
* `duration` and `octave`) by using the dot notation:
47+
*/
48+
def caseClassProjection(res0: String, res1: Int): Unit = {
49+
case class Note(name: String, duration: String, octave: Int)
50+
val c3 = Note("C", "Quarter", 3)
51+
c3.name shouldBe "C"
52+
c3.duration shouldBe res0
53+
c3.octave shouldBe res1
54+
}
55+
56+
/**
57+
* = Defining Alternatives With Sealed Traits =
58+
*
59+
* If we look at the introductory picture, we see that musical symbols
60+
* can be either ''notes'' or ''rests'' (but nothing else).
61+
*
62+
* So, we want to introduce the concept of ''symbol'', as something
63+
* that can be embodied by a fixed set of alternatives: a note or rest.
64+
* We can express this in Scala using a ''sealed trait'' definition:
65+
*
66+
* {{{
67+
* sealed trait Symbol
68+
* case class Note(name: String, duration: String, octave: Int) extends Symbol
69+
* case class Rest(duration: String) extends Symbol
70+
* }}}
71+
*
72+
* A sealed trait definition introduces a new type (here, `Symbol`), but no
73+
* constructor for it. Constructors are defined by alternatives that
74+
* ''extend'' the sealed trait:
75+
*
76+
* {{{
77+
* val symbol1: Symbol = Note("C", "Quarter", 3)
78+
* val symbol2: Symbol = Rest("Whole")
79+
* }}}
80+
*
81+
* = Pattern Matching =
82+
*
83+
* Since the `Symbol` type has no members, we can not do anything
84+
* useful when we manipulate one. We need a way to distinguish between
85+
* the different cases of symbols. ''Pattern matching'' allows us
86+
* to do so:
87+
*
88+
* {{{
89+
* def symbolDuration(symbol: Symbol): String =
90+
* symbol match {
91+
* case Note(name, duration, octave) => duration
92+
* case Rest(duration) => duration
93+
* }
94+
* }}}
95+
*
96+
* The above `match` expression first checks if the given `Symbol` is a
97+
* `Note`, and if it is the case it extracts its fields (`name`, `duration`
98+
* and `octave`) and evaluates the expression at the right of the arrow.
99+
* Otherwise, it checks if the given `Symbol` is a `Rest`, and if it
100+
* is the case it extracts its `duration` field, and evaluates the
101+
* expression at the right of the arrow.
102+
*
103+
* When we write `case Rest(duration) => …`, we say that `Rest(…)` is a
104+
* ''constructor pattern'': it matches all the values of type `Rest`
105+
* that have been constructed with arguments matching the pattern `duration`.
106+
*
107+
* The pattern `duration` is called a ''variable pattern''. It matches
108+
* any value and ''binds'' its name (here, `duration`) to this value.
109+
*
110+
* More generally, an expression of the form
111+
*
112+
* {{{
113+
* e match {
114+
* case p1 => e1
115+
* case p2 => e2
116+
* …
117+
* case pn => pn
118+
* }
119+
* }}}
120+
*
121+
* matches the value of the selector `e` with the patterns
122+
* `p1`, …, `pn` in the order in which they are written.
123+
*
124+
* The whole match expression is rewritten to the right-hand side of the first
125+
* case where the pattern matches the selector `e`.
126+
*
127+
* References to pattern variables are replaced by the corresponding
128+
* parts in the selector.
129+
*
130+
* == Exhaustivity ==
131+
*
132+
* Having defined `Symbol` as a sealed trait gives us the guarantee that
133+
* the possible case of symbols is fixed. The compiler can leverage this
134+
* knowledge to warn us if we write code that does not handle ''all''
135+
* the cases:
136+
*/
137+
def unexhaustive(): Unit = {
138+
sealed trait Symbol
139+
case class Note(name: String, duration: String, octave: Int) extends Symbol
140+
case class Rest(duration: String) extends Symbol
141+
142+
def nonExhaustiveDuration(symbol: Symbol): String =
143+
symbol match {
144+
case Rest(duration) => duration
145+
}
146+
}
147+
148+
/**
149+
* Try to run the above code to see how the compiler informs us that
150+
* we don’t handle all the cases in `nonExhaustiveDuration`.
151+
*
152+
* = Equals =
153+
*
154+
* It is worth noting that, since the purpose of case classes is to
155+
* aggregate values, comparing case class instances compare their values:
156+
*
157+
*/
158+
def caseClassEquals(res0: Boolean, res1: Boolean): Unit = {
159+
case class Note(name: String, duration: String, octave: Int)
160+
val c3 = Note("C", "Quarter", 3)
161+
val otherC3 = Note("C", "Quarter", 3)
162+
val f3 = Note("F", "Quarter", 3)
163+
(c3 == otherC3) shouldBe res0
164+
(c3 == f3) shouldBe res1
165+
}
166+
167+
/**
168+
* = Enumerations =
169+
*
170+
* Our above definition of the `Note` type allows users to create instances
171+
* with invalid names and durations:
172+
*
173+
* {{{
174+
* val invalidNote = Note("not a name", "not a duration", 3)
175+
* }}}
176+
*
177+
* It is generally a bad idea to work with a model that makes it possible
178+
* to reach invalid states. In our case, we want to restrict the space
179+
* of the possible note names and durations to a set of fixed alternatives.
180+
*
181+
* In the case of note names, the alternatives are either `A`, `B`, `C`,
182+
* `D`, `E`, `F` or `G`. We can express the fact that note names are
183+
* a fixed set of alternatives by using a sealed trait, but in contrast to
184+
* the previous example alternatives are not case classes because they
185+
* aggregate no information:
186+
*
187+
* {{{
188+
* sealed trait NoteName
189+
* case object A extends NoteName
190+
* case object B extends NoteName
191+
* case object C extends NoteName
192+
* …
193+
* case object G extends NoteName
194+
* }}}
195+
*
196+
* = Algebraic Data Types =
197+
*
198+
* Data types defined with sealed trait and case classes are called
199+
* ''algebraic data types''. An algebraic data type definition can
200+
* be thought of as a set of possible values.
201+
*
202+
* Algebraic data types are a powerful way to structure information.
203+
*
204+
* If a concept of your program’s domain can be formulated in terms of
205+
* an ''is'' relationship, you will express it with a sealed trait:
206+
*
207+
* “A symbol ''is'' either a note ''or'' a rest.”
208+
*
209+
* {{{
210+
* sealed trait Symbol
211+
* case class Note(…) extends Symbol
212+
* case class Rest(…) extends Symbol
213+
* }}}
214+
*
215+
* On the other hand, if a concept of your program’s domain can be
216+
* formulated in terms of an ''has'' relationship, you will express it
217+
* with a case class:
218+
*
219+
* “A note ''has'' a name, a duration ''and'' an octave number.”
220+
*
221+
* {{{
222+
* case class Note(name: String, duration: String, octave: Int) extends Symbol
223+
* }}}
224+
*/
225+
def nothing(): Unit = ()
6226
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package scalatutorial.sections
2+
3+
import org.scalacheck.Shapeless._
4+
import org.scalaexercises.Test
5+
import org.scalatest.Spec
6+
import org.scalatest.prop.Checkers
7+
import shapeless.HNil
8+
9+
class StructuringInformationSpec extends Spec with Checkers {
10+
11+
def `check case class projection`: Unit = {
12+
check(Test.testSuccess(StructuringInformation.caseClassProjection _, "Quarter" :: 3 :: HNil))
13+
}
14+
15+
def `check case class equals`: Unit = {
16+
check(Test.testSuccess(StructuringInformation.caseClassEquals _, true :: false :: HNil))
17+
}
18+
19+
20+
21+
}

0 commit comments

Comments
 (0)