Программирование на Scala - путешествие скептика

Как и многие разработчики, я хорошо разбираюсь в C / C ++ и Java. Я написал много работающего программного обеспечения, и подавляющее большинство из них было императивным или объектно-ориентированным. Конечно, мне нравились лямбды, и я заметил разницу, которую сделало неизменным для моего многопоточного кода. Но для меня функциональное программирование казалось чем-то для эгоцентричных пуристов, более озабоченных красотой своего кода, а не чем-то, что мне нужно для выполнения моей работы.

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

Хотя Scala может многое предложить, о чем я не буду рассказывать (например, классы типов и конструкции функционального программирования для управления побочными эффектами), я думаю, что полезно рассмотреть, какие вещи наиболее доступны разработчикам, пришедшим из Java / Экосистема C ++.

Куда делись все ошибки?

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

Решив, что это не так, я сел и подумал. Я понял, что многие из ошибок, которые я больше не видел, обычно были результатом простых ошибок программирования:

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

В производственной системе Java, над которой я работал совсем недавно, на них приходилось около 50% ошибок в нашей кодовой базе (число, которое я только что придумал, но кажется примерно правильным). Значительное количество наших проблем было не ошибками бизнес-логики, а простыми ошибками программирования, которых можно было избежать. Неужели Scala лучше справляется с некоторыми из этих вещей?

Больше никаких NPE!

Одно исследование 712 программных проектов с открытым исходным кодом (Анализ частых изменений кода исправления ошибок) показало, что около 5% исправлений ошибок были связаны с проблемами с отсутствующими нулевыми проверками. Более того, это самая частая повторяющаяся ошибка, которая встречается почти в 50% исследованных ими проектов. В моем Java-проекте у нас было как минимум такое же количество похожих ошибок, несмотря на использование инструментов статического анализа и аннотации кода с помощью аннотаций @Nonnull и @Nullable и в целом хорошее покрытие модульным тестом. . Слишком легко облажаться, и слишком много унаследованного кода и библиотек небезопасно иметь дело с нулевой безопасностью.

Более того, очень большая часть вашего кода в конечном итоге записывает проверки на null. Исследование (Отслеживание нулевых проверок в системах Java с открытым исходным кодом) показало, что примерно 35% условных выражений в коде, который они исследовали, содержали нулевые проверки.

Этой ошибки нет в идиоматической Scala (или не существует в первом приближении). Вы не разрешаете создание экземпляров нулевых экземпляров ваших типов и вместо этого используете тип Option. По абсолютно подавляющему соглашению в Scala, если это не Option, то он никогда не будет нулевым. Эта, казалось бы, небольшая разница невероятно раскрепощает. Недавно мне пришлось просмотреть некоторый Java-код, и я потратил чрезмерное количество времени, отслеживая несколько уровней API, проверяя нулевую безопасность различных функций и параметров. Если вы подумываете о переходе с Java или C ++ на Scala, делайте это только по той причине, что никогда не нужно снова писать if (x == null) в своем коде.

Тип Безопасность

Как только вы познакомитесь с преимуществами безопасности типов с помощью таких функций, как Option, вы быстро попадете на крючок. Когда язык поддерживает статическую типизацию таким образом, что пользоваться ею приятно, вы понимаете, что это не обременение для вас, а набор тестов вашего кода, выполняемых компилятором, а не модульные тесты, которые вы должны писать. Хорошим примером этого является использование типобезопасных конструкторов, чтобы гарантировать, что вы можете создавать экземпляры типов, которые гарантированно имеют определенные инварианты. Подобно тому, как тип Option позволяет нам применять инвариант, что любой экземпляр, который мы получаем в программе Scala, никогда не может быть нулевым, так же могут применяться другие типы инвариантов. В Java мы бы сделали это, создав частный конструктор и фабричный метод, который генерировал бы исключение в случае ошибки. Однако в Scala мы предпочли бы использовать Option или Either (что позволяет использовать ветвь Left для сообщения об ошибках и Right для сообщения об успешном выполнении), чтобы обернуть результирующий тип и заставить вызывающего абонента обработать его явно.

На первый взгляд это кажется громоздким. Но когда вы осознаете мощь других механизмов, которые предоставляет Scala, например для понимания для объединения этих проверок вместе, или вы узнаете о типе Applicative Validated из библиотеки cats, что позволяет свернуть несколько значений, заключенных в одно объединенное допустимое (или недопустимое) значение; он меняет ваш подход к построению программ. Внезапно, когда вы видите переменную этого типа внутри вашей программы, вам не нужно задумываться (или писать код для проверки), действительна она или нет, поскольку вы уже гарантировали, что это благодаря конструктору безопасного типа. .

Для более легких типов, таких как String, Scala предоставляет простой способ упаковки, который (в основном) бесплатный во время выполнения.

Для компилятора это будет выглядеть как строка (если мы не делаем определенные вещи, требующие отражения во время выполнения).

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

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

