пятница, 22 ноября 2013 г.

Функциональное реактивное программирование

Это примерный текст выступления "Функциональное реактивное программирование", которое Алексей Осипенко и я проводили на ноябрьской встрече Донецкого Лямбда-клуба.

Мир основан на изменяющемся состоянии. Мы живём во времени и думаем во времени, и разнообразнейшие события прилетают откуда ни возьмись, и тоже во времени. Как организовать приложение, для которого эти события являются вводом - то есть, любое интерактивное приложение? А если хочется не иметь изменяемого состояния, иметь чистые функции и работать как можно более декларативно? На эти вопросы отвечает функционально-реактивное программирование (functional reactive programming). Мы постараемся рассказать о таком подходе.


Нам очень нравится определение функционального программирования из выступления Дэниэла Спивака “Жизнь в пост-функциональном мире”. Это “функция как основная единица абстракции”. Основа основ функцинального программирования - это композируемые примитивы для решения широкого класса задач. За счёт этого о программах, написанных полностью в функциональной парадигме, легко рассуждать в терминах математики - они предсказуемы и даже доказуемы.


“Функциональное программирование объединяет гибкость и мощь абстрактной математики с интуитивной понятностью абстрактной математики”.


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


События имеют свойство происходить в произвольное время и совершенно не предсказуемы заранее. Чаще всего их обрабатывают как-то так:


function Counter(element) {
  var that = this;
  this.count = 0;
  this.clicked = function () {
    this.count += 1;  
  }
  $(element).click(function() {
    return that.clicked();
  });
};

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


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


Однако же мы не зря сюда пришли. Реактивное программирование, о котором мы хотим рассказать, основывается по большому счёту на концепциях из функционального программирования и его способах заводить примитивы.
Лирическое отступление: почему мы про UI и где ещё это применимо. Это применимо везде, где есть входящие события. Ну вот просто везде. Однако UI ставит наиболее интересные и сложные задачи по комбинации потоков: хоть сколь-нибудь развесистый пользовательский интерфейс начинает требовать всё более и более новых и интересных комбинаторов.


Идея примитивов ФРП вкратце. Естественный код:


$("#login").on("click", function (event) {
  var value = $(event.target).val();
  if (value.length > 0) {
    $("#notice").text(value);
  }
});


Вводим один уровень абстракции:


var clickE = $("#login").events("click");
clickE.onValue(function (event) {
  var value = $(event.target).val();
  if (value.length > 0) {
    $("#notice").text(value);
  }
});


На первый взгляд кажется, что мы ничего не добились. Однако же, теперь с этим примитивом мы можем кое-что делать.


var clickE = $("#login").events("click");
var values = clickE.map(function (event) {
  return $(event.target).val();
});
var nonEmptyValues = values.filter(function (value) {
  return value.length > 0;
});
nonEmptyValues.onValue(function (value) {
  $("#notice").text(value);
});


Ну и инлайним лишние переменные:


var values = $("#login").events("click").map(function (event) {
  return $(event.target).val();
}).filter(function (value) {
  return value.length > 0;
});

values.onValue(function (value) {
  $("#notice").text(value);
});


Лирическое отступление: point-free style или tacit programming. Если функция - гражданин первого класса, то имея богатую библиотеку хелперных функций, можно писать, избегая термов. Получается элегантно.

