Статьи

Scala Pattern Matching: случай для нового мышления?

16-й президент США. Авраам Линкольн однажды сказал: «Поскольку наше дело новое, мы должны думать и действовать заново». В разработке программного обеспечения вещи, вероятно, не так драматичны, как гражданские войны и отмена рабства, но у нас есть интересные логические концепции относительно «случая». В Java оператор case предусматривает некоторое ограниченное условное ветвление. В Scala можно построить очень сложную логику сопоставления с образцом, используя конструкцию case / match, которая не просто приносит новые возможности, но и новый тип мышления для реализации новых возможностей.

Давайте начнем с классического домашнего задания 1-го курса Computer Science: серия Фибоначчи, которая не начинается с 0, 1, а начинается с 1, 1. Таким образом, серия будет выглядеть так: 1, 1, 2, 3, 5, 8 , 13, …каждое число является суммой двух предыдущих,

В Java мы могли бы сделать: 

public int fibonacci(int i) {
    if (i < 0) 
        return 0;
    switch(i) {
        case 0:
            return 1;
        case 1:
            return 1;
        default:
            return fibonacci(i-1) + fibonacci(i - 2);
    }
}     

Все прямо вперед. Если 0он передается, он считается первым элементом в серии, поэтому 1должен быть возвращен. Примечание: чтобы добавить еще немного остроты на вечеринку и сделать вещи немного более интересными, я добавил немного логики, чтобы возвращать, 0если отрицательное число передается в наш метод Фибоначчи.

В Scala для достижения того же поведения мы бы сделали:

def fibonacci(in: Int): Int = {
  in match {
    case n if n <= 0 => 0
    case 0 | 1 => 1
    case n => fibonacci(n - 1) + fibonacci(n- 2)
  }
}

Ключевые моменты:

  • Тип возврата рекурсивного метода Фибоначчи — Int. Рекурсивные методы должны явно указывать тип возвращаемого значения (см .: Одерский — Программирование в Scala — Глава 2).
  • Можно проверить наличие нескольких значений в одной строке, используя |обозначения. Я делаю это, чтобы вернуть 1 для 0 и 1 в строке 4 примера .
  • Нет необходимости в нескольких returnутверждениях. В Java вы должны использовать несколько returnоператоров или несколько breakоператоров.
  • Сопоставление с образцом — это выражение, которое всегда что-то возвращает.
  • В этом примере я использую охрану для проверки на отрицательное число, и если число отрицательное, возвращается ноль.
  • В Scala также можно проверять разные типы. Также возможно использовать символ подстановки _. Мы не использовали ни в Фибоначчи, но просто чтобы проиллюстрировать эти функции …
    def multitypes(in: Any): String = in match {
      case i:Int => "You are an int!"
      case "Alex" => "You must be Alex"
      case s:String => "I don't know who you are but I know you are a String"
      case _ => "I haven't a clue who you are"
    }

Сопоставление с образцом может быть использовано с Scala Maps для полезного эффекта. Предположим, у нас есть Карта, чтобы отразить, кто, по нашему мнению, должен играть в каждой позиции линии Lions для австралийской серии Lions . Ключи карты будут позицией в задней линии, а соответствующее значение будет игроком, который, по нашему мнению, должен играть там. Для представления игрока в регби мы используем класс case . Теперь, вы, Java-главы, представьте, что класс case является неизменяемым POJO, написанным в чрезвычайно сжатой форме — они тоже могут быть изменяемыми, но пока думаем, что они неизменные.

case class RugbyPlayer(name: String, country: String);
val robKearney = RugbyPlayer("Rob Kearney", "Ireland");
val georgeNorth = RugbyPlayer("George North", "Wales");
val brianODriscol = RugbyPlayer("Brian O'Driscol", "Ireland");
val jonnySexton = RugbyPlayer("Jonny Sexton", "Ireland");  
val benYoungs = RugbyPlayer("Ben Youngs", "England");
     
// build a map
val lionsPlayers = Map("FullBack" -> robKearney, "RightWing" -> georgeNorth, 
      "OutsideCentre" -> brianODriscol, "Outhalf" -> jonnySexton, "Scrumhalf" -> benYoungs);
     
// Note: Unlike Java HashMaps Scala Maps can return nulls. This achieved by returing
// an Option which can either be Some or None. 
     
// So, if we ask for something that exists in the Map like below
println(lionsPlayers.get("Outhalf"));  
// Outputs: Some(RugbyPlayer(Jonny Sexton,Ireland))
     
// If we ask for something that is not in the Map yet like below
println(lionsPlayers.get("InsideCentre"));
// Outputs: None

В этом примере у нас есть игроки на каждую позицию, кроме внутреннего центра — о чем мы не можем определиться . Карты Scala позволяют хранить значения NULL в качестве значений. Теперь в нашем случае мы фактически не храним нулевое значение для внутреннего центра . Таким образом, вместо того, чтобы возвращать значение null для внутреннего центра (как, если бы мы использовали Java HashMap), Noneвозвращается тип .

