Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Параметры “по имени” (ByName)

Предупреждение

Возможно, в будущем этот механизм будет отключен.

Передача “по имени”, трейт ByName

При обычном исполнении все аргументы метода вычисляются перед вызовом метода. Но иногда нужно какой-то аргумент метода не выполнять при передаче, а обернуть в некоторую сущность (т.н. thunk), которую выполнить только в нужный момент уже внутри метода. Можно назвать это передачей “по имени”.

Обычный вызов:

def work(a: Int, b: Int) = {
  println("work")
  println(a + b)
}

def main = {
  work(123.println($), 777.println($))
}

Выдаёт:

123
777
work
900

Здесь 123 и 777 выводятся в консоль до work. Это передача значения “по ссылке” или “по значению” (для неизменяемых объектов принципиальной разницы нет).

Если определить тип параметра как Func/FuncX, то аргументом можно передавать анонимную функцию, созданную при помощи #-выражения (опционально без func, если определение по месту вызова). Это является аналогом передачи “по имени”, но несколько громоздко.

def work(a: Func[Int], b: Func[Int]) = {
  println("work")
  println(a.! + b.!)
}

def main = {
  work(#{123.println($)}, #{777.println($)})
}

Выдаёт:

work
123
777
900

Здесь же 123 и 777 выводятся в консоль после work.

Чтобы упростить вызов, можно вместо Func в параметрах метода использовать ByName (тоже из пакета libretto):

def work(a: ByName[Int], b: ByName[Int]) = {
  println("work")
  println(a.! + b.!)
}

def main = {
  work(123.println($), 777.println($))
}

Выдаёт:

work
123
777
900

Это полный аналог предыдущего примера, но вызов work проще, поскольку не нужно использовать # для оборачивания аргументов. Компилятор делает это сам, если видит использование ByName в параметре метода.

ByName[Int] можно использовать только в параметре метода, тогда это аналог Func[Int] + маркер для компилятора, что аргумент нужно автоматически обернуть в анонимную функцию.

На уровне метода ByName превращается в обычный Func/Func0, который и нужно дальше использовать (если есть необходимость явно указать тип):

def work(a: ByName[Int]) = {
  println(type(a))
  fix f: Func[Int] = a // если нужно явно указать тип
  println(a.!) // одно и то же 
  println(f.!) // одно и то же
}

def main = {
  work(123) // 123 автоматические оборачивается #{ 123 }
}

Выдаёт:

libretto/Func0[libretto/Int]
123
123

Для дальнейшей передачи нужно использовать Func/Func0 (чтобы избежать дополнительного оборачивания)

def workFunc(f: Func[Int]) = {
  println(f.!)
}

def work(a: ByName[Int]) = {
  workFunc(a)
}

def main = {
  work(123)
}

Выдаёт:

123

Более сложный случай, если передаваемый аргумент не просто возвращает значение, а его нужно выполнить в каком-то контексте. Для этого используется ByName с двумя параметрическими типами. Первый обозначает тип контекста для аргумента (будет подставлен автоматически), второй - это (как всегда) тип результата:

def workFunc(f: Func[Int, String]) = {
  println(f.!(123))
}

def workByName(f: ByName[Int, String]) = {
  println(f.!(123))
}

def main = {
  workFunc(#(v: Int){v.{ "=" + ($ + 777).string }})
  workByName("=" + ($ + 777).string)
}

Выдаёт

=900
=900

Вызов workByName компилятором превращается в вызов, аналогичный workFunc. Т.е. аргумент оборачивается в Func/Func1, а единственный параметр автоматически подаётся как $-контекст аргументу.

Соответственно, ByName[Int, String] виден как Func[Int, String] в методе. И то, что будет контекстом для аргумента, передаётся явным единственным параметром. Как и в случае более простого ByName (с одним параметрическим типом), этот ByName (с двумя параметрическими типами) является только маркером компилятору, который должен автоматически обернуть аргумент в анонимную функцию. Дальше нужно работать только с типами Func/Func1.

ByName удобен для создания методов, работа с которыми выглядит как управляющая конструкция языка.

def from0to9(body: ByName[Int, Int]): Int = {
  var sum = 0
  0..9 as v.${
    sum = sum + body.!(v)
  }
  sum
}

def main = {
  println: from0to9: {
    $ * 100 // $ типа Int
  }
}

Выдаёт:

4500

Серьёзные недостатки ByName

Хотя ByName довольно мощный, но есть существенные недостатки:

  1. Сложное понимание, особенно связь между ByName и Func/Func0/Func1.

  2. Неоднородная “хакерская” хитрость, когда параметр в месте вызова анонимной функции автоматически (т.е. скрыто) превращается в контекст в месте определения.

  3. При изучении кода в месте вызова метода невозможно понять, где вызов “по имени”, без просмотра сигнатуры метода.

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

  5. Усложнение компиляции.

Поэтому не рекомендуется активное использование ByName. Пока решения нет, но, возможно, в будущем ByName и вовсе будет убран из языка.

Альтернативой является использование обычных Func/Func0/Func1 с определением аргумента при помощи #-выражения без использования func и без указания типа (по желанию).

Например:

def nTimes(n: Int, body: Func[Any*]) = {
  1..n.${
    body.!
  }
}

def main = {    
  nTimes(3): #{
    println("Hello")
  }
}

Выдаёт:

Hello
Hello
Hello

При просмотре исходного кода сразу понятно, что второй аргумент метода nTimes вызывается лениво (“по имени”), даже без просмотра сигнатуры самого метода.

Аналогично с передачей параметра.

trait Helper {
  def `!=`(key: Int, value: String): ()
}

def init(body: Func[Helper, ()]): Map[Int, String] = {
  fix m = map[Int, String]()
  fix helper = new Helper {
    def `!=`(key, value) = m.!(key) = value
  }
  body.!(helper)
  m
}

def main = {    
  fix m = init: #(helper) {
    helper.!(1) = "one"
    helper.!(2) = "two"
  }
  println(m.keys)
}

Выдаёт:

(1,2)

Такой вариант пусть и слегка более громоздкий, но более понятный: сразу виден ленивый (“по имени”) код, есть именование параметра, которое более понятно, чем использование $. При желании можно даже протипизировать этот параметр (helper: Helper).