Pattern matching - 1. část

Scala podporuje obecný způsob, kterým lze identifikovat jednotlivé typy objektů z hierarchie/kompozice tříd a dostat z nich potřebná data: Pattern matching, tzv. "switch na steroidech".

Pattern matching představuje vlastně reverzní postup ke konstrukci objektů. Typicky potřebujeme z objektů extrahovat stejná data, která se předávala do konstruktoru při vytváření instance. Pattern matching není pouhým switchem, jedná spíše o extrakci dat spojenou s následným větvením kódu podle výsledků extrakce, tedy o zobecnění switche, tak jak jej známe z Javy/C. Nic nám ale nebrání jej použít i jako klasický switch pro větvení kódu podle konstantních hodnot (čísel, řetězců apd.).

Myšlenka pattern matchingu není však inspirována jazyky rodiny C, má své kořeny v teorii algebraických datových typů, kompozitních typů, které jsou vytvářeny kombinací jiných typů a analyzovány (rozkládány) pomocí pattern matchingu. Pattern matching je tedy známou konstrukcí ve funkcionálních jazycích Haskell, OCaml, Standard ML aj.

Syntaxe výrazu match

Za matchovaným výrazem se uvádí klíčové slovo match a poté složené závorky se seznamem case větví. Za klíčovým slovem case vždy následuje matchovaný pattern, poté symbol => a příkaz nebo seznam příkazů, který se vykoná, pokud výraz vyhovuje matchovanému patternu. Pokud výraz vyhoví, dalšími case větvemi se již neprochází. Nikde nemusíme psát žádný "break" (máme zaručeno, že se vykoná vždy nejvýše jedna větev).

Pokud žádná z case větví výraz nematchovala, vyvolá se výjimka scala.MatchError. Case větve musí pokrývat všechny případy, pokud se při běhu programu nechceme setkat s touto výjimkou. Nezajímavé/obecné případy můžeme shrnout např. pod poslední větev case _, která matchuje vše. Kompilátor Scaly zobrazuje varování, pokud nemáme v match výrazu zahrnuté všechny možné případy a zároveň přímo uvádí patterny, které match nepokrývá, a kvůli kterým bychom mohli dostat MatchError.

Match je ve skutečnosti výraz s návratovou hodnotou - vrací výsledek vyhodnocení větve, která výraz matchovala (společný nadtyp typů, které jsou vraceny jednotlivými větvemi):

Výraz <jméno> @ můžeme použít před jakýmkoliv patternem, do pojmenované hodnoty se pak uloží hodnota matchovaná daným patternem.

Extrakce dat z objektů

Aby objekt mohl být dekomponován a matchovány také jeho parametry (data, která obsahuje), musí implementovat metodu unapply, pomocí které se provádí extrakce dat z objektu. Metoda unapply je duální operací k metodě apply, která se typicky používá jako factory metoda pro konstrukci instance ze vstupních parametrů. Obě tyto speciální metody jsou rozpoznávány Scala kompilátorem.

Všechny case classes mají metodu unapply automaticky vygenerovanou kompilátorem, a můžeme je tak přímo používat v pattern matchingu. V ostatních třídách musíme metodu unapply doimplementovat, pokud z nich chceme data přes pattern matching snadno extrahovat.

Pattern matching nabízí práci na příjemnější vyšší úrovni, než je přímé použití metod isInstanceOf[T] a asInstanceOf[T] (přetypování), které jsou dost nízkoúrovňové. Navíc samotné přetypování není typově bezpečné (správnost použití nám samotný kompilátor nezkontroluje).

Pokud bychom se pokusili napsat match výraz s patterny nekompatibilních typů, výraz se ani nezkompiluje:

"foo" match { case i: Int => println(i) }

Druhy matchovaných patternů

Stručně si představíme jednotlivé typy patternů, které můžeme zapsat za klíčové slovo case v pattern matchingu.

Typed pattern

Typed pattern s uložením matchované nenullové instance daného typu do proměnné. Takto lze matchovat nejen např. case classes, ale funguje i na hodnotové typy jako Int aj.

case n: Number => n 

Constructor pattern

Constructor pattern s volitelným uložením matchované instance do proměnné (zde op). Matchuje nejen typ objektu ale také jeho parametry - data, která lze z objektu extrahovat. Na parametry se opět rekurzivním způsobem uplatňuje pattern matching (deep matching) - mohou být matchovány opět constructor patternem (vnořené extrahovatelné třídy), variable patternem, constant patternem, ...

case op @ BinOperation("+", expr, Number(n)) => op

Constructor pattern podporuje třídy s implementovanou metodou unapply (např. case classes, mezi které patří i n-tice - tuples) a také typy reprezentující sekvence. Matchovat lze např. List(0, 1, _*), seznam s přesně zadanými prvními dvěma elementy, _* matchuje dalších 0 a více elementů sekvence.

Variable pattern

Variable pattern, který matchuje cokoliv - matchovaná instance se uloží do pojmenované hodnoty.

case x => x

Constant pattern

Matchuje hodnotu oproti pojmenované konstantě. Od variable patternu se liší konvencí - první písmeno názvu konstanty je velké. Pokud by název začínal malým písmenem, použil by se variable pattern. Kvalifikované jméno jako math.Pi je vždy považováno za jméno konstanty. Také val a object mohou být použity v constant patternu (např. Nil matchuje pouze prázdný List).

case Pi => println("PI = " + Pi)

Pattern guards

Za patternem v case větvi, těsně před šipkou (=>), může být uveden výraz if (<podmínka>). V podmínce můžeme používat proměnné, do kterých jsme si uložili hodnoty z patternu. Pokud podmínka nebude splněna, case větev výraz nenamatchuje a pokračuje se vyhodnocováním dalších case větví.

Wildcards

Namísto názvu proměnné můžeme ve variable patternu použít _ (pokud nás v dalším kódu ve skutečnosti hodnota proměnné nezajímá). V typed patternu můžeme použít _ namísto konkrétních typových parametrů (např. m: Map[_,_]). Také v constructor patternu se _ výborně hodí jako zástupný symbol pro parametr, který namatchuje cokoliv, když nás konkrétní hodnota parametru v dalším zpracování nezajímá. Jak je vidět, podtržítko je ve Scale zvláště oblíbený a všemocný symbol.

Zdroje

  1. Odersky, Martin, Lex Spoon, Bill Venners. Programming in Scala, Second Edition. Artima Press, 2010. Web: http://booksites.artima.com/programming_in_scala_2ed.
  2. Odersky, Martin. Functional Programming Principles in Scala. 2012. Dostupné na webu: https://class.coursera.org/progfun-2012-001/class/index.

Článek obsahuje 0 komentářů