var values = $("#login").events("click").map(
  chain(pluck('target'), $, method('val')
).filter(nonEmpty);
values.onValue(function (value) { $("#notice").text(value); });


Теперь немного разберёмся с денотационной семантикой. Функциональных примитивов для ФРП - два. Традиционно их называют Event и Behaviour, мы же по своих соображениям возьмём другие - Stream и Box.


Stream представляет собой дискретный поток событий и семантически эквивалентен списку кортежей “время-значение” при неубывающем времени:


type Stream[a] = [(T, a)]


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


type Box[a] = T -> a


На этих двух китах стоит функциональное реактивное программирование. Отличаются они тем, что при подписке на поток вы не получаете значения последнего произошедшего события - только следующее. Подписавшись же на коробку, вы сразу получаете значение последнего произошедшего события, и начинаете ждать следующего.


Лирическое отступление: push против pull. Семантика, которую я дал для события и поведения приводит нас к pull-driven реализации, которая, хоть её очень легко понять, чисто и красиво написать и замечательно о ней рассуждать и доказывать, по эффективности сильно проигрывает push-driven реализации. Однако же, push-driven реализация в целом более некрасива и сложна. Мы будем говорить в основном о push-driven реализации, потому что только ей можно нормально пользоваться в реальном мире.


Писать будем только интерфейс библиотеки, и если захотим поделиться чем-то интересным - тогда и реализацию.


Что у нас в потоке может быть? Всё. Простейшие конструкторы:


Box.nothing();   // empty box
Box.unit(10);    // box with constant value of 10
Stream.never();  // empty stream
Stream.unit(10); // stream with immediate value of 10


Самоочевидно. 

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


Введём и мы у себя понятие Error события, которое раз появившись будет нетронутым проходить все нормальные ошибки и комбинаторы до тех пор, пока кто-то специально не поинтересуется. Естественно, нужен элементарный способ сконструировать ошибку.


Stream.error(10); // stream with immediate error of 10
Box.error(10);    // box with constant error of 10

Это UI, поэтому, здесь будет полезна работа с временем. Базовый конструктор для этого:


Stream.later(1000, 10); // in a second pops out 10


Более интересно - из DOM-события. Это логично добавить в jQuery.fn:


$('input').stream('keyup'); // stream with keyup events


Ещё хочется привязать стрим к какому-то событию в будущем. Можно создавать стрим из функции обратного вызова (callback), а можно воспользоваться другой абстракцией из функционального мира: promise.


Вот это способ обработать конец асинхронного действия с функцией обратного вызова:


asyncCall(data,function(response){
  // You have response from your asynchronous call here.
}, function(err){
  // if the async call fails, the error callback is invoked
});


А вот так это делается с помощью promise:


var promise = asyncCall(data);

promise.done(function(response){
  // You have response from your asynchronous call here.
}).fail(function(err){
  // if the async call fails, you have the error response here.
});


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


Интерфейс очень удобный и очень нужный, поэтому мы им и возпользуемся:


Stream.fromPromise($.ajax(params)); 
// pops a success event on successful response
// or the error event on error response

Итак, что мы можем сделать с нашими потоками?


Самое простое - отобразить, или пропустить через функцию. Для коллекций, наверно, это всем знакомо.


_.map([1, 2, 3], function (x) { return x*x; }); // returns [1, 4, 9]


На потоке и коробке map работает совершенно очевидным способом - получается новый поток или коробка, значения на которой соответствуют исходным, пропущенным через функцию.


priceValue.map(function(x) {
  return x > 1000;
});


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


Лирическое отступление. Вы только что познакомились с функтором. В функциональном программировании, функтор - это контейнер, позволяющий выполнить некоторую функцию над своим содержимым и завернуть результат в такой же контейнер. Эта операция обычно называется fmap. В этом смысле, список и наша коробочка со значением (или поток событий) - одно и то же. Потому что их можно обработать единообразно - пропустить значения через функцию, сохранив форму контейнера. fmap там ещё должен удовлетворять двум простеньким законам, и если будет интересно, мы про них расскажем. А пока продолжим.


Рядом со словом map часто звучит что? filter. Rails программисты знают его под словом “select”.


priceValue.filter(function(x) {
   return x >= 1000;
});


Мы получим значение цены, если оно превышает 1000. Опять-таки, хорошая аналогия с коллекциями.


Ладно. Перейдём к по-настоящему интересной функции. flatMap. Это тоже преобразование значений в контейнере, но существенно более общим образом. Функция, которая передаётся в flatMap должна возвращать не значение, а значение, завёрнутое в новый контейнер. Сам же flatMap каким-то образом связывает результаты этой функции и возвращает опять-таки обёрнутое значение. Что-то на словах сложно получается.


Ну вот, например, есть список пользователей, а у каждого из них есть список комментариев. Нужно получить список всех комментариев.


_.flatMap(users, function (user) { return user.comments(); });
// returns flat list of all the comments the users have


Обратите внимание, что comments() может вернуть и пустой список - что, в свою очередь, никак не вложится в результат.


В нашем случае функция внутри flatMap должная возвращать не список, а поток или коробку. А вот связывать результат можно по-разному. Можно в результирующий поток замешать события всех дочерних потоков. Это наиболее логичная стратегия для потока. А можно подписаться на первый дочерний поток, а при появлении второго, переключиться на него и отписаться от первого. Это наиболее логичная стратегия для коробки.
Заканчивать же результирующий поток будем тогда, когда все дети и родительский поток закончились.


Зачем нам такое преобразование? Хм. Сейчас покажу. Это невероятно мощная штука. Для начала, её можно использовать для прозрачного связывания локальных потоков с асинхронными действиями.


var requests = usernames.map(function(u) {
  return { url: "/check-username/" + u; }
});
requests.flatMap(function (params) {
  return Stream.fromPromise($.ajax(params));
});
// usernames  => "John",          "Peter"
// requests   => {"url": "/check-username/John"}, ...
// flatMap    => {correct: true}, {correct: false}

Во-вторых, с её помощью можно написать map и filter.


Stream.prototype.map = function (f) {
  return this.flatMap(function (x) {
    return Stream.unit(f(x));
  });
}

Stream.prototype.filter = function (f) {
  return this.flatMap(function (x) {
    if f(x) {
      return Stream.unit(x);
    } else {
      return Stream.nothing();
    }
  });
}


Но наибольшие возможности flatMap открывает для написания комбинаторов. Давайте предположим, что нам нужно скомбинировать две коробки в одну. Например для получения значения “пароль и подтверждение” заполнен корректно.


password.map2(passwordConfirmation,
              function(l, r) { return l == r; });


Давайте попробуем написать map2:


Box.prototype.map2 = function (other, f) {
  return this.flatMap(function (x) {
    return other.map(function (y) { return f(x, y); });
  });
}



Поскольку операция map2 имеет смысл только на Box, мы используем версию flatMap, которая берёт только последний поток.


Те, кто сейчас сказал “о, я всё понял!” - поздравляю, вы познакомились с монадами.


Да, монада - это способ положить простое значение в контейнер и способ его достать и сделать новый контейнер. Ну плюс три простых как азимовские закона, о которых мы тоже можем рассказать. Положить простое значение в контейнер принято называть unit (мы его назвали так же), а сделать новый контейнер принято называть bind (мы его назвали flatMap и это имя реально имеет шанс победить, потому что на Scala тупо больше человек пишет код за деньги).


Поскольку мы получили монаду, мы получили аппликативный функтор, и это подтверждается наличием map2 (который позволяет нам тривиально определить apply).


Box.prototype.apply = function (other) {
  return this.map2(function (f, x) { return f(x); } );
}

Собственно, аппликативный функтор позволяет скомбинировать функтор, содержащий функцию с функтором, содержащим значение, и получить функтор, содержащий преобразованное значение. Ну и 4 мудрёных закона, о которых можно поговорить отдельно.


Для потоков же, вместо map2, мы сделаем не менее полезную функцию - merge, позволяющую просто объединить потоки в один. Используя flatMap для потоков, который слушает все дочерние потоки, сделать это очень просто.


var id = function (x) { return x; }
Stream.merge = function (streams) {
  return Stream.fromList(streams).flatMap(id);
}


merge - полезная штука.


var inc = plusClicks.map(function() { return 1; });
var dec = minusClicks.map(function() { return -1; });
var change = plus.merge(minus);


Ещё пара примеров фокусов с flatMap:


Stream.prototype.debounce = function (delay) {
  return this.flatMapLast(function (value) {
      return Stream.later(delay, value);
  });
}

debounce сглаживает слишком частые события. То есть, если на исходном потоке события будут возникать чаще, чем delay, то отправится только последнее из них. Здесь мы используем неродной flatMap, на что и указываем.

Или вот - очень полезный комбинатор sampledBy:


Box.prototype.sampledBy = function (sampler) {
  var that = this;
  return sampler.flatMap(function () { return that.take(1); } );
}


О нём легко думать, как об отображении (map) потока на коробку.


Вот как просто и красиво таким способом записывается такая сложная и неприятная вещь, как drag and drop. Получаем события мыши:


mousedown = $(document).stream('mousedown', '.item')
mouseup = $(document).stream('mouseup')
mousemove = $(document).stream('mousemove')


Легко и непринуждённо получаем такую прелесть, как событие mouseDrag:


mousedrag = mousedown.flatMapLast( 
  (e)-> mousemove.takeUntil(mouseup)
)


Ловим положение курсора...


cursorPosition = mousemove.box().map(
  (e)-> { x: e.clientX, y: e.clientY }
)


...в нужные нам моменты…


startPosition = cursorPosition.sampledBy mousedown
currentPosition = cursorPosition.sampledBy mousedrag


Потом немножко считаем…


shiftPosition = startPosition.zip(currentPosition)
  .map((s, m)-> 
    { left: m.x-s.x, top: m.y-s.y }
  )


...и двигаем объект, куда просили:


shiftPosition.onValue( (pos)-> $('.item').css(pos) )


Ах, да, и просим браузер не умничать:


mousedown.onValue (e)-> e.preventDefault()

Можно ещё вместо этого обойтись замыканиями:


newPosition = mousedown.flatMapLast (md)->
 target = $(md.target);
 {left, top} = target.offset();
 [startX, startY] = [md.clientX - left, md.clientY - top]
 mousemove.map (mm)->
   target: target
   left: mm.clientX - startX
   top: mm.clientY - startY
 .takeUntil mouseup


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


Итак, как теперь жить:


  • Bacon.js, полностью рабочая библиотека.
  • jnoid, демонстрационная библиотека, которая была написана, чтобы разобраться в теме и другие тоже могли в ней разобраться. Для этого даже было использовано “литературное программирование”.
  • javelin для closurescript от пользователя с одним из самых зачётных ников на Гитхабе.
  • elm для маньяков. Красивая вещь для поразбираться, не очень подходит для работы - там jQuery нету.


Берите любой и пробуйте решать проблемы. Не пожалеете.

Другие ссылки:


понедельник, 12 августа 2013 г.

Дюжина прочитанных книг - второй выпуск

Согласно статистике, предыдущий подобный выпуск хоть кто-то, да прочитал. Поэтому публикую второй.

Итак, 12 книг, прочитанных с момента предыдущего выпуска, в произвольном порядке, с короткими рецензиями и оценками без спойлеров.

1. "Анафем" Нила Стивенсона. Лучшее, что я читал, со времени "Ложной слепоты". Необычная вселенная, интересные персонажи, восхитительная концепция матического мира. Обилие выдуманных слов, на мой взгляд - не фича, можно было бы и без них, но в некоторых случаях таки именно они помогают автору передать мысль. Книге можно было бы пожелать быть покороче, возможно, иметь сюжет позакрученнее, но вытерпеть можно. Несколько раз я откладывал книгу в сторону и на полчасика просто задумывался. Эти моменты стоят того, чтобы прочитать книгу.

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

Оценка: 10+.

2. "Большое, малое и человеческий разум" Пенроуза. Взгляд Пенроуза на то, как работает человеческий разум, стоит того, чтобы с ним ознакомиться. Поэтому, рекомендую вам "Новый ум короля" и "Тени разума". Эта же книга является их продолжением, и содержит довольно много полемики по поводу его неортодоксальных тезисов, поэтому, в ней есть главы, написанные Хокингом, Картрайт и Шимони. Мне понравилось.

Оценка: 8.

3. "Функциональное программирование на Scala" Рунара Бьярнасона. Наконец-то! Наконец-то нашлась книга, объяснившая мне моноиды, функторы, аппликативные функторы, Trampoline, корекурсию, алгебраические типы данных, а самое страшное - монады! Книга прекрасно инкрементально излагает основы функционального программирования, функционального дизайна, способы решения проблем в этой парадигме, как вообще думают эти люди и зачем нужна теория категорий. Упражнения ни в коем случае нельзя пропускать - автор считает, что вы их делаете и весомая часть книги находится именно там.

Способ изложения - поставить проблему, разработать для неё набор примитивов, написать на них высокоуровневый интерфейс. Повторить несколько раз, оглянуться, нет ли чего похожего в том, что написали, чтобы выделить абстракцию более высокого уровня.

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

Оценка: 9.

4. "Rita Hayworth and Shawshank Redemption" Стивена Кинга. Решил попробовать читать художественную литературу на английском, и чтобы было проще, начать с хорошо знакомых и любимых книг. Эта книга восхитительна и будет таковой всегда. Она не о ужасах тюрьмы и не о изощрённых хитростях побега, она - о человеке и свободе. Если не читали - прочитайте немедленно.

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

Оценка: 10+.

5. "Варяг" Александра Мазина. Думал, что буду читать как книгу для отдыха. Так и вышло. Однако, если сравнивать книгу с другими книгами того же смысла (не особо нагружающими голову, которые можно читать на ходу и всё же не пойти на красный свет), книга резко становится весьма неплохой. Язык хороший (почти), сюжет не такой уж и прямолинейный, много экшена (куда без него), но при этом откровенной чуши нет.

Оценка: 5.

6. "Место для битвы" Александра Мазина. Да, я даже прочитал первую книгу продолжения. И она даже оказалась лучше начала. Язык автора стал намного лучше, сюжет, конечно, блистать не стал, но всё же интересно, чем это закончится.

Оценка: 6.

7. "Московская магия" Артёма Михалёва. А вот это уже типичная жёлтая книга, для чтения, когда вообще не хочется думать. На фоне таких книг, опять-таки весьма неплоха хотя бы сюжетом - несколько сюжетных линий нетипичны для этого жанра.

Хотя - рояли, плоские герои...

Оценка: 3.

8. "Тёмное сердце" Артёма Михалёва. Да, продолжение. Да, не хуже и не лучше.

Оценка: 3.

9. "1984" Джорджа Оруэлла. Читал в рамках "почитать знакомую художественную литературу на английском". Книга хорошая, сильная, отлично проработанная, повороты сюжета для меня непредсказуемые. Нужно только учитывать, что послевкусие от книги неоднозначное - можно надолго впасть в плохое настроение.

По поводу особенностей оригинала - оказалось неожиданно легко. Очень простой язык, мало лишних синонимов, простые и короткие предложения. Если хотите попробовать читать на английском, вполне можно пробовать с этой книги.

Оценка: 9.

10. "Элегантная вселенная" Брайана Грина. Классная книга, классный перевод, ничуть не хуже Хокинговских "Историй". Теория суперструн и браны, квантовый мир, мультивселенные и другие интересные штуки захватывают и не отпускают.

Минусы - книга старая, до постройки "БАК" и до проигранного пари Хокинга, кто знаком с темой - это почувствует.

Рекомендую всем, кому понравилась "Краткая история времени" Хокинга.

Оценка: 9.

11. "Урожаи и посевы" Александра Гротендика. Очень интересная книга, пролившая свет для меня на то, чем занимаются математики. Не в смысле, чем они полезны, а именно в смысле, что что они на работе сидят и делают. То есть, автор пытается показать природу математического открытия. Автор, кстати, весьма серьёзная личность.

Как это часто бывает с хорошими книгами, иногда книга казалась нудной, но в целом это того стоило.

Оценка: 9.

12. "Лифт" Юрия Бурнозова. Рассказ. По рекомендации Андрея Бабичева. Спасибо, Андрей, я теперь в лифт не сяду =)

