Scala. Полёт нормальный

топ 100 блогов lex_kravetski29.08.2012 Большинство программистов, думаю, прочитав слово «Scala», среагируют в ответ термином «WTF?». Чуть менее значительная часть скажет «что-то такое слышал, но не представляю, зачем это нужно». И лишь совсем немногие смогут похвастаться знанием этого языка. Между последней и предпоследней группой находится группа заинтересованных, но сомневающихся, так что данная статья, увы, совсем для немногих.

Мне посчастливилось около полугода программировать на Scala, и теперь я таки могу авторитетно рассуждать о нюансах девелопмента на данном языке. Самое главное, я могу ответить на наиболее часто возникающий у заинтересовавшихся вопрос «а можно ли вообще на нём девелопить?». Что для прикола поразвлечься понятно можно, но писать-то что-то полезное, оно как?

В общем, авторитетно отвечаю. Можно. Все сомнения о том, что какие-то штуки не будут работать, всё развалится в самый последний момент, возникнут непредвиденные сложности с языком, будет чего-то важного не хватать и так далее, они логичны и обоснованы, однако эксперимент показал, в случае со Scala такого не происходит. За полгода разработки я не встретил ни одной непреодолимой сложности, всё работало, из Java вызывалось и так далее.

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

Собственно, мой старый код с новыми стандартными библиотеками не компилировался, однако я всё исправил довольно быстро — благо, код был написан без копипэйста, поэтому править пришлось мало где.

А без копипэйста он был написан благодаря тому, что написан на Скале. Да, во всех языках предлагают копипэйстить по минимуму, но в большинстве случаев это не удаётся, даже если очень хочешь. В первую очередь потому, что в той же, например, Java (и в C++ тоже) некоторые вещи, будучи обёрнутыми во что-то некопипэйстное, занимают даже больше места, чем с копипэйстом. Когда есть способ написать вместо десяти строк одну, то возникает соблазн написать одну. Если вместо десяти строк «по уму» следует написать восемь или тем более пятнадцать, очень тяжело побороть желание временно скопипэйстить, а в никогда не наступающем «потом» всё исправить на «как надо».

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

Я знаю, что многие видят в сотнях ритуальных, но необходимых строк «серьёзность» и «обстоятельность подхода», я же вижу в этом хреновый дизайн и языка и библиотек к нему. Наиболее частые команды должны быть аналогом кнопки «зафигарить», а сложно и многословно следует выглядеть только грязным хакам.

Сто пудов. Есть, например, концепция настроек приложения. С ними, настройками делают вполне стандартные операции. Ну там, получить настройку, установить настройку, записать в файл, считать из файла. Сколько ни встречаю готовых решений, каждый раз надо вокруг каждого из этих действий плясать с бубном так, будто ты первый землянин на Марсе. Создавать какие-то объекты, приводить типы, создавать потоки, файлы, папки, хрен знает что ещё. Хотя, ядрён батон, я не хочу ничего экстраординарного. Когда захочу, я скажу. А пока, блин, мне пофиг, какой файл и в каком формате, главное, чтобы всё записалось, а потом прочиталось. Одна долбанная команда «прочитатьНастройку(ИмяМоейНастройки)». Пусть оно само разыщет дефолтный файл или создаст новые настройки, если файла нет. Пусть оно приведёт типы. Пусть оно само всё запишет куда надо, если я что-то поменяю в настройках. Или хотя бы, хрен с ним, пусть даже я сам скажу «записать», но без указания всех тонкостей процесса записи.

Кнопки «зафигарить» можно сделать почти на любом языке, но Скала сильно помогает  делать именно такое. С её помощью и реализация и API становятся лаконичными донельзя.

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

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

Так, разумеется, будет не у всех. И вот именно тут заключена проблема: самое трудное в разработке на Scala — убедить других тоже вести разработку на Scala. В Scala визуально очень много нового. Новое требует привычки и преодоления внутреннего барьера, люди это знают, поэтому сопротивляются до последнего. «Ой, нет, потом. Тут надо разбираться, а сейчас некогда, я в будущем когда-нибудь разберусь, но только не сейчас».

