14.12. Ekstraktory

Mechanizm dopasowywania wzorców nie jest ograniczony tylko do klas przypadku i standardowych klas języka Scala. We wzorcach można używać tak zwanych ekstraktorów. Ekstraktor jest obiektem, który posiada odpowiednio zdefiniowaną metodę lub metody unapply lub unapplySeq. Przykład ekstraktora znajduje się w pliku Extractors1.scala.

Plik Extractors1.scala:
object HasUpperCase1 {
  def unapply(s: String): Boolean = s.exists(_.isUpper)
}

W obiekcie HasUpperCase1 jest zdefiniowana metoda unapply, która przyjmuje parametr typu String, a zwraca wartość logiczną. W przypadku, gdy parametrem metody jest łańcuch znakowy zawierający przynajmniej jedną dużą literę, metoda zwraca wartość true, oznaczającą udane dopasowanie wzorca, a w przeciwnym wypadku false, co oznacza brak dopasowania. Tak zdefiniowany obiekt można wykorzystywać do dopasowywania wzorców. Należy pamiętać o nawiasach następujących po nazwie obiektu. Poniższe przykłady pokazują wykorzystanie ekstraktora HasUpperCase w wyrażeniach match.

scala> "abc" match { case HasUpperCase1() => "yes" case _ => "no" }
res0: String = no

scala> "AbC" match { case HasUpperCase1() => "yes" case _ => "no" }
res1: String = yes

Niestety, próba sprawdzenia wartości liczbowej powoduje błąd.

scala> 123 match { case HasUpperCase1() => "yes" case _ => "no" }
<console>:11: error: scrutinee is incompatible with pattern type;
 found   : String
 required: Int
       123 match { case HasUpperCase1() => "yes" case _ => "no" }
                                     ^

Ekstraktor przygotowany jest do sprawdzania wartości typu String, ale nie typu Int. Plik Extractors2.scala zawiera ekstraktor HasUpperCase2, który rozwiązuje ten problem poprzez dodanie nowej metody unapply, przyjmującej wartości typu Any.

Plik Extractors2.scala:
object HasUpperCase2 {
  def unapply(s: String): Boolean = s.exists(_.isUpper)
  def unapply(s: Any): Boolean = false
}

Tak zdefiniowany ekstraktor działa z różnymi typami danych. Można go wykorzystywać do sprawdzania, czy wartość dowolnego typu jest łańcuchem znakowym mającym jakieś duże litery.

scala> "Abc" match { case HasUpperCase2() => "yes" case _ => "no" }
res3: String = yes

scala> "abc" match { case HasUpperCase2() => "yes" case _ => "no" }
res4: String = no

scala> 123 match { case HasUpperCase2() => "yes" case _ => "no" }
res5: String = no

scala> 123.5 match { case HasUpperCase2() => "yes" case _ => "no" }
res6: String = no

Ekstraktor zdefiniowany w pliku Extractors3.scala realizuje dodatkowe zadanie. Oprócz zwracania informacji o tym, czy łańcuch znakowy zawiera duże litery, potrafi wyłuskać i zwrócić te litery z łańcucha znaków. Tym razem wartością zwracaną przez metodę unapply jest instancja klasy MatchResult.

Plik Extractors3.scala:
object HasUpperCase3 {
  class MatchResult(val s: String) extends AnyVal {
    def get = new MatchElements(s)
    def isEmpty = s.count(_.isUpper) < 2
  }
  class MatchElements(s: String) {
    val u = s.filter(_.isUpper)
    def _1 : Char = u.charAt(0)
    def _2 : Char = u.charAt(1)
    def _3 : String = u.substring(2)
  }
  def unapply(x: Any): MatchResult = x match {
    case s: String => new MatchResult(s)
    case _ => new MatchResult("")
  }
}

Instancja klasy MatchResult jest tylko przykładowym, jednym z wielu możliwych obiektów, jakie mogą być zwracane przez metodę unapply w samodzielnie definiowanych ekstraktorach. Mógłby to być obiekt innego typu, ale powinien udostępniać następujące dwie metody:

Pierwsza z nich informuje, czy dopasowanie może się udać. Druga natomiast zwraca obiekt pozwalający na wyłuskanie wartości parametrów ekstraktora. Poszczególne parametry ekstraktora odpowiadają metodom takiego obiektu o nazwach _1, _2, _3 i tak dalej. Liczba parametrów ekstraktora oraz ich typy zależą od tego, ile takich metod oraz jakich typów udostępnia obiekt zwracany przez metodę get. W przykładzie mamy trzy takie metody, więc ekstraktor ma trzy parametry. Poniższe wyrażenia pokazują przykładowe sposoby użycia ekstraktora oraz nieudane próby użycia go z dwoma i czterema parametrami.

scala> "AbCdEfG" match { case HasUpperCase3(a,b,c) => s"$a,$b,$c" case _ => "no match" }
res7: String = A,C,EG

scala> "AbCd" match { case HasUpperCase3(a,b,c) => s"$a,$b,$c" case _ => "no match" }
res8: String = A,C,

scala> "Abcd" match { case HasUpperCase3(a,b,c) => s"$a,$b,$c" case _ => "no match" }
res9: String = no match

scala> "AbCd" match { case HasUpperCase3(_,_) => "yes" case _ => "no" }
<console>:11: error: not enough patterns for object HasUpperCase3 offering (Char, Char, String): expected 3, found 2
       "AbCd" match { case HasUpperCase3(_,_) => "yes" case _ => "no" }
                           ^

scala> "AbCd" match { case HasUpperCase3(_,_,_,_) => "yes" case _ => "no" }
<console>:11: error: too many patterns for object HasUpperCase3 offering (Char, Char, String): expected 3, found 4
       "AbCd" match { case HasUpperCase3(_,_,_,_) => "yes" case _ => "no" }
                           ^