Оценка: 7.



Сейчас в прочтении находятся: "В начале была командная строка" Нила Стивенсона, "Real World Haskell" Брайана Сулливана и очень медленно продвигающийся "Иисус есть Христос" Джеймса Талмейджа.

Ну и вы советуйте, если есть что.

четверг, 11 июля 2013 г.

Немного мыслей о языках программирования



Есть такая штука - естественный язык. Википедия определяет его как язык для общения людей, не созданный целенаправленно.

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

Словом, то, что в моём сознании стоит за "естественным языком", это язык, служащий для выражения мыслей и идей общего назначения. Это бытовые мысли: "хочу есть", "как пройти в библиотеку", это художественные произведения, это философские трактаты, это вообще научные произведения в гуманитарных науках, да и в какой-то мере в естественных и абстрактных тоже - хотя там встречаются и другие языки.

Причём, у естественных языков есть правила. Синтаксические (грамматические), семантические, прагматические. При том, что адресат языка допускает широкие отклонения от правил без потери передаваемой мысли, тем не менее, это поглощает дополнительные ресурсы считывателя на отсеивание этих помех. Когда отступлений от правил немного (запятая там, не та гласная здесь), это ещё не так заметно, но когда их много, и хуже того, в пределах грамматической, семантической и прагматической норм находится отличная от желаемой трактовка - возникает коллизия и адресат высказывания вынужден как-то эту коллизию решать. Если же информации для решения коллизии недостаточно, приходится даже запрашивать дополнительную информацию - переспрашивать. Синтетический пример: "Простите, а где вы пропустили запятую в "Оставаться в школе не разрешаю никому покинуть помещение."?".

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

А вот есть такая область человеческого знания, как музыка. Для того, чтобы записывать музыкальные мысли и идеи есть язык. И этот язык - ноты. Музыканты как-то договорились его использовать и очень довольны. На самом деле, музыкальную идею можно рассказать и на естественном языке. Но там будет довольно плохое соотношение "сигнал-шум". Этот язык относится к категории "формальных", что в бытовом понимании означает всего лишь более строгие правила. Более формальная трактовка понятия "формальный язык" говорит, что это множество конечных цепочек над конечным алфавитом, порождённых некоей, опять-таки, формальной структурой (грамматикой, БНФ-конструкцией или вообще конечным словарём), что тоже намекает нам, что он менее свободен, чем естественный. За счёт большей ограниченности исчезают коллизии (в естественном языке коллизии можно уменьшить избыточностью, именно поэтому на любом формальном языке идея занимает меньше места), то есть, исчезает необходимость переспрашивать и вероятность быть неправильно понятым. При этом, адресат языка всё ещё человек, и он всё ещё может иногда правильно понять идею, даже если она изложена с нарушением правил нотной грамоты. Однако в лучшем случае это вызовет у него раздражение, в худшем же будет потерян смысл.

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

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

Не так. Программы пишутся для людей. Об этом говорят хотя бы отступы и выравнивание, которые большинство языков программирования игнорируют. И ваш браузер, в котором вы это читаете, допускает большое количество отклонений от нормы html, пытаясь донести высказанную человеком мысль до другого человека - вас.

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

Опять-таки, есть масса не синтаксических правил в компьютерных языках, на которые компьютеру наплевать. Про отступы я уже говорил, но есть ещё именование идентификаторов, предельная сложность одной единицы кода, шаблоны проектирования, уровни доступа, запахи кода, парадигмы (функциональное / объектное / аспектное программирование), мутабельность-иммутабельность... Которые важны исключительно людям.

Итог: язык программирования - это удобный способ описать вычислительную идею. В нём есть некие правила, которых необходимо придерживаться, чтобы облегчить прохождение идеи до получателя - другого человека. Естественный язык - это удобный способ описать идею "общего назначения". В нём есть некие правила, которых необходимо придерживаться, чтобы облегчить прохождение идеи до получателя - другого человека.

А выводов два.

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

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

P.S. На картинке сверху Чарль Сандерс Пирс, основоположник семиотики.