Реальный выигрыш будет в основном для тех, кто уже сейчас пишет по сути на Scala, только под Явой. То есть использует похожие концепции для выражения своих мыслей и понимает эти концепции. Тот, кто в некотором смысле сам для себя их «изобрёл». В этом случае он получает инструмент, при помощи которого пользоваться ими на порядок проще, и начинает переться. Те же, кто копипастил, не видят перемен к лучшему — какая разница, где копипастить? Они видят только, что синтаксис другой, а их коллеги повсеместно используют какую-то долбанную магию вуду.

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

Как сделать настройки? О, это очень просто. Сначала создаёшь поток, его оборачиваешь в другой поток, потом создаёшь контекст и файловую систему, выставляешь вот эти тридцать параметров, передаёшь вот это вон туда, вот это — сюда, и вуаля — настройки записаны. Что? Не записалось? А это ты, милый друг, забыл завести конфигурационные файлы для контекста, файловой системы и потоков, а также зарегистрировать их в системе. И конфигурационного файла системы у тебя тоже нет. Сделай, и всё заработает. Только сначала прочитай хотя бы первые пять томов описания библиотеки.

Разумеется что-то подобное можно навертеть в любом языке, но Скала не особо-то стимулирует разработку подобного, поскольку предлагает варианты гораздо проще. Из-за чего пропадает замечательный способ ощущать собственный зашкаливающий профессионализм на ровном месте. «дайМнеНастроки(classOf[РазрешениеЭкрана])», сцуко, выглядит не столь впечатляюще, как полсотни строк с контекстами и системами. И тем более не сравнится с совершенно нечитаемыми файлами xml-конфигураций. Как-то непрофессионально, что ли.

К счастью, Скала устроена так, что на ней в принципе можно писать как на Java. С небольшими поправками и чуть другим синтаксисом, но всё равно можно. Это даёт «профессионалам» отличную возможность навертеть ровно ту же самую хренотень, что они наворачивают в Java, а всем остальным — плавно перейти с одного языка на другой.

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

Что касается фич, их в Скале изрядно. Но есть три киллер-фичи — такие, что будь в языке только они (плюс то, что есть в Java), уже имело бы смысл срочно на него перейти.

Эти фичи — функции высшего порядка, множественное наследование и сравнение по шаблонам.

Само собой, каждую из них имеет смысл изобрести самостоятельно, чтобы оценить всю прелесть и хорошо понять смысл их использования, но я всё-таки скажу про них своими словами.

Функции высшего порядка — это возможность передать в функцию ссылку на другую функцию. То, что в Java обычно делается через определение интерфейса и последующее создание его анонимных экземпляров там, где метод требует в качестве параметра ссылку на объект, расширяющий этот интерфейс. Лаконичность записи в данном случае решает. Анонимный экземпляр в Java выглядит очень криво, как ни крути. А приходится делать именно так, поскольку ссылки на функции ничего не стоят без замыканий — возможности при определении функции «видеть» весь локальный контекст.

def disconnectOld(time: Long) = {
        connectsList = connectsList.filter(_.timeOfLife > time)
}


Пример весьма синтетический, но уже по нему видно, как и что тут делается. «filter» — это функция, которая требует в качестве параметра другую функцию, которая будет для каждого элемента сообщать, следует ли этот элемент оставить или выбросить. Функция определяется прямо на лету при помощи «_.timeOfLife > time». Причём «time» взят прямо из локального контекста — он пришёл к нам в качестве параметра «disconnect». Без замыканий, возможности «на лету» определить функцию и передать ссылку на неё в другую функцию пришлось бы нахреначить целую кучу строк кода. И результат был бы гораздо менее понятным.

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

Вторая киллер-фича — множественное наследование. Оно несколько напоминает одновременно оное в С++ и имплементацию интерфейсов в Java, но имеет отличия, делающие его на порядок более мощным и одновременно удобным. Скажем так, в отличии от С++ множественное наследование всегда виртуальное (то есть, если родительские классы являются детьми одного и того же класса, то в унаследованном от них классе «дедушка» будет в единственном экземпляре, а не в двух, как в С++ по умолчанию). Кроме того, порядок вызова методов через «super» однозначно определён.

Правда, наследоваться можно только от trait, а не от class. Trait в этом плане похож на интерфейс в Java, но может содержать и поля, и методы с телом, что радикально меняет положение вещей. В Java интерфейсы как правило используются для имитации множественного наследования (путём расширения интерфейса и дальнейшего тупого делегирования всех заявленных методов какой-то агрегированной реализации того же интерфейса), для фокусов с анонимными экземплярами и иногда по назначению — для связывания алгоритма с данными.

