Fajne rzeczy w Scali: klasy przypadków

namiekko.pl 6 lat temu

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 interesująca cecha języka, jaką poznałam, to klasy przypadków (ang. case classes) i dopasowywanie wzorców (ang. pattern matching).

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:

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.

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):

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:

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żeli nie przewidzisz wszystkich wzorców i na etapie wywołania okaże się, iż zmienna nie pasuje do żadnego wzorca?
A1: Otrzymasz scala.MatchError.

Q2: Jak utworzyć odpowiednik javowego default?
A2: Tak:

case _ => ...

Q3: Czy to działa także dla typów prostych.
A3: Tak. Co więcej, możliwa jest choćby taka operacja:

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!

Idź do oryginalnego materiału