Параметры “по имени” (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 довольно мощный, но есть существенные недостатки:
-
Сложное понимание, особенно связь между
ByNameиFunc/Func0/Func1. -
Неоднородная “хакерская” хитрость, когда параметр в месте вызова анонимной функции автоматически (т.е. скрыто) превращается в контекст в месте определения.
-
При изучении кода в месте вызова метода невозможно понять, где вызов “по имени”, без просмотра сигнатуры метода.
-
Эмуляция управляющих конструкций усложняет понимание: базовые конструкции известны, а для пользовательских придётся изучать документацию.
-
Усложнение компиляции.
Поэтому не рекомендуется активное использование 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).