В Скала же trait обретает свой главный смысл: разбиение кода на отдельные концепции, которые можно многократно использовать в самых разных местах. Собственно, в Java это тоже можно сделать, в чём тогда плюс? Плюс опять же в лаконичности.

Вот, например, означенная система подписки. Положим, у нас есть концепция сигналов. К сигналу можно подписаться, передав в него функцию (см. «первая киллер-фича») и, соответственно, можно его подать. Вроде бы всё просто. Но проблема в том, что в не универсальном варианте выходит примерно то, что мы видим в SWT или других GUI-библиотеках: заведена туева хуча интерфейсов подписчика, а у каждого поставщика отдельный метод подписки для каждого сигнала, который он может подать. Запредельное количество совершенно идентичного кода. При этом всё ни фига не гибко. Нет возможности сложить разные сигналы в контейнер и подать их сразу все. Сигналы не могут вернуть значение в явном виде (только как поле объекта, который они же сами и послали в качестве параметра уведомления). Для подписки нужен анонимный экземпляр и если вдруг выяснится, что случайно подписались не к тому событию, то надо не просто заменить функцию, которая вызывалась для подписывания, на другую, но ещё и создавать экземпляр другого класса, методы в котором скорее всего называются иначе.

Если же подходить более обстоятельно, то сигнал нужен один, универсальный. Но как его сделать? У сигнала более одного «измерения» вариантов. Сигнал может иметь одного подписчика, нескольких или же нескольких, но вызываемых по очереди, пока кто-то из них не ответит «сигнал принят». Он может сигнализировать функцией без параметров, с одним параметром, с двумя, с тремя. Он может ловить ошибки при оповещении подписчиков и пересылать их в логгер, может считать ошибки валидным значением, а может вообще забивать на них, считая, что пусть всё упадёт по эксепшену, если что не так, но до тех пор, зато, работает быстро.

Мы берём, например, варианты с количеством подписчиков и реализуем их как трёх наследников базового класса. Теперь ещё четыре варианта по количеству параметров. 3*4 = 12. Теперь по реакции на эксепшен. 12*3 = 36.

Тридцать шесть классов. Даже если в каждом из них всего лишь делегирование тому объекту, который делает всю реальную работу, это всё равно огромное количество абсолютно бессмысленного, механического кода.

Оттуда и вытекает стандартное: «давайте всегда будет функция с одним аргументом, а реакция на ошибки и куча других вещей будет настраиваться параметрами и реализовываться одним охрененным if-else-блоком». По уму, блин, тридцать шесть классов! Долбануться в атаке! А если ещё одно свойство добавится? Стописят классов будет? Хрен вам. Делаем один магический объект.

А магический объект — это крест на повторном использовании.

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

Но при множественном наследовании ситуация меняется. В этом случае достаточно унаследовать свой объект от нужных вариантов каждого компонента. Это делается в одну строку и не требует вникания во внутреннее устройство родителей. Вуаля. Мы можем собрать тот сигнал, который нам нужен, за пять секунд. Мы можем определить новые его свойства за минуту. И эти свойства не будут намертво прикручены к нашему классу, но будут ещё одним компонентом.

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

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

Третья киллер-фича — сравнивание с шаблоном. Это по сути switch-case, но продвинутый настолько, что может подменить собой чуть не половину кода. Смысл в том, что case-выражение может быть не просто сравнением с константой, но ещё сравниванием или извлечением полей, приведением типов и определением переменных.

// С таким API контроли делать конечно не надо, но это просто пример. 

someColor match {
        case RGB(r, g, b) ⇒ control.setRGB(r, g, b)
        case c: TurboColor ⇒ control.setTurboColor(c) // с имеет тип TurboColor
        case c: Long if (c == 0) ⇒ control.setBlack()
        case c: Long ⇒ control.setNonBlack(c) // с имеет тип Long
        case _ ⇒ println(“Some shit”)
}


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

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

if (someColor instanceof TurboColor) {
        TurboColor tc = (TurboColor)someColor;
        …
}


Три ТурбоКалора, блин, в двух строках. Видимо на всякий случай — а то вдруг непонятно, что тут речь о ТурбоКалоре.

Вдобавок к киллер-фичам есть ещё набор просто фич, многие из которых тоже весьма нехилые.