Metoda get może również zwracać obiekt niezawierający metody _1. W takim przypadku ekstraktor ma jeden parametr. Przykładem takiego ekstraktora jest obiekt HasUpperCase4 z pliku Extractors4.scala.

Plik Extractors4.scala:
object HasUpperCase4 {
  class MatchResult(val s: String) extends AnyVal {
    def get: String = s.filter(_.isUpper)
    def isEmpty = !s.exists(_.isUpper)
  }
  def unapply(x: Any): MatchResult = x match {
    case s: String => new MatchResult(s)
    case _ => new MatchResult("")
  }
}

Ekstraktor HasUpperCase4 może posłużyć do wyłuskania wszystkich dużych liter podanego napisu.

scala> "AbCdEfG" match { case HasUpperCase4(a) => a case _ => "no match" }
res12: String = ACEG

scala> "abcd" match { case HasUpperCase4(a) => a case _ => "no match" }
res13: String = no match

Scala umożliwia definiowanie ekstraktorów przyjmujących zmienną liczbę parametrów. Taki ekstraktor można utworzyć implementując metodę unapplySeq zdefiniowaną według reguł podobnych do opisanych dla metod unapply (metoda powinna zwracać obiekt z metodami isEmpty oraz get). Metoda get powinna albo zwracać wartość typu Seq[T] (w takim przypadku ekstraktor może być używany z dowolną liczbą parametrów), albo obiekt zawierający metody o nazwach _1, _2, itd., z których ostatnia zwraca wartość typu Seq[T] (w takim przypadku ekstraktor może mieć pewną minimalną liczbę wymaganych parametrów). Przykłady z plików Extractors5.scala oraz Extractors6.scala pokazują tak zdefiniowane ekstraktory.

Plik Extractors5.scala:
object HasUpperCase5 {
  class MatchResult(val s: String) extends AnyVal {
    def get = new MatchElements(s)
    def isEmpty = s.count(_.isUpper) < 2
  }
  class MatchElements(s: String) {
    val u = s.filter(_.isUpper)
    def _1: Char = u.charAt(0)
    def _2: Char = u.charAt(1)
    def _3: Seq[Char] = u.substring(2).toSeq
  }
  def unapplySeq(x: Any): MatchResult = x match {
    case s: String => new MatchResult(s)
    case _ => new MatchResult("")
  }
}
Plik Extractors6.scala:
object HasUpperCase6 {
  class MatchResult(val s: String) extends AnyVal {
    def get: Seq[Char] = s.filter(_.isUpper).toSeq
    def isEmpty = !s.exists(_.isUpper)
  }
  def unapplySeq(x: Any): MatchResult = x match {
    case s: String => new MatchResult(s)
    case _ => new MatchResult("")
  }
}

Ekstraktor HasUpperCase5 można używać z wykorzystaniem dwóch lub więcej parametrów (obiekty klasy MatchElements mają zdefiniowane metody _1, _2 i _3, z których ostatnia zwraca sekwencję). Każdy z parametrów reprezentuje jedną z wielkich liter sprawdzanego napisu.

scala> "AbCd" match { case HasUpperCase5(a,b) => s"$a,$b" case _ => "no match" }
res14: String = A,C

scala> "Abcd" match { case HasUpperCase5(a,b) => s"$a,$b" case _ => "no match" }
res15: String = no match

scala> "AbDdE" match { case HasUpperCase5(a,b) => s"$a,$b" case _ => "no match" }
res16: String = no match

scala> "AbCdE" match { case HasUpperCase5(a,b,c) => s"$a,$b,$c" case _ => "no match" }
res17: String = A,C,E

scala> "AbCdEfGeH" match { case HasUpperCase5(a,b,_,_,c) => s"$a,$b,$c" case _ => "no match" }
res18: String = A,C,H

Próba użycia ekstraktora z mniejszą liczbą parametrów powoduje wystąpienie błędu.

scala> "AbCdE" match { case HasUpperCase5(a) => s"$a" case _ => "no match" }
<console>:11: error: not enough patterns for object HasUpperCase5 offering (Char, Char, Char*): expected at least 2, found 1
       "AbCdE" match { case HasUpperCase5(a) => s"$a" case _ => "no match" }
                            ^

Ekstraktor HasUpperCase6 można używać z dowolną liczbą parametrów.

scala> "Abcd" match { case HasUpperCase6() => "match" case _ => "no match" } 
res20: String = no match

scala> "abcd" match { case HasUpperCase6() => "match" case _ => "no match" } 
res21: String = no match

scala> "Abcd" match { case HasUpperCase6(a) => s"$a" case _ => "no match" } 
res22: String = A

scala> "AbCd" match { case HasUpperCase6(a) => s"$a" case _ => "no match" } 
res23: String = no match

scala> "AbCd" match { case HasUpperCase6(a,b) => s"$a,$b" case _ => "no match" } 
res24: String = A,C

Dopasowanie z wiersza nie udaje się, mimo że dopasowywany napis posiada wielką literę. Jednak liczba parametrów we wzorcu nie odpowiada liczbie wielkich liter. Podobnie jest w wierszu . Z kolei dopasowanie z wiersza nie udaje się, gdyż dopasowywany napis nie ma żadnej wielkiej litery. Liczba wielkich liter zgadza się z liczbą parametrów wzorca w wyrażeniach z wierszy oraz i te dopasowania udają się.

Język programowania Scala Wydanie 2. Copyright © Grzegorz Balcerek 2016

Licencja Creative Commons

Ten utwór jest dostępny na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowe.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.