Как Scala изменила мой взгляд на программирование

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

Мощное сопоставление с образцом

Сопоставление с образцом в Scala удивительно и широко используется. Сопоставление с образцом — это, по сути, оператор switch на стероидах, который позволяет вам сопоставлять по значению, типу и даже вложенным значениям. Вы также можете добавить условные операторы, чтобы обеспечить успешное совпадение только при выполнении указанного условия. Сопоставление с образцом всегда возвращает значение последнего вычисленного оператора, поэтому сопоставление с образцом также активно используется для создания новых переменных (см. ниже). Поскольку сопоставление с образцом настолько универсально, я заметил, что люди предпочитают его операторам if. Давайте посмотрим на некоторые примеры.

Монады

При написании бизнес-логики часто возникают необходимые особые случаи или обработка ошибок, которые усложняют наш код. Монады позволяют вам обрабатывать эти сложные сценарии четко и лаконично в декларативной манере. Проще говоря, монада — это просто интерфейс вокруг некоторых данных, который заставляет программиста обращаться к данным или изменять их определенным образом. Придерживаясь этого стандартизированного монадного интерфейса, мы можем аккуратно объединить несколько преобразований данных в конвейер, который выполняет нужные нам вычисления. Этот декларативный стиль программирования позволяет нам вычислять вещи, составляя функции вместо указания деталей реализации вычислений. Другими словами, монады позволяют программисту сосредоточиться на потоке данных, а не на потоке управления. Ниже мы рассмотрим три моих любимых монады Scala.

Вариант

Монада Option является оболочкой для значения, которое, возможно, равно null. Например, если вы хотите сохранить целое число, допускающее значение NULL, в Scala, вам следует создать переменную с типом Option[Int]. Если Option содержит целое число 5, то переменная будет иметь значение Some(5). Если Option пуст (иначе целое число равно null), переменная будет иметь значение None. Some и None оба являются дочерними классами класса Option. Если мы сохраним значение, допускающее значение NULL, в Option, компилятор заставит нас обрабатывать нулевые случаи при разыменовании Option. Ниже приведены несколько примеров использования параметров.

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

Да, мы можем сократить приведенный выше код, активно инициализируя viewerId = user.getId(), но давайте проигнорируем оптимизации, специфичные для примера, потому что они не защитят нас от ада проверки нулей в целом. Ниже приведена версия Scala с включенными для ясности типами возвращаемых функций.

Если getSubscriptionOpt, getPremiumTierOpt или getPremiumIdOpt вернут None, мы безопасно вернемся к идентификатору пользователя. Также обратите внимание на то, как встраивание возможности null в систему типов заставляет нас явно обрабатывать все случаи null, потому что в противном случае интерфейс Option не позволит нам получить доступ к хранимому значению. Мы могли бы остаться в мире опций, продолжая использовать flatMap, но если мы когда-нибудь захотим получить доступ к значению внутри, мы должны предоставить резервное значение. Вы можете подумать, что операторы Элвиса обеспечивают столь же мощную функциональность, но я не согласен, потому что map и flatMap также могут использоваться для выполнения более сложных многострочных лямбда-функций.

Будущее

Монада Future — это оболочка для асинхронной операции, которая либо завершится успешно, либо завершится ошибкой когда-нибудь в будущем. Фьючерсы позволяют легко связывать зависимые синхронизирующие или асинхронные операции вместе без блокировки. Вот несколько примеров использования фьючерсов.

Обратите внимание, как система типов услужливо показывает нам текущий тип, хранящийся в Future, до и после каждого преобразования. Вы видите, как Future flatMap позволяет нам создавать Futures на основе возвращаемого значения других Futures, аналогично тому, как мы можем создавать Options на основе возвращаемого значения других Options? Это сходство существует, потому что фьючерсы и опционы являются монадами.

Теперь давайте представим, что мы хотим вычислить некоторую строку, а затем сохранить слова этой строки в базе данных без блокировки. Предположим, что вычисление строк и операции сохранения базы данных являются асинхронными. Вот как мы можем сделать это, используя библиотеку Python asyncio.

Вот как мы бы сделали это на Scala.

computeString.map(_.split(" ")).flatMap(persistWords)

Конечно, мы можем создавать промежуточные переменные, если это делает код более читабельным, но мы не обязаны. Стоит отметить, что преобразования map и flatMap применяются только в случае успешного завершения Future, точно так же, как они применяются только к непустым параметрам. Если вы хотите элегантно обрабатывать будущие сбои, вы можете использовать Future.recover. Также ознакомьтесь со Scala для понимания, если вы заинтересованы в использовании более обобщенной версии модели async/await. Потому что включения — это просто синтаксический сахар для flatMap и некоторых других функций Scala, поэтому основные концепции остаются прежними.

Пытаться

Монада Try — это оболочка вокруг блока кода, которая либо успешно возвращает значение, либо выдает исключение. Это монадный эквивалент обычной функции try/catch, включенной в большинство языков. Если блок кода Try выдает исключение, монада вернет исключение, завернутое в объект Failure. Если блок кода Try выполняется успешно, монада возвращает значение последнего вычисленного оператора, заключенного в объект Success.

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

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

Вот версия Scala.

Другие приятные вещи

Неизменяемость

Одна из центральных идиом Scala — сделать все объекты неизменяемыми, если нет веской причины этого не делать. Неизменяемость отчасти является причиной того, что стиль вычислений map и flatMap работает так хорошо. Если бы мы передавали изменяемые объекты с состоянием через конвейер, это лишало бы цели, потому что тогда нам пришлось бы снова начинать отслеживать состояние. О неизменяемых объектах рассуждать проще, потому что не нужно отслеживать все возможные взаимодействия с этими объектами (см. антипаттерн действие на расстоянии). Неизменяемость также бесплатно дает нам потокобезопасность, поэтому компилятор может распараллелить чтение и лучше использовать преимущества современного многоядерного оборудования. Но неизменяемость также может привести к увеличению объема выделяемой памяти и скорости сборки мусора, так что это не всегда лучший выбор для каждого приложения.

Каждое выражение возвращает значение

Каждое выражение в Scala возвращает значение. Функции, сопоставления шаблонов, операторы if и т. д. будут неявно возвращать значение последнего вычисленного оператора. Считается лучшей практикой полагаться на эти неявные возвраты вместо использования ключевого слова return. Это начинает иметь смысл, когда мы узнаем, что return используется для управления потоком управления программой. Помните, что функциональные языки, такие как Scala, побуждают вас вместо этого сосредоточиться на потоке данных посредством функциональной композиции. И поскольку каждый оператор возвращает значение, мы фактически можем создать программу, связывая операторы вместе функциональным образом.

Найдите меня в Твиттере и загляните на мой личный сайт.

Первоначально опубликовано на https://www.awelm.com 8 декабря 2021 г.