Выведение типов по контексту при сохранении строгой типизации (то есть, если из тела метода или инициализации переменной можно вычислить тип результата, то его не обязательно указывать в явном виде). Радикально сокращает количество ТурбоКалоров.

Продвинутый for (по сути это явовский for(x : xs), но с кучей бонусов). Не так, чтобы прямо очень полезно, но временами помогает.

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

Не надо писать return. Аналогично предыдущему абзацу. Гораздо понятнее выглядит

def f(x: Int) = if  (x > 0) “Больше нуля” else “Не больше нуля” 


чем то же самое с ретурнами повсюду.

И кстати, да, if-else возвращает значение. И любой блок кода тоже возвращает значение.

val y = {
        val x = 1 + 2 + 3
        x * 5
}


Функцию можно определить внутри другой функции.

Хвостовая рекурсия (Google it, Luke).

Псевдонимы типов. Это позволяет сделать дженерики, у которых тип только для внутреннего пользования. Грубо говоря, вместо

class A {
        List> transpose(List> source) {
                …
        }
}


писать

class A[T] {
        type L = List[List[T]]
        
        def transpose(source: L) = …
}


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

Видимость с учётом пакета. Говорят, будет в Java 100500.

class A {
        private[mypackage] val x = 0 // x виден всем членам mypackage
}


Взаимозаменяемость полей и пропертей.

class A {
        var x = 0

        def y = x

        def y_=(newY: Int) {
                x = newY
        }
}

val a = new A

a.x = 10
a.y = 20 


На самом деле поля вообще все скрыты: «var x» реально заводит скрытое поле и геттер-сеттер для него.

«Ленивые» значения.

class A {
        lazy val x = doSomethingVeryLong // x инициализируется только при первом обращении
}


В комплекте с языком идёт зачипатая библиотека, в которой, в частности, контейнеры имеют туеву хучу функций для всех нужд человечества.

И это я ещё не всё перечислил.

Сложно ли понять новые по отношению к Java конструкции языка? Большинство конструкций — довольно просто. Есть несколько сложных для понимания (без которых, впрочем, можно обойтись), есть несколько нюансов, логика появления которых не совсем понятна, но в целом на мой вкус в среднем со сторонними Java-библиотеками разбираться даже сложнее. Особенно с теми, создатели которых питали страсть к профессиональным ритуалам — вдумчивому конфигурированию в xml всего и вся перед использованием и так далее. В Scala основные конструкции просты и понятны, трудности же с пониманием возникнут в редких, особо замутных местах.

Конечно, чтобы узнать, как всем этим пользоваться, требуется эн времени, и многим будет лень разбираться. С другой стороны многим другим лень копипастить и сто раз писать один и тот же код. Так что тут имеет место быть зависимость от личных предпочтений.

В остальном никаких физически сдерживающих факторов я не обнаружил. Всё пипцато вызывается из Java, собирается Мавеном (хотя SimpleBuildTool наверно лучше), плагин под Idea вполне годный. Под Eclipse давно не смотрел, говорят, теперь тоже уже неплох.

Всякие там сторонние библиотеки без проблем вызываются. Java-рефлексия работает (в некоторых нетривиальных случаях надо выяснять, во что реально превращается та или иная scala-конструкция, но методы остаются методами).

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

В общем, эксперимент можно считать удачным. Я продолжаю писать на Scala.



Оставить комментарий

Архив записей в блогах:
Я не знаю зачем, наверное опять из-за своего неуемного любопытства, потому что иной причины придумать даже для себя не могла, но я зарегистрировалась на "ЛитЭре" и даже выложила там рассказик. Один. Просто посмотреть как оно работает. И даже заказала себе обложку. Двести рублей все удоволь ...
...
интервью ...
(Слева девочка  в шортиках держит змею двумя руками, но я снимала-то овечку и хамелиона, а на стуле ещё лямурчик и  тщедушный и кролик... Девчонка случайно в кадр попала.) Я же червяков боюсь до обморока, а тут... Не помню, как сорвалась с мета (в карьер), но меня, ...
Эта сука открыла свой грязный рот и выдало, мы не можем потерять Казахстан. Вот уж сука из сук. Когда убивали и убивают на Украине это можно, а вот обижать Назарбаева или Токаева это зась. Молчало бы уже своим ...