Tag Archives: scala

Scala: вызов метода с tuple вместо набора параметров

tl;dr

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

То же самое, но длинно

Потребовался нам в проекте генератор тестовых данных.  Модель данных уже есть, типы и ограничения прописаны, можно спокойно брать и генерить.  Нашлась даже подходящая библиотека с набором методов для генерации примитивов – DataFactory от товарища Andy Gibson’а.

Получилось всё легко и просто, выглядит при этом в таком духе:

field match
{
	case f:Field.NumberField => df.getNumberBetween(0, 1000)
	case f:Field.StringField => genRandomString(100, 200)
	// и так далее
}

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

def limits(min:Int,  max:Int):(Int,  Int) =
{
	field.validators.collectFirst(
	{
		case v:Validator.Length => (v.min, v.max)
		case v:Validator.Range 	=> (v.from, v.to)
	}).getOrElse((min, max))
}

Только вот методы DataFactory принимают параметры по отдельности, и правильно делают.  Не говоря уже о том, что это вообще жабья библиотека, и понятием наскальных туплов она не владеет.

Затруднение с этим тоже не великое: наскальные экстракторы работают замечательно, вполне можно написать:

val (min, max) = limits(0, 10)

Но это же надо будет в каждом кейсе писать… Аж по две переменные два промежуточных значения… Или по одному, но с уродливыми ._1 и ._2. Неэстетично-с…

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

Процесс преобразования метода в функцию в скале принято называть звучным термином из лямбда-исчисления «eta expansion».  По сути это означает, что компилятор сгенерирует вложенный класс-обёртку, унаследованный от FunctionN (в нашем случае Function2), который в методе apply вызывает нужный метод.

Синтаксически это оформляется подчёркиванием после имени метода.  Кстати, это совпадает с синтаксисом частичного применения функций (curring), что есть весьма консистентно.

С использованием вышеперечисленного, вызов – тема этого поста выглядит следующим образом:

(df.getNumberBetween _).tupled(limits(0, 1000))

Можно пойти ещё дальше и часть этого кода обобщить.  Например, в виде такого метода:

def limited[A](f:(Int,  Int) => A)(min:Int,  max:Int):A =
	f.tupled(limits(min, max))

Более того, в случае передачи метода в качестве параметра типа «функция», компилятор способен произвести eta expansion самостоятельно безо всяких подчёркиваний.  Конечный результат может выглядеть так:

field match
{
	case f:Field.NumberField => limited(df.getNumberBetween)(0, 1000)
	case f:Field.StringField => limited(genRandomString)(100, 200)
}

Стоило оно того или нет – это вопрос, но знать в любом случае полезно.

Scala REPL, IntelliJ IDEA, Console2

Update: с момента написания этого текста идейская наскальная консоль стала намного лучше, и для репла стоит пользоваться ей.  Использовать Console2 теперь актуальнее для запуска sbt.  Потому что при перегенерации идейского проекта плагином sbt-idea проект перегружается, и все открытые в идее консоли, включая sbt’шную, закрываются.  Так что гораздо удобнее запускать sbt во внешней консоли.  Настраивается так же, только надо ещё текущим каталогом указать $ProjectFileDir$.


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

Правда, несколько мелких препятствий всё-таки имеется.

Первое: конторские машины у нас под виндами, а родная виндовая консоль тоже убога до невозможности. Глубоко убеждён, что отказывать пользователю даже в растягивании окошка в ширину мышкой в наше время просто неприлично. Не говоря уже про прочие радости, вроде строго блочного выделения текста. Хорошо, что существуют альтернативные консоли, в частности Console2, которой мы и воспользуемся. К слову, в интернетах попадается ещё такая штука, как ScalaConsole с гуём на свинге, но, несмотря на название, это не консоль с командной строкой и историей, а редактор для наскальных скриптов с возможностью их запуска, каковой в идее реализован гораздо лучше.

Второе: привычный jar-hell вполне распространяется и на scala interpreter. Вольное обращение с автоматический подгрузкой зависимостей каким-нибудь мавеном с лёгкостью приводит classpath в состояние полной непотребности. Померил в одном из наших проектов: длинный список jar’ок, каждая с полным путём по пакетам в локальном мавеновском репозитории, в общей сложности на 8 килобайт. Приложенная к экрану линейка и нехитрая арифметика показывают, что, будучи записанным в строчку скромным 12 кеглем, этот classpath протянется на абсурдные 15-20 метров. Очевидно, что ручное редактирование командной строки длиной в 20 метров – занятие весьма и весьма некомфортное. А поскольку изменение зависимостей в активно разрабатывающемся проекте – действие регулярное, то и classpath реплу придётся постоянно обновлять. Бороться с этим будем получая его у самой идеи, через макроподстановку в параметрах внешнего инструмента.

Третье: помимо зависимостей от библиотек, у проекта ещё, как минимум, есть зависимость от конкретных версий JDK и самой скалы. Вынужден признать: я так и не нашёл приемлемого способа получения от идеи каталога lib выбранного в проекте компилятора скалы. Более того, учитывая то, что подключается наскальный sdk в виде глобальной библиотеки, в которой некоторых репловских jar’ок нет, подозреваю, что и сами они их для своей запускалки получают как-нибудь затейливо, а может и просто без них обходятся. Впрочем, версии жабы и скалы обновляются значительно реже, необходимости разрабатывать сразу под несколько версий у нас, слава Богу, пока нет, поэтому счёл приемлемым вариант ручного изменения путей в окружении или настройках console2.

В конечном итоге всё сводится к следующей последовательности действий:

  1. поставить console2
  2. настроить в ней scala interpreter одним из шеллов
  3. подключить это в идею в качестве external tool.

Console2 нужной редакции качается с родного соурсфорджа, последняя на момент написания версия 2.00 beta 147, вполне работоспособна. Настраивается по вкусу. Отдельно стоит упомянуть о горячих клавишах, которых она по умолчанию себе хочет штук тридцать. Из них стоит отключить или переназначить хотя бы те, которые конфликтуют с наскальным реплом. А можно и все сразу, потому как их функции в массе своей ни разу не актуальны, и в любом случае доступны из меню. Вообще, чем меньше консоль позволяет себе самодеятельности, тем лучше.


В разделе настроек с неинтуитивным названием Tabs нужно добавить элемент, указать имя вкладки (оно потом понадобится при вызове),  и прописать ему в поле Shell вызов нужного scala interpreter с нужным JDK.

 

 

У меня это выглядит так:

C:\Tools\Java\jdk1.6.0_24\bin\java -Dfile.encoding=UTF-8 -classpath
"C:\Tools\Scala\scala-2.9.0.1\lib\scala-compiler.jar;
C:\Tools\Scala\scala-2.9.0.1\lib\scala-library.jar;
C:\Tools\Scala\scala-2.9.0.1\lib\jline.jar;"
scala.tools.nsc.MainGenericRunner

В настройках идеи нас интересует кнопка «Add…» в разделе External Tools.


В этом окне надо поснимать лишние галочки, прописать имя инструмента, указать экзешник console2 и такие параметры:

-t scala -r "-classpath \"$Classpath$\""

Ключом -t передаётся имя консольского tab с нашим шеллом.


В результате всех этих действий, в меню Tools должен появиться пункт Scala Console,запускающий в Console2 Scala REPL с classpath теущего проекта.