Для других позиций в задней строке у нас есть совпадающие значения, и Someвозвращается тип, который оборачивается вокруг соответствующего RugbyPlayer. (Примечание: оба Someи None  простираются от Option).

Мы можем написать функцию, образец которой соответствует на возвращаемое значение из HashMap и возвращает нам что-то более удобное для пользователя.

def show(x: Option[RugbyPlayer]) = x match {
  case Some(rugbyPlayerExt) => rugbyPlayerExt.name  // If a rugby player is matched return its name
  case None => "Not decided yet ?" // 
}
println(show(lionsPlayers.get("Outhalf")))  // outputs: Jonny Sexton
println(show(lionsPlayers.get("InsideCentre"))) // Outputs: Not decided yet

Этот пример иллюстрирует не только сопоставление с образцом, но и другую концепцию, известную как извлечение . Игрок в регби при совпадении извлекается и присваивается rugbyPlayerExt. Затем мы можем вернуть значение имени игрока в регби, получив его от rugbyPlayerExt. На самом деле, мы также можем добавить охрану и изменить некоторую логику. Предположим, у нас был предвзятый журналист ( Стивен Джонс ), который не хотел, чтобы в команде были ирландские игроки. Он мог реализовать свою собственную предвзятую функцию, чтобы проверить ирландских игроков

def biasedShow(x: Option[RugbyPlayer]) = x match {
  case Some(rugbyPlayerExt) if rugbyPlayerExt.country == "Ireland" => 
     rugbyPlayerExt.name + ", don't pick him."
  case Some(rugbyPlayerExt) => rugbyPlayerExt.name
  case None => "Not decided yet ?"
}
println(biasedShow(lionsPlayers.get("Outhalf"))) // Outputs Jonny... don't pick him
println(biasedShow(lionsPlayers.get("Scrumhalf"))) // Outputs Ben Youngs

Коллекции соответствия шаблонов

Scala также предоставляет некоторые мощные функции сопоставления с образцами для коллекций. Вот тривиальный экзамен для получения длины списка.

def length[A](list : List[A]) : Int = list match {
  case _ :: tail => 1 + length(tail)
  case Nil => 0
}

И предположим, что мы хотим разобрать аргументы из кортежа …

def parseArgument(arg : String, value: Any) = (arg, value) match {
  case ("-l", lang) => setLanguage(lang)  
  case ("-o" | "--optim", n : Int) if ((0 < n) && (n <= 3)) => setOptimizationLevel(n)
  case ("-h" | "--help", null) => displayHelp()
  case bad => badArgument(bad)
}

Функции с одним параметром


Рассмотрим список чисел от 1 до 10. Метод фильтра принимает функцию с одним параметром, которая возвращает
trueили
false. Функция с одним параметром может применяться для каждого элемента в списке и будет возвращаться
trueили
falseдля каждого элемента. Возвращаемые элементы
trueбудут отфильтрованы; возвращаемые элементы
falseбудут отфильтрованы из результирующего списка.

scala> val myList = List(1,2,3,4,5,6,7,8,9,10)
myList: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
 
scala> myList.filter(x => x % 2 ==1)
res13: List[Int] = List(1, 3, 5, 7, 9)

Теперь сейчас, слушай и запомни это.
Шаблон может быть передан любому методу, который принимает функцию с одним параметром . Вместо передачи функции с одним параметром, которая всегда возвращала истину или ложь, мы могли бы использовать шаблон, который всегда возвращает истину или ложь.

scala> myList.filter {
     |     case i: Int => i % 2 == 1   // odd number will return false
     |     case _ => false             // anything else will return false
     | }
res14: List[Int] = List(1, 3, 5, 7, 9)

Использовать это позже?

Scala компилирует шаблоны в
PartialFunction. Это означает, что выражения шаблона Scala не только могут быть переданы другим функциям, но также могут быть сохранены для дальнейшего использования.

scala> val patternToUseLater = : PartialFunction[String, String] = {
     |   case "Dublin" => "Ireland"
     |   case _ => "Unknown"
      }


Этот пример говорит о том,
patternToUseLaterчто это частичная функция, которая принимает строку и возвращает строку. Последний элемент состояния в функции возвращается по умолчанию, и поскольку выражение case является частичной функцией, оно будет возвращено как частичная функция и назначено,
pattenrToUseLaterчто, конечно, может использовать ее позже.

Наконец,
Джонни Секстон — феноменальный игрок в регби, и стыдно слышать, что он покидает Ленстер. Очевидно, что из-за напряженного графика Секстона мы не можем быть уверены, что Джонни читает этот блог, но если он, Джонни, извините, что вы ушли, мы желаем вам всего наилучшего и, надеюсь, еще раз увидимся в Голубом Джерси.