(https://hardcode.fm/2018/07/28/episode001.html 00:40:57)
Часть подкаста про имена и смыслы - охуенная! Не знаю, куда написать фидбэк, поэтому пишу сюда.
Я тоже давно думал про эту дихотомию между многозначными (но знакомыми) и однозначными (но незнакомыми) именами. Мне кажется, это два разных стиля мышления: условно "математический" и условно "поэтический".
Когда мы создаем какое-то математическое построение, имена играют чисто утилитарную роль: по сути, это просто ссылки на ранее определенные структуры, эдакие сишные указатели, но для мозга. Для нас нет разницы, назвать структуру "группой", "театром" или, например, 0xdeadbeef - на самом деле мы создаем новое значение, которого до этого у нас в языке вообще не было, и нам просто нужно как-то к нему обращаться, так что подойдет любое слово, любая последовательность звуков и букв.
Если же мы создаем поэзию, то все наоборот: нам обычно важны сами слова, а не их семантика ("семантика" в смысле некоего точного, отдельного значения, однозначно соотнесенного с этим словом). Какого-то конкретного смысла у слова в данном контексте может вообще не быть - мы играем на связях этого слова с другими словами, аффектами, состояниями, и таким вот опосредованным путем пытаемся передать то самое "облачко", о котором шла речь в эпизоде.
Если есть между математиками и поэтами кое-что общее, то это вот что: и те, и другие не боятся создавать новые слова\смысловые структуры под конкретную задачу, которая перед ними сейчас стоит. Разница в том, что математик рассчитывает на понимание написанного без контекста (на практике - с минимальным контекстом): каждое использованное понятие явным образом вводится (и в пределе весь текст может быть вообще забит в прувер и подвергнут механической обработке), - в то время как поэт (музыкант, кстати, тоже), напротив, рассчитывает на то, что читатель будет иметь общий с ним контекст (языковой, культурный - ну то есть буквально, что читатель читал\слушал то же и испытывал те же состояния, что и автор).
В чисто практических терминах разница в этих подходах в том, что на загрузку в мозг математики требуется время: надо взять и по кирпичикам выстроить в голове ту же семантическую сеть, которая декомпозирована автором в отдельные определения на бумаге (но зато можно быть уверенным, что коммуникация будет (почти) однозначной и сеть получится (почти) точно такой же); музыка же или (в меньшей мере, но все же) поэзия воспринимаются конкретно, прямо сейчас, потому что дергает уже существующую семантическую сеть (или не дергает, если автор промахнулся с ожиданиями относительно читателя - то есть это такой lossy compression).
Как это все относится к программированию? Казалось бы, очевидно, что математический путь нам должен быть ближе - но! На самом деле цели взаимодействия с построенной системой у разработчика и математика (или студента, например) разные: нам не нужно загружать всю систему себе в голову, нам (обычно) нужно локализовать какой-то конкретный дата\контрол флоу, внести какое-то локальное изменение, найти дающие нужный нам результат входы в систему. Никто не изучает легаси просто ради того, чтобы любоваться спрятанными в нем прекрасными структурами и абстракциями (хотя ладно, наверняка и такие мазохисты существуют); обычно нам нужно (не имея полной картины!) взять и сделать, чтоб работало (и притом еще вчера).
И вот здесь как раз в дело вступает контекст: это и соглашения по именованию, и общие идиомы, и паттерны, и распространенные архитектуры - наконец, мы пытаемся буквально использовать метафоры из реальной жизни (по сути весь ООП (был) построен на этой посылке (спойлер: такие метафоры обычно не работают)). Если X на самом деле Y, но очень похож на Z, то зачастую мы назовем его Z - просто потому, что это сэкономит время 95% юзеров, которые будут использовать его именно в этом качестве (ну, или это мы сейчас думаем, что будут).
В итоге, конечно, важны оба подхода - первый все-таки в большинстве случаев предпочтительнее, но может плавно перетекать во второй; надо стремиться именно к выработке общего словаря оригинальных терминов и смыслов, к некоей общей "теории дизайна систем", которая будет включена в общий контекст индустрии. И еще было бы неплохо отвязаться от английского языка как эдакого общего знаменателя для всех обозначений: в идеале, для идентификаторов нужен легковесный сконструированный язык на основе общий латинских (по факту - давно интернациональных) корней с гибкой грамматикой и простым словообразованием. И еще пара идей:
- Меньше LoC - меньше возможных багов. Меньше имен - меньше проблем с их придумыванием. Если вместо введения нового имени можно тривиально и очевидно соединить уже имеющиеся, то лучше так и сделать. В этом плане любопытен APL и tacit programming вообще (дисклеймер: я не предлагаю писать рейтрейсеры в 8 строчек на j и прочие прелестные извращения):
+
- сложить числа,/+
- суммировать список, и так далее. По факту имя является реализацией, а потому абсолютно точно, понятно и однозначно. Это хороший идеал (но непонятно, как его достичь). - Туда же: явное лучше неявного. Structural лучше, чем nominal. Композиция лучше, чем комбинаторный взрыв. Open maps лучше, чем records и javabeans. Если можно вместо именования данных указать сами данные, то лучше так и сделать.
- Если
let <имя> = <функция> 42
понятней, чем просто<функция> 42
, то это плохая функция. То же и с аргументами. В идеале, функция должна однозначно определять свои аргументы и результат.numToWhichWeAdd = 1, numThatWeAdd = 2, numToWhichWeAdd.plus(numThatWeAdd)
не понятнее, чем1 + 2
. Если от введения временных переменных перед передачей их в функцию код становится понятнее, чем при прямой передаче аргументов без временных имен, то с кодом что-то не так. Если функция берет больше 2-3 аргументов, с функцией что-то не так. - Паттерны нужно формализовывать и называть уникальными именами. Нужно больше монад и трансдьюсеров (но только если все о них знают).
- Не бояться придумывать новые слова.
aeThaex3
Да, полностью согласен со всеми поинтами в этом абзаце. Но мне все-таки кажется, что в целом прогресс хоть и медленно, но идет в этом направлении, общий вектор развития правильный.
Ну, в лайттейбл это в принципе и есть (в cider, кстати, тоже добавили: #light над функцией или cider-enlighten-mode), но проблема там в том, что не всегда понятно, как именно этот реальный execution flow показать пользователю. Грубо говоря, (map my-fn (range 9000)) - что показывать для my-fn?
В этом свете, кстати, очень интересно выглядит spec - я помню, что видел где-то тулзу для динамической генерации спеков на основе данных, бегающих внутри программы. То есть если для my-fn редактор покажет что-то вроде :arg (s/and number? #(< 0 % 9000)) - а в реальном коде это будет скорее набор ключей с предикатами - то это будет уже кое-что интересное. А если сюда еще добавить эдакий аналог shrinking'а, чтобы ide сама искала и подсвечивала отдельные выбивающиеся из общей картины данные, мм...
Это прикольная идея, да! Какие-то тулзы для синхронизации состояния и текста точно нужны, правда лично мне чаще хочется обратного: чтобы состояние синхронизировалось с текстом - без полной перезагрузки неймспейса и полностью прозрачно. Типа, вырезал блок defn - соответствующая var удалилась (а точнее "затенилась", с бэкапом рантаймового состояния на ней), все инвалидированные вызовы подсветились. Вставил обратно - все вернулось назад.
Еще можно попробовать как-то визуализировать текущее состояние неймспейса - в либе вроде этого нет, но я так понимаю это должно быть несложно добавить. Хотя тут вопрос больше в том, как сделать удобный интерфейс для такой визуализации. Да, а еще можно вообще попробовать отойти от файликов в фс и делать save в какой-нибудь глобальный регистр тегированных функций - глобальный, распределенный и на блокчейне, разумеется ;) Ну если серьезно, то тут и вправду много интересных штук можно придумать.
Я тут плохо выразился. Имелось в виду по аналогии: меньше кода = меньше багов (условно), так же и меньше имен = меньше плохих имен.
Да, согласен, есть такое. Поэтому и "непонятно, как его достичь" ;)
Тут зависит от контекста. Откуда sum и diff взялись? Идея в том, чтобы не было такого: sum = compute_sum(...), и так далее. Более того, даже если реализация нашего сompute_sum вроде бы совпадает с +, то лучше вынести его в отдельную функцию (и назвать ее как раз sum; в tacit-стиле, кстати, итоговое выражение останется тем же: sum / diff), потому что локальное имя - это непрозрачная конструкция, оно внутри black box'а объемлющей его функции. Что если нам понадобится добавить обработку ошибок, логирование, дополнительные констрейнты на значение, ну или просто посмотреть, во что вычисляется сумма на данных инпутах? Когда у нас есть отдельная функция, мы ее можем просто взять и заинструментить, например, а с локальным именем нам придется прямо туда добавлять дебагпринты, spy, ассерты и так далее. То есть мы как бы вроде бы и создаем отдельную абстракцию с помощью ввода нового имени, но при этом она остается намертво вмурована в окружающий ее контекст - получается мы как бы ее создаем и сразу выкидываем. А отдельная функция - это уже "материальная" абстракция, которую понимает наша среда, которая может расширяться независимо от остальных частей, которая имеет глобальное уникальное имя и т.п. В итоге получается более прозрачная система, которую проще просветить насквозь и потыкать палочкой (в каком-то смысле тут есть аналогия с лисп\смолток и сишным подходами).
Я имел в виду, по аналогии с nominal/structural typing, что идентичность на основе структуры чаще гибче и понятнее. Ну и тут абстракция == введение нового имени. Грубо говоря, вот можно написать тип
Reducer a,b
, а можно написатьa b -> a
. У меня ощущение, что общим местом считается, что введение таких алиасов на каждый чих - это абсолютное добро. Но при этомReducer a b
, во-первых, тупо длиннее, а во-вторых - непонятнее, потому что мне нужно пойти и почитать документацию, чтобы узнать, что это такое. Аa b -> a
- это просто функция, любой читающий это сразу точно знает, что это такое и как оно работает. Аналогия с джавабинами и простыми мапами, "общий контекст индустрии". Или, не знаю, например нам в качестве аргумента нужна entity вида :user/email, :admin/group. Мы можем создать иерархию User->Admin (а потом нам понадобятся, например, боты-админы и все поломается), мы можем создать композицию с кривым именем типа UserThatIsAdmin (и так на каждую возможную комбинацию), а можем просто так и написать, что нам нужна "entity вида :user/email, :admin/group", без дополнительных имен. Пример так себе, но идея понятна, да?