Риски первого проекта на scala и способы их минимизации
Ниже — конспект моего доклада на первой встрече группы пользователей scala в Украине.
Вторая встреча состоится 14 января dou.ua/calendar/1181
Добрый день уважаемые господа. Перед тем как перейти к основной части повествования, несколько общих замечаний: этот рассказ будет о сложностях при разработке на scala и я боюсь, что он может быть воспринят критиками как еще один YASTCA «yet another scala too complex article». Это не так — любой язык предоставляет нам какой-то баланс возможностей и ограничений, а представление о том, что вот есть какая-то одна идеальная технология мне кажется наивным, это такая юношеская болезнь технологического фетишизма. И вот с одной стороны, я этим переболел уже давно, а с другой стороны — для меня scala неожиданно стала подобна серебряной пуле, позволяющей моей фирме с малыми силами конкурировать с большими организациями. Поэтому слушая этот рассказ о недостатках scala, не забывайте что для меня это не только набор проблем, но и серебрянная пуля ;)
Макроуровень:
Срединный путь в отсутствие колеи
Одним из основных новшеств в чань буддизме, по сравнению с традиционным индийском, стало понятие о срединном пути: если раньше человек стоял перед выбором: либо приземленная мирская жизнь, либо поиск просветления в монастыре, то срединный путь открывал возможность совмещения обыденного и духовного смыслов. Этот путь очень привлекателен, но открыт немногим. Почему — отсуствие ориентиров.
Точно так же, с появлением scala, программиствам в поисках просветления не обязательно уходить в haskell, а можно делать нечто полезное для общества [троллинг, не принимайте всерьез ;)]. Казалось бы прекрасно, но — у нас нет колеи.
Скала предоставляет одновременно много способов написания одного и того же кода, и если один человек еще как-то может нащупать путь, который представляется оптимальным, то в группе из нескольких человек априорные представления о лучшем выборе с неизбежностью будут у разных людей разными.
При этом стандартные соглашения о стиле кодирования действительно описывают только стиль и оставляют открытыми все семантические вопросы, среди которых:
- Способы обработки ошибок — что мы предпочитаем делать — генерировать исключения или использовать Either (?)
- Предпочтительные наборы коллекций — по умолчанию мы используем mutable или immutable варианты. (?)
- Способы задания поведения. В scala их три (функции, trait-s использующиеся как java интерфейсы, структурные типы)
- Спектр используемых инструментов — чего мы боимся, а чего — нет (?) (@cps, scalaz, views)
Поэтому в scala проекте о многих вещах нужно договариваться либо заранее, либо сразу после того, как возникли первые противоречия (второе мне нравится больше). Однако эти противоречия надо заметить.
Соответственно, я бы рекомендовал при старте нового проекта просто делать принудительное перекрестное ревью кода раз в несколько дней, отмечать противоречия, как-то их разрешать и записывать.
Инфраструктура vs Функциональность
Как мы знаем, любой проект состоит из слоя инфраструктуры и слоя собственно-обеспечивающего функциональность. Особенность scala состоит в легкости первоначальной реализации сложных вещей: написать и начать пользоваться каким-то DSL довольно просто, однако развивать его и сопровождать — гораздо сложнее.
Поэтому имеет смысл делать свою реализацию Dimensions или общей XML сериализации только если это действительно необходимо.
Необычные кривые обучения
Еще одна «общая» неожиданность, которая может подстерегать проект — это нетривиальная кривая обучения. Мы привыкли к тому, что стандартная кривая обучения выглядит следующим образом:
Однако в scala есть очень много мест, где кривая обучения выглядит по-другому. Например так:
Пример — xml. Использовать XML литералы можно сразу, а вот что-бы сделать что-то сложное, необходимо изучать дизайн библиотеки, который далеко не самый интуитивный.
А кривая обучения некоторых пакетов (тот же lift) — вообще напоминает кривую Дирихле.
Ванная без ребенка
И последнее замечание «макроуровня» — если уж мы пошли на использование scala, то не использовать существующие высокоуровневые библиотеки доступа к данным или сериализации из-за их сложности — это тоже большая ошибка, так как продуктивность разработчика, использующего DSL со статической типизацией заметно выше, чем продуктивность разработчика отлаживающего генерацию строк запросов.
Микроуровень
Обработка ошибок: Either vs Exception.
Как мы знаем, в scala одновременно существует и идиома исключительных ситуаций, точно так же как она реализована и в других языках (то есть throw Exception) и идиома возврата специального типа Ether со следующим упрощенным определением:
*PLACEHOLDERS_PRE_2*
Можно ли выработать какие-то универсальные правила, позволяющие сказать — когда обработку ошибок надо реализовывать с помощью Either, а когда — с помощью исключения?
Люди с бекграундом Haskell часто предпочитают вариант с Either, однако, на практике:
если мы взаимодействуем с java кодом, то использование Either не спасает нас от обработки исключений. В отличие от haskell, где наиболее распротраненный путь создания сложного из простого — это композиция функций, в scala часто используется просто последовательная запись. И из-за возможности обработки ошибок записывать (f; g; v) как (f.right map g).right map v — скорее усложнение чем упрощение. Вообще, языки с императивной семантикой нам бесптлатно дают монаду, которая называется «выполнение кода». Имеет ли смысл поверх этого делать еще один уровень интерпретации — вопрос неоднозначный. В моей практике — почти никогда.
Поэтому я бы рекомендовал следующую политику:
- Стандартная обработка ошибок в scala — это в первую очередь исключительные ситуации (unchecked exception)
- Either используется в тех случаях, когда нам надо обязательно надо обработать результат неудачной операции, то есть именно те случаи, когда в java мы бы использовали checked exceptions.
В случаях, когда мы работаем в транзакционном окружении есть формальный критерий различения Either/Exception: при обработке прерывания мы, как правило, откатываем транзакцию, при обработке Either — нет.
Еще одна проблема Either — отсутствие базовой левой части в стандартной библиотеке. То есть хотелось бы видеть нечто вроде:*PLACEHOLDERS_PRE_3*
в стандартной библиотеке.
Границы сред и ленивые вычисления
Еще одна часто встречаемая проблема, для которой в scala пока нет универсального приемлемого решения — контроль границы среды. К примеру, посмотрим на следующий фрагмент кода:
*PLACEHOLDERS_PRE_4*
Где тут ошибка ?
Ошибка возникает, если мы вызовем этот код вне контекста транзакции, так как запрос нам вернет объект типа Query[Customer] который «лениво» преобразуется в Seq[Customer] который так=же лениво преобразуется в java.util.List[Customer].
И итерация данных собственно начнется тогда, когда мы начнем чтение списка в java-коде. К тому времени транзакция уже завершиться, соответственно попытка чтения вызовет исключение
Можно ли предотвратить существование таких ошибок:
- четко представлять себе возможные границы сред
- на уровне библиотеки вполне можно написать классы-адаптеры, которые копируют данные в память в момент завершения транзакции.
Вообще, идея для упрощения использование держать что-то в ThreadLocal и надеяться что этот контекст будет доступен всегда, она и для java-ы довольно рискованная, а для scala, где есть развитые методы нестандартной передачи управления, это вообще становится подводной миной, делая все операции передачи замыкания в неизвестный контекст потенциально опасными. Было бы логично рекомендовать авторам библиотек прямо передавать контекст (возможно с помощью неявных параметров) везде, где только возможно.
Неявное преобразование к типу общего назначения
Implicit чрезвычайно похоже на неявное преобразование типов в С++ — очень мощное средство, с помощью которого чрезвычайно легко сделать что-то не то. К примеру -отключить в языке статическую типизацию:
*PLACEHOLDERS_PRE_5*
В моем понимании, использование implicit оправданно в cледующих случаях:
- если мы с его помощью скрываем излишнюю сложность, позволяя параметризировать стандартное поведение в сложных случаях или внутри библиотеки
если мы используем implicit для эмуляции extensions methods — то есть создаем специальный класс, в котором определяем дополнительные методы
если мы используем implicit как адаптор к какой-то бибилиотеки и импортируем эти преобразования только в своем слое взаимодействия
- когда на выходе «implicit» у нас класс общего назначения
Заключение
Надеюсь, что перечисленные здесь вещи, на которые стоит обратить внимание сделают процесс вхождения в scala более легким и предсказуемым, а код — простым и понятным. В целом, мне ситуация со scala очень напоминает С++ в
Маєте важливу новину про українське ІТ? Розкажіть спільноті. Це анонімно.І підписуйтеся на Telegram-канал редакції DOU
67 коментарів
Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.