Polynom Labs

Уютный бложек команды карагандинских программистов

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

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

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