March 13

Последствия и его стильный код

Идеальный язык программирования содержит одну инструкцию "сделать хорошо", которую идеальный компилятор переводит за 0 времени в программу, которая делает хорошо

На самом деле программисты не нужны. Ну, в идеале надо как можно меньше писать кода. С появлением ChatGPT это стало очевидно, люди не хотят программировать, люди хотят решать задачи, желательно как менее прикасаясь к коду. Это ещё давно было подмечено в ТРИЗ:

Идеальная система — это которой нет, но её функции выполняются.

Так, а при чём декларативный код? Да при том что он ближе всего к идеалу: мы пишем что надо сделать, а компьютер сам разбирается, как. Честно говоря "как" это вообще вопрос второстепенный, и становится он первостепенным когда с "что" мы уже разобрались.

Давайте возьмём типичный код из палаты мер и весов. Писать будем на JS, чисто потому что поддерживает все фичи, нам нужные.

function squareNumbers(numbers) {
  let result = [];
  for(let i = 1; i < numbers.length; i++) {
    let squared = numbers[i] * numbers[i];
    result.push(squared);
  } 
}

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

Довольно частая ситуация, что породила мемы и имеет гордое название "ошибка на единицу". Давайте избавимся от цикла, всё равно же он нам не нужен, да?

function squareNumbers(numbers) {
  let result = [];
  for(let number of numbers) {
    let squared = number * number;
    result.push(squared);
  }
  return result;
}

Теперь мы явно говорим компьютеру, что мы хотим взять каждое число из массива. Декларативность тут исключает возможность ошибиться на единицу и прямо пишем наше намерение.

Но давайте пойдём дальше. На самом деле мы не хотим на каждый чих присоединять к массиву новый элемент. Мы хотим обработать каждый элемент! Давайте сделаем это намерение более ясным:

function squareNumbers(numbers) {
  let result = new Array(numbers.length);
  for(let [index, number] of numbers.entries()) {
    result[index] = number * number;
  }
  return result;
}

И теперь нам остался всего один шаг до декларативного подхода:

function squareNumbers(numbers) {
  return numbers.map(number => number * number);
}

Мы сделали нашу функцию короче, выразив наши изначальные намерения. Заодно и уменьшили возможные ошибки. Если в изначальном примере было неясно, i = 1 это ошибка или намерение, то исходя из текущей версии наше намерение может быть довольно легко прочитано.

Разумеется, так бывает не всегда. Иногда декларировать своё намерение не представляется возможным. Иногда слишком неочевидным. Но в целом декларативное программирование рулит.

Мы можем это легко понять, потому что, например, в веб фреймворках часто рассматривается такая штука как Router с декларативным интерфейсом, какие маршруты какая функция обрабатывает. Просто так проще чем это описывать императивно.

Рассмотрим ещё несколько возражений из оригинальной статьи:

Во-первых не всем всегда бывает понятна лаконичная абстракция по типу той же lambda функции.

Лямбда функции это... обычные функции. То что питонисты с ними мало знакомы в основном прямое следствие того, что ФП пользоваться в нём, мягко говоря, проблематично.

На самом деле мы могли бы переписать пример из статьи с помощью list comprehension:

numbers = [1, 2, 3, 4, 5]
squared_numbers = [x * x for x in numbers]

Но компоновать код в таком стиле не особо представляется возможным. В нормальных языках для этого есть пайпы или их аналог:

numbers = [1, 2, 3, 4, 5]
squared_numbers = numbers
    |> map \x -> x * x
    |> to_list

Так оно получается гораздо более приятным для чтения. Но увы, чего в питоне нет того нет.

Но если мы возьмём полностью декларативный код с абсолютной "лаконичностью" и использованием синтаксического сахара то получим намного менее читабельный рассказ.

Тут сразу же есть несколько высказываний. Во-первых, совсем необязательно стремиться к абсолютной лаконичности, насколько это возможно. Декларативный стиль поощряет опускать детали, которые не отражают ваши намерения.

Это имеет неожиданную сторону. Обычно программисты ошибаются в шаблонном коде, потому что... склонны его пропускать мимо глаз. А пропускают потому, что он не отражает намерения программиста.

Меньше шаблонного кода — меньше ошибок.

Во-вторых, читаемость и синтаксический сахар — палка о двух концах. Все знают, что в JS классы это сахар над прототипами. Сравните два кода:

var Klass = (function() {
  function Klass() {}
  Klass.prototype.foo = function() {
    return 1;
  };
  return Klass;
})()
class Klass {
  foo() {
    return 1;
  }
}

Очень сложно сказать что первый код читаемее. А ведь синтаксический сахар сделал его... ну... более декларативным.

Ну и напоследок, самое вкусное:

Мы бездумно год за годом на этом своем backend используем методы сетевых библиотек даже не зная как они работают и что делают на самом деле

Как говорил старина Дейкстра

The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.

Смысл абстракций в том, чтобы мы не думали о том, как они работают. Обычно нам надо думать над логикой, чтобы она работала правильно. В конце концов, толку от быстрой программы если она не работает?

Если мы лезем внутрь абстракций, мы прибавляем себе головной боли, чтобы работать корректно с ещё большим количеством вещей одновременно. А чем больше вещей... тем больше ошибок.