Если вы занимаетесь какой-либо формой многопоточного или параллельного программирования, вы быстро узнаете преимущества неизменяемых структур данных. Они предотвращают возникновение состояний гонки, поскольку изменения в общем состоянии больше невозможны. В Java мы можем пометить переменные как окончательные, но мы должны предпринять осторожные шаги, чтобы убедиться, что экземпляры, к которым эти переменные привязаны, также являются неизменяемыми (например, вручную скопируйте значения при возврате из геттеров). В Scala у нас есть case class общего назначения, который предоставляет нам это для значений данных. Кроме того, в Scala есть ряд классов коллекций, которые обеспечивают эффективную реализацию неизменяемых структур данных, таких как списки, множества, векторы и хеш-таблицы.

Обработка исключений

Если вы обертываете библиотеки Java в Scala, иногда вам все равно придется иметь дело с исключениями. Scala предоставляет тип Try, который позволяет преобразовать исключение в тип, с которым мы можем более эффективно работать:

На первый взгляд, это не дает нам многого по сравнению с подходом try / catch, с которым мы знакомы по Java. Но Попробовать также позволяет нам исправлять наши ошибки или преобразовывать их в другие типы, чтобы упростить управление. Например, если мы превратим все наши результаты в Either с помощью метода расширения cats toEither, мы можем использовать «для понимания», чтобы собрать все данные и запустить только результат. если все элементы выполнены успешно:

Мы помним, что наш метод применения EmailAddress вернул Either [Exception, EmailAddress]. Из-за способа определения Either мы продолжим оценку следующей строки, только если это будет успешным и метод вернет Right (EmailAddress). Кроме того, мы отправим электронное письмо, только если оба значения действительны. Больше никаких вложенных операторов if, чтобы облажаться.

Было ли это все Сладость и Свет?

Что ж, нет, у Scala есть свои неровности. У него нет достойной поддержки для типов объединения или перечислений. Тип Перечисление в значительной степени сломан. В конечном итоге вам нужно использовать запечатанный трейт, который не ужасен, но тяжел по шаблону. Библиотека enumeratum предоставляет несколько полезных макросов, которые могут вам помочь.

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

Поддержка IDE довольно раздражает. Вы получаете множество ситуаций, когда ваш код имеет волнистые красные линии в IntelliJ, и вам нужно войти в SBT, чтобы увидеть, нужно ли его компилировать в любом случае - это в основном связано с поддержкой макросов Scala в библиотеках, которые должны быть явно добавлены в плагин. Рефакторинг есть, но он ограничен тем, что вы получаете по сравнению с Java (по общему признанию, вы очень испорчены, если используете IntelliJ для рефакторинга Java-кода).

Вывод типа иногда затрудняет чтение кода. Часто вы смотрите на «для понимания» и не знаете, каковы типы отдельных компонентов. IntelliJ действительно предоставляет полезную инспекцию, которая расскажет вам о любом предполагаемом типе, который в основном работает, но в целом я нахожу это болезненной попыткой прочитать код, который я не писал.

Иногда вы обнаруживаете, что боретесь с компилятором больше, чем хотели бы. Если вы не определились, какой именно тип или неявное выражение вам нужно, может потребоваться время, чтобы распутать его. «Для понимания», как правило, сбивают вас с толку до тех пор, пока вы не овладеете им. Хорошая новость в том, что после компиляции он почти всегда работает так, как вы ожидали.

И что вы думаете о возвращении к Java / C ++ сейчас?

Раньше я видел разговоры о функциональном программировании и безопасности типов, где люди жаловались, что они не могут использовать ‹вставить сюда императивный язык›, потому что это не давало всех гарантий, с которыми они были знакомы. Я всегда ненавидел такое отношение, потому что всегда нужно стремиться уметь хорошо решать проблемы на любом языке. Однако я должен признаться, что недавно копаясь в какой-то библиотеке Java и пытаясь заставить ее работать, не имея тех же гарантий безопасности во время компиляции, к которым я привык, я сдался и переписал ее на Scala.

В разработке программного обеспечения я всегда верил, что скорость зависит от уверенности. Когда вы пишете одну строку кода, обычно легко убедить себя в правильности этого кода, и вы можете почувствовать себя более продуктивным, если напишете эту одну строку, не утруждая себя написанием тестов. Но по мере роста вашей кодовой базы вас всегда беспокоит, может ли ваше изменение нарушить какой-либо другой фрагмент кода. Если у вас есть хорошее покрытие тестами, вы можете запустить тесты, чтобы быть уверенным, и не задумываться над этим. Если вы этого не сделаете, вы в конечном итоге будете смотреть на кучу кода, пытаясь вспомнить, как он работает, прежде чем решить, сломали вы его или нет.

Языковые особенности Scala, такие как безопасность типов и неизменность, расширяют мою зону комфорта, давая мне уверенность в том, что мой код будет работать. Лучшее, что мне нужно сделать, чтобы получить это дополнительное преимущество, - это скомпилировать его!

Короче говоря, это не просто лучшая Java (хотя вы можете использовать ее таким образом) - если вы примете ее, это вообще лучший способ написания кода.

Первоначально опубликовано на www.skedulo.com 6 сентября 2018 г.