Понимание того, как использовать параметры в распространенных сценариях

Scala становится все популярнее как альтернатива Java. Он имеет строгую систему типов, полезные языковые функции, такие как сопоставление с образцом, и поддержку функционального программирования. К сожалению, особенности, которые отличают Scala от более «традиционных» языков, также могут затруднить переход к программированию на Scala. Если вы недавно начали работать над проектом Scala, одной из первых языковых функций, которые вам, возможно, потребуется понять, являются Параметры.

Что такое параметры в Scala?

Option - это контейнер для ровно 0 или 1 экземпляров любого указанного вами типа (например, Boolean, Int, List и т. Д.). Один из способов представить себе вариант - это как ящик: вы знаете, что в нем находится, и когда вы открываете ящик, в нем либо что-то есть, либо ничего.

Как вы «открываете» опцию, чтобы увидеть, есть ли в ней что-нибудь? Если он содержит элемент, Option будет экземпляром Some; если он «пустой», Option будет экземпляром None.

Мы можем увидеть это в действии, вызвав функцию List find, которая возвращает Option:

// Scala
val numbers: List[Int] = 1 :: 3 :: 5 :: 7 :: Nil
val result: Option[Int] = numbers.find(_ > 4)
result match {
  case Some(numberFound) => print(s"$numberFound is greater than 4")
  case None => print("There aren't any numbers > 4)
}
// Prints "5 is greater than 4"

В этом примере функция find возвращает параметр, содержащий тип Int. Это говорит нам о том, что result - это Option, который либо содержит один Int, либо пуст. Поскольку список numbers содержит элементы больше 4, result будет экземпляром Some, содержащим значение 5.

Чем полезны опции?

На первый взгляд Options может показаться не такой уж большой проблемой. Однако они становятся действительно мощными, когда заменяют функции, которые могут возвращать null. Давайте посмотрим на простой пример:

// Java
public String someFunction(String argument) {
    // do some work
    return someValue;
}

Эта функция, по-видимому, принимает значение String и возвращает значение String, но так ли это на самом деле? Как мы узнаем, может ли функция обрабатывать нулевые значения? Гарантируется ли, что возвращаемое значение будет ненулевым, или нам нужно проверить, не является ли оно ненулевым после вызова этой функции? Незнание, когда проверять нулевые значения, является рецептом для множества исключений NullPointerExceptions. Единственный способ убедиться в этом - прочитать документацию или код внутри функции.

Однако в Scala:

// Scala
def someFunction(arg: String): Option[String] = {
  // do some work
  someOption
}

В отличие от примера Java, это объявление функции более явно указывает, что она принимает и возвращает. Он определяет, что ему требуется (ненулевой) String, и потребитель функции вынужден проверять, имеет ли возвращаемый Option значение.

Давайте посмотрим на это на другом примере

Рассмотрим Java HashMap:

// Java
HashMap simpleParse = new HashMap<Integer, String>();
simpleParse.put(1, "one");
simpleParse.put(2, "two");
simpleParse.put(3, "three");
String word1 = (String)simpleParse.get(1);
String word4 = (String)simpleParse.get(4);

Оба вызова simpleParse.get выполняются и возвращают строки, но word4 действительно имеет значение null. Если функция должна вернуть вызов simpleParse.get, любой, кто вызывает эту функцию, может не знать, что нужно проверять нулевые значения.

Параметры явно определяют, когда функция может или не может возвращать значение. Давайте посмотрим на тот же сценарий в Scala с параметрами:

// Scala
val simpleParse = Map(1 -> "one", 2 -> "two", 3 -> "three")
val word1: Option[String] = simpleParse.get(1)
val word4: Option[String] = simpleParse.get(4)

Сделано таким образом, и word1, и word4 являются параметрами типа string, но word1 будет экземпляром Some, а word4 будет экземпляром None. Любые функции, возвращающие simpleParse.get, требуют, чтобы вызывающий проверил, имеет ли параметр значение. Больше никаких исключений NullPointerExceptions.

Слово предостережения

В Scala's Option есть метод, который я рекомендую никогда не использовать: get()

Этот метод часто используется в некоторых онлайн-руководствах и отличается от метода Map.get(), использованного в приведенных выше примерах. Функция Option.get возвращает значение непустого параметра Option; если Option пуст, он выдает java.util.NoSuchElementException.

Итак, если мы используем параметры word1 и word4, указанные выше:

// Scala
val result1 = word1.get // result1 == "one"
val result4 = word4.get // throws NoSuchElementException

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

Как работать с опциями

В следующих примерах мы исследуем общие операции в Java и способы их улучшения с помощью параметров Scala.

Во-первых, примечание о методах Option:

Многие методы членов в Options аналогичны методам, которые существуют в коллекциях, таких как Lists. Например, обычно используемые методы Option включают map, flatMap, foreach, filter, exists и т. Д.

Поначалу может сбивать с толку их использование в параметрах, но в этих случаях может быть полезно думать о параметрах как о списках, которые имеют значение 0 или 1. Мы увидим несколько примеров этого в следующих случаях.

Выполнение действия, если значение не равно нулю

В большинстве приложений будет много нулевых проверок, которые выглядят примерно так:

// Java
String parsed = (String)simpleParse.get(3);
if (parsed != null) {
    // do some work involving the parsed value
}

Теперь давайте посмотрим на общий способ выполнения этой функции с помощью параметров:

// Scala
simpleParse.get(3).map { parsed =>
    // do some work involving the parsed value
}

Обратите внимание на использование map в опции, возвращаемой simpleParse.get. Думайте о Option в этом случае как о списке с 0 или 1 элементами. Если опция пуста, следующий за ней блок кода запускаться не будет. Если Option имеет значение, оно присваивается переменной parsed, которая, как мы знаем, не равна нулю и может использоваться в блоке кода.

Назначение значения по умолчанию при нулевом значении

Допустим, мы хотим получить значение от simpleParse, но если мы не получаем результат, мы хотим присвоить слово «неизвестно». В Java мы могли бы сделать что-то вроде этого:

// Java
String parsed = (String)simpleParse.get(-2);
String result = (parsed != null) ? parsed : "unknown";

Используя тернарный оператор, мы смогли попробовать получить значение с карты и присвоить значение по умолчанию в 2 строки. Поскольку функция Scala Map.get возвращает Option, мы можем использовать вместо нее функцию getOrElse Option:

// Scala
val result: String = simpleParse.get(-2).getOrElse("unknown")

В одной строке мы можем безопасно выполнить одну и ту же задачу! Вызов getOrElse для Option возвращает значение Option, если оно не пустое. Поскольку наш вызов simpleParse.get(-2) возвращает пустой Option, getOrElse вместо этого возвращает значение, которое мы предоставили, в данном случае строку «unknown».

Что, если мы хотим поработать, прежде чем присвоить значение по умолчанию?

// Java
String parsed = (String)simpleParse.get(-2);
String result;
if (parsed != null) {
    result = parsed;
} else {
    // do some task, like logging parse failure
    result = "unknown";
}

В этом примере мы можем выполнить некоторую задачу перед присвоением result значения по умолчанию. Это увеличило количество кода, требуемого для партии.

Посмотрим, как это можно сделать в Scala:

// Scala
val result = simpleParse.get(-2).getOrElse {
    // do some task, like logging parse failure
    "unknown"
}

Версия Scala дает тот же результат с меньшим количеством кода и более четким потоком.

Манипулирование проанализированным значением перед назначением

Допустим, в вашем Java-коде вы хотите выполнить некоторые действия перед назначением result = parsed, например, зарегистрировать успешный синтаксический анализ или добавить сообщение. На Java это может выглядеть примерно так:

// Java
String parsed = (String)simpleParse.get(2);
String result;
if (parsed != null) {
    // do some task, like logging the successful parse
    result = "Parsed number: " + parsed;
} else {
    // do some other task
    result = "unknown";
}

Чтобы добиться этого в Scala, мы можем объединить уже изученные примеры:

// Scala
val result = simpleParse.get(2).map { parsed => 
    // do some task, like logging the successful parse
    "Parsed number: " + parsed
}.getOrElse {
    // do some other task
    "unknown"
}

Давайте разберемся с этим.

Функция map концептуально изменяет содержимое Option с вызова на simpleParse.get(2). Он не возвращает измененную строку, а возвращает параметр, содержащий измененную строку. В этом случае непустая опция, содержащая «два», становится непустой опцией, содержащей «проанализированное число: два», к тому времени, когда она достигает вызова getOrElse. Таким образом, функция просто возвращает значение в Option и пропускает следующий блок кода.

Если вызов simpleParse.get должен был дать пустой Option, вызов map пропустил бы следующий блок кода и вместо этого остался бы пустым, что привело бы к вызову getOrElse для выполнения его определенной настройки блока кода result на «неизвестно».

Обработка вложенных параметров

Часто значения Java, которые могут быть нулевыми, передаются другим функциям, которые также могут создавать нулевые значения. Этот сценарий мало что изменит:

// Java
String parsed = (String)simpleParse.get(-1);
String result;
if (parsed != null) {
    result = functionThatMayReturnNull(parsed);
    // result is either a String or null
}
// do something based on the value of result

Однако в Scala это может немного отличаться:

// Scala
val result = simpleParse.get(-1).map { parsed =>
  functionThatReturnsAnOption(parsed)
}
// result is an Option[Option[String]]
// do something based on the value of result

Вызов map возвращает Option, содержащий все, что оценивается блоком кода. Поскольку мы вызываем функцию, которая возвращает параметр типа String, мы получаем вложенный параметр. Чтобы избавиться от вложенного Option, мы могли бы сделать что-то вроде этого:

// Scala
val result = simpleParse.get(-1).map { parsed =>
  functionThatReturnsAnOption(parsed).getOrElse("")
}
// result is an Option[String]

Эта модификация извлекает значение параметра или устанавливает значение по умолчанию внутри блока кода map. Хотя в некоторых случаях это может быть правильным шаблоном, обычно более правильным будет использовать flatMap:

// Scala
val result = simpleParse.get(-1).flatMap { parsed =>
  functionThatReturnsAnOption(parsed)
}
// result is an Option[String]

Параметры сведения с flatMap концептуально аналогичны использованию flatMap в списке. Поскольку Option может иметь только 0 или 1 значение, мы можем думать о flatMap как об удалении самого внешнего Option, оставляя нам только внутренний Option, пустой или непустой. Этот шаблон больше всего похож на приведенный выше пример Java.

Вывод

При использовании в качестве альтернативы нулевым значениям параметры позволяют более явно указать, будет ли функция возвращать значение. Они требуют от потребителя выполнения проверок, предотвращающих непредвиденные исключения NullPointerExceptions, и упрощают запоминание крайних случаев, поощряя более надежный код. Опции также предоставляют отличные инструменты для уменьшения количества необходимых строк кода при сохранении читабельности и надежности.

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