Статьи

Сопоставление с шаблоном Scala: случай для нового мышления?

Новое мышление?

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
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 для достижения того же поведения мы бы сделали:

1
2
3
4
5
6
7
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 также можно проверять разные типы. Также можно использовать символ подстановки _ . Мы не использовали ни в Фибоначчи, но просто чтобы проиллюстрировать эти функции …
    1
    2
    3
    4
    5
    6
    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 . Ключи карты будут позицией в задней линии, а соответствующее значение будет игроком, который, по нашему мнению, должен играть там. Для представления игрока в регби мы используем
кейс-класс . Теперь, вы, Java-главы, представьте, что класс case является неизменяемым POJO, написанным в чрезвычайно сжатой форме — они тоже могут быть изменяемыми, но пока думаем, что они неизменные.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 и Option расширяются от Option ). Мы можем написать функцию, шаблон которой соответствует возвращаемому значению из HashMap и возвращает нам что-то более удобное для пользователя.

1
2
3
4
5
6
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 . Затем мы можем вернуть значение имени игрока в регби, получив его из rugbyPlayerExt . На самом деле, мы также можем добавить охрану и изменить некоторую логику. Предположим, у нас был предвзятый журналист (
Стивен Джонс ), который не хотел, чтобы в команде были ирландские игроки. Он мог реализовать свою собственную предвзятую функцию, чтобы проверить ирландских игроков

1
2
3
4
5
6
7
8
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 также предоставляет некоторые мощные функции сопоставления с образцами для коллекций. Вот тривиальный экзамен для получения длины списка.

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

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

1
2
3
4
5
6
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. Метод filter принимает функцию с одним параметром, которая возвращает
true или false . Функция с одним параметром может применяться для каждого элемента в списке и будет возвращать true или false для каждого элемента. Элементы, которые возвращают true будут отфильтрованы; элементы, которые возвращают false будут отфильтрованы из результирующего списка.

1
2
3
4
5
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)

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

1
2
3
4
5
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 не только могут быть переданы другим функциям, но также могут быть сохранены для последующего использования.

1
2
3
4
scala> val patternToUseLater = : PartialFunction[String, String] = {
     |   case 'Dublin' => 'Ireland'
     |   case _ => 'Unknown'
      }

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

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

Ссылка: сопоставление с образцом Scala: случай для нового мышления? от нашего партнера JCG Алекса Стейвли в блоге Techlin в Дублине .