Tag Archives: штаны на лямках

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)
}

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