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

от автора

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

Поднимайте 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 вниз!


ссылка на оригинал статьи https://habr.com/ru/articles/911790/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *