Блоки кода и передача параметров

Изначально я написал эту короткую заметку как напоминание о такой конструкции в Scala как блоки кода. По мере моего знакомства с языком, я добавлял сюда другие связанные с ними конструкции и нюансы языка. В итоге самым интересным в заметке стала вторая половина, где идёт речь о ленивых вычислениях, но т.к. это всё нарастало на описание блоков кода, то по историческим соображениям статья до сих пор так и называется. Надо будет вынести всё в другие заметки.

При знакомстве с синтаксисом Scala, может создаться впечатление, блок кода (код внутри фигурных скобок) это просто анонимная функция без параметров, или Function0. Это совсем не так.

Прежде чем подойти к детальному разбору блоков кода, необходимо отметить, что это далеко не единственное применение фигурных скобок в Scala. Иногда фигурные скобки могут использоваться при обычном вызове функции, когда функция принимает только один аргумент. Как объясняет Мартин Одерски в книге "Programming in Scala", это нужно для того чтобы программист мог создавать из функций свои контрольные структуры, которыми Scala не особо перегружена.

Для демонстрации различий блоков кода и функций подойдёт следующий листинг:

def f(b: Int) = {
  println("Внутри функции")
  b + 1
  b + 2
}

f({ println("Блок вызван"); 1+1 })

Итак, наш блок представляет собой всего лишь целочисленное значение (о чём говорит сигнатура принимающей функции), но при этом ещё и выводится сообщение на экран. И что очень важно, это сообщение выводится только единожды, во время передачи в функцию.

На этом месте стоит отвлечься от блоков кода и вспомнить о передаче параметров в функцию. Тут мы имеем дело с привычной нам по большинству языков программирования стратегией исполнения называемой передачей переметра по значению. Обычно, говоря о передаче параметра, сравнивают две наиболее часто встречающиеся стратегии: передача по значению и передача по ссылке. В мире Scala передача по ссылке не актуальна, зато имеется так называемая передача параметра по имени. Я пока не знаю почему это так называется, но ни с каким именем я тут связи не вижу.

Гораздо более понятным, хотя и более общим (не тождественным) понятием является нестрогая модель вычислений. Одерски начинал свой курс лекций по функциональному программированию с объяснения разницы между привычным pass-by-value и непривычным pass-by-name.

Итак, блок кода из первого листинга превращается в целочисленное значение, потому что в выражении важна последняя строка, которая в нашем случае равна 2. И выполняется это выражение лишь один раз. Т.е. делает всё что ему положено (побочные варажения вроде вывода сообщений на экран) и превращается в целочисленное значение.

Непонимание того, что важно именно последнее выражение часто приводит к ошибке среди начинающих в передаче анонимной функции:

var i = 0
List(1, 2, 3). map { i += 1; _ + 1 }

Возможно, программист хочет этим куском кода увеличивать i с каждым элементом массива. Но после выполнения, итератор будет равен 1. Почему так происходит? Потому что значение блока кода будет равно анонимной функции x => x + 1, с сигнатурой Int => Int ведь именно она является последним выражением, а увеличение i является всего лишь побочным выражением, таким же как вызов println в предыдущем примере, поэтому вызывается лишь единожды.

Для работы с побочными выражениями, а точнее, для работы с анонимными функциями с более чем одним выражением, я бы не ленился объявить нормальную анонимную функцию, без плэйсхолдера:

var i = 0
List(1, 2, 3).map { x => i += 1; x + 1 }

Снова возвратимся к передаче параметра в функцию. Для демонстрации передачи параметра по имени, чуть-чуть изменим первый пример (вся соль в первой строке):

def f(b: => Int) = {
  println("Внутри функции")
  b + 1
  b + 2
}

f({ println("Блок вызван"); 1+1 })

Как видим, результаты значительно изменились. Во-первых, теперь первым делом выводится Внутри функции, и во-вторых, Блок вызван теперь выводится дважды. Как несложно догадаться, наш блок кода теперь выполняется не в месте передачи функции, а в месте непосредственного обращения. Таким образом, если значение блока не понадобится нам внутри функции (например, если мы используем условия), то код просто не выполнится. Нечто отдалённо напоминающее ленивые вычисления. С другой стороны, как уже было замечено, если значение встречается несколько раз, то и выполняется оно тоже целиком несколько раз, так что, использовать это необходимо с осторожностью.

def f(x: => Int) = x * x
var y = 0
f { y += 1; y }

В этом примере y будет равен двум потому что x вызывается два раза, и каждый раз увеличивает y на один. Возвращаемой функцией значение тоже будет равно двум, т.к. первый раз x был равен одному, второй раз двум.

Это упрощение передачи функции без параметров (def f(b: () => Int)). Оба варианта транслируются в Function0, но используя функцию без параметров нам необходимо будет при вызове функции так же явно указывать что мы передаём функцию: f(() => 3). И точно не стоит это путать с передачей функции принимающей один параметр Unit. И чтобы окончательно вынести мозг всеми этими "нюансами", стоит отметить что мы не можем использовать передачу по имени в кейс-классах, т.к. они неявно преобразуют параметры в постоянные переменные (val) и в этом случае нам необходимо использовать вышеупомянутый синтаксис с передачей функции без параметров (def f(b: () => Int)), ну или переопределить метод apply в объекте-компаньоне нашего кейс-класса. Всё сложно.

Для закрепления разницы между этими двумя стратегиями выполнения, можно добавить следующий код:

def callByValue(rnd: Int) { for(i <- 0 until 3) println(rnd) }
def callByName(rnd: => Int) { for(i <- 0 until 3) println(rnd) }

callByValue(new scala.util.Random().nextInt())
callByName(new scala.util.Random().nextInt())

Таким образом, блоки кода чаще всего (по моим наблюдениям) используются вместе с передачей параметра по имени для ленивых вычислений. И если уж речь пошла о ленивых вычислениях, то в Scala имеется следующий идиоматический подход:

def functionWithLazyVal(veryExpensiveComputation: => Int) = {
  lazy val veryExpensiveValue = veryExpensiveComputation
  ???
}

В вышеприведённом примере, объединяется передача параметра по имени (откладывание исполнения функции) и ключевое слово lazy, обязывающее присвоить значение только один раз и только по необходимости или иными словами закэшировать это значение. Если бы в объявлении была использована привычная передача по значению, то выражение veryExpensiveComputation исполнилось бы при вызове functionWithLazyVal, что вероятно было бы лишним в некоторых случаях.