Как стать автором
Обновить

Поднимайте If вверх, опускайте For вниз

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров17K
Автор оригинала: Alex Kladov

Эта статья — краткая заметка о двух связанных друг с другом эмпирических правилах.

Поднимайте If вверх

Если внутри функции есть условие 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)
  }
}

Опускайте For вниз

Этот совет взят из подхода, ориентированного на обработку данных. Программы обычно работают с сериями объектов, или, по крайней мере, горячий путь выполнения обычно связан с обработкой множества сущностей. Именно из-за количества сущностей путь и становится в первую очередь горячим. Поэтому часто бывает разумно ввести концепцию «группы» объектов и в базовом случае выполнять операции с группами, а скалярная версия при этом будет особым случаем групповой обработки:

// ХОРОШО
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 вниз!

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+36
Комментарии25

Публикации

Ближайшие события