Skończyłam pierwszy z pięciu kursów programowania w języku Scala w pięciokursowej specjalizacji na Courserze, prowadzonej przez twórcę tego języka.
Kolejna ciekawa cecha języka, jaką poznałam, to klasy przypadków (ang. case classes) i dopasowywanie wzorców (ang. pattern matching).
Spis treści
Instrukcje wielokrotnego wyboru i wzorce w innych językach
W wielu językach programowania istnieje jakaś forma instrukcji switch
. W Javie pozwala ona dokonywać wyboru dla zmiennej typu prostego oraz, nie od początku, zmiennej typu String
. Na przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int month = 2; String monthString; switch (month) { case 1: monthString = "Styczeń"; break; case 2: monthString = "Luty"; break; ... case 12: monthString = "Grudzień"; break; default: monthString = "Nie ma takiego miesiąca"; break; } |
Dopasowywanie wzorców i instrukcja match
w Scali
Niektóre języki, w tym Scala, idą o krok dalej. Instrukcji wielokrotnego wyboru można w nich używać nie tylko na danych typów prostych. Co więcej, dopasoowywanie wzorców pozwala nam opisać przypadki w sposób niepełny – np. z zastosowaniem symboli wieloznacznych (ang. wildcards).
Poniżej przykład ze Scali, w którym przetwarzane są wierzchołki drzewa binarnego – osobno wierzchołki wewnętrzne (Fork
), osobno liście (Leaf
) drzewa.
1 2 3 4 5 |
def weight(tree: CodeTree): Int = tree match { case Fork(_, _, _, weight) => weight case Leaf('ą', _) => -100 case Leaf(_, weight) => weight } |
W zależności od typu wierzchołka (Fork
lub Leaf
) oraz od wartości jednego z pól w przypadku klasy Leaf
, w odmienny sposób zwrócona zostanie waga wierzchołka.
Nieistotne parametry zostały zastąpione symbolem _
, który dopasuje się do wszystkiego. Istotnym parametrom przypisujemy nazwę, dzięki czemu będzie można się do nich odwołać. Można też użyć ukonkretnionych wartości, żeby zdefiniować konkretne przypadki.
Po prawej stronie case
nie ma żadnego przypisania ani instrukcji, ponieważ match
jest wyrażeniem – zwróci jako wartość prawą stronę operatora =>
.
Test powyższej funkcji (przechodzący, daję słowo):
1 2 3 4 5 |
test("weights") { assert(weight(Leaf('ą',1)) === -100) assert(weight(Leaf('a',1)) === 1) assert(weight(Fork(Leaf('a',2), Leaf('b',3), List('a','b'), 5)) === 5) } |
Klasy przypadków i ich cechy
Brakuje jeszcze definicji klas Leaf
oraz Fork
. Jak łatwo się domyślić, żeby używać ich w ten sposób, klasy te muszą zostać zdefiniowane jako klasy przypadków.
Ich definicja wygląda tak:
1 2 3 |
abstract class CodeTree case class Fork(left: CodeTree, right: CodeTree, chars: List[Char], weight: Int) extends CodeTree case class Leaf(char: Char, weight: Int) extends CodeTree |
Klasy przypadków:
- definiuje się z użyciem słowa kluczowego case,
- powinny być niemodyfikowalne,
- są porównywane przez podobieństwo strukturalne i wartości, a nie przez referencję,
- wartości przekazane w konstruktorze są publicznie dostępne.
FAQ
Poniżej kilka szybkich pytań i odpowiedzi:
Q1: Co się stanie, jeśli nie przewidzisz wszystkich wzorców i na etapie wywołania okaże się, że zmienna nie pasuje do żadnego wzorca?
A1: Otrzymasz scala.MatchError.
Q2: Jak utworzyć odpowiednik javowego default
?
A2: Tak:
1 |
case _ => ... |
Q3: Czy to działa także dla typów prostych.
A3: Tak. Co więcej, możliwa jest nawet taka operacja:
1 2 3 4 5 |
def weight(tree: Any): Int = tree match { case Fork(_, _, _, weight) => weight case Leaf(_, weight) => weight case 1 => 1 } |
PS.
Bardzo to wszystko wygodne i czytelne.
O, podstawowa dokumentacja Scali została przetłumaczona na język polski!
Jedna myśl nt. „Fajne rzeczy w Scali: klasy przypadków”