Поднимайте If вверх, опускайте For вниз
- воскресенье, 25 мая 2025 г. в 00:00:02
 

Эта статья — краткая заметка о двух связанных друг с другом эмпирических правилах.
Если внутри функции есть условие if, то подумайте, нельзя ли его переместить в вызывающую сторону:
// ХОРОШО
fn frobnicate(walrus: Walrus) {
    ...
}
// ПЛОХО
fn frobnicate(walrus: Option<Walrus>) {
  let walrus = match walrus {
    Some(it) => it,
    None => return,
  };
  ...
}В подобных примерах часто существуют предварительные условия: функция может проверять предусловие внутри и «ничего не делать», если оно не выполняется, или же может передать задачу проверки предварительного условия вызывающей её стороне, а при помощи типов (или assert) принудительно удовлетворить этому условию. Подъём проверок вверх, особенно в случае предварительных условий, может иметь лавинообразный эффект и привести к уменьшению общего количества проверок. Именно поэтому и возникло это правило.
Ещё одна причина заключается в том, что поток управления и if сложны, к тому же оказываются источниками багов. Поднимая if наверх, мы часто приходим к централизации потока управления в одной функции, имеющей сложную логику ветвления, но сама работа при этом делегируется к простым линейным подпрограммам.
Если у вас есть сложный поток управления, то лучше перенести его в одну функцию, умещающуюся на одном экране, а не разбрасывать его по файлу. Более того, если весь поток находится в одном месте, то часто можно заменить избыточность и невыполняющиеся условия. Сравните:
fn f() {
  if foo && bar {
    if foo {
    } else {
    }
  }
}
fn g() {
  if foo && bar {
    h()
  }
}
fn h() {
  if foo {
  } else {
  }
}В случае f гораздо проще заметить «мёртвое» ветвление, чем в последовательности g и h!
Есть и другой схожий паттерн, который я называю рефакторингом «растворяющихся enum». Иногда код начинает выглядеть так:
enum E {
  Foo(i32),
  Bar(String),
}
fn main() {
  let e = f();
  g(e)
}
fn f() -> E {
  if condition {
    E::Foo(x)
  } else {
    E::Bar(y)
  }
}
fn g(e: E) {
  match e {
    E::Foo(x) => foo(x),
    E::Bar(y) => bar(y)
  }
}Здесь две команды ветвления; если поднять их наверх, то становится очевидно, что это одно и то же условие, которое повторяется трижды (в третий раз оно превращается в структуру данных):
fn main() {
  if condition {
    foo(x)
  } else {
    bar(y)
  }
}Этот совет взят из подхода, ориентированного на обработку данных. Программы обычно работают с сериями объектов, или, по крайней мере, горячий путь выполнения обычно связан с обработкой множества сущностей. Именно из-за количества сущностей путь и становится в первую очередь горячим. Поэтому часто бывает разумно ввести концепцию «группы» объектов и в базовом случае выполнять операции с группами, а скалярная версия при этом будет особым случаем групповой обработки:
// ХОРОШО
frobnicate_batch(walruses)
// ПЛОХО
for walrus in walruses {
  frobnicate(walrus)
}Основное преимущество здесь — производительность. А крайних случаях — огромный рост производительности.
Если у вас есть целая группа объектов для обработки, можно амортизировать затраты на запуск и более гибко определять порядок обработки. На самом деле, вам даже не нужно обрабатывать сущности в каком-либо конкретном порядке: можно реализовать трюк с векторизацией/массивом структур, чтобы сначала обрабатывать одно поле всех сущностей, а затем переходить к другим полям.
Наверно, самый забавный пример здесь — это умножение многочленов на основании быстрого преобразования Фурье: оказывается, одновременное вычисление многочлена в куче точек можно выполнять быстрее, чем кучу вычислений отдельных точек!
Два эти совета про for и if даже можно комбинировать!
// ХОРОШО
if condition {
  for walrus in walruses {
    walrus.frobnicate()
  }
} else {
  for walrus in walruses {
    walrus.transmogrify()
  }
}
// ПЛОХО
for walrus in walruses {
  if condition {
    walrus.frobnicate()
  } else {
    walrus.transmogrify()
  }
}Версия ХОРОШО хороша тем, что ей не приходится многократно проверять condition, она избавляется от ветвления в горячем цикле, а потенциально и обеспечивает возможность векторизации. Этот паттерн работает и на микро-, и на макроуровне — хорошей версией архитектуры можно считать TigerBeetle, в которой в плоскости данных мы одновременно работаем с группами объектов, чтобы амортизировать стоимость принятия решений в плоскости управления.
Хотя совет про for в первую очередь связан с повышением производительности, иногда он и улучшает выразительность. Когда-то был довольно успешен jQuery, работавший с коллекциями элементов. Язык абстрактных векторных пространств чаще оказывается более удобным инструментом, чем куча уравнений с координатами.
Подведём итог: поднимайте if наверх и опускайте for вниз!