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

О вреде синтаксического сахара

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

Ситуация здесь как в анекдоте о дорогах, в котором американец хвастается:
— Я когда еду по дороге, ставлю стакан с водой на капот и гоню со скоростью 100 км/час. Ни одна капля воды не расплескается.
На что русский говорит:
— А мы все залезаем на заднее сиденье и всю дорогу в карты режемся, да пиво пьём.
— А кто же машиной управляет?!..
— Да куда она, на хрен, из колеи денется.

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

Но довольно теории, пришло время замарать руки. Приведу пример. Допустим, мы пишем ORM и в определённый момент нам понадобилось получить список имен полей модели, чтобы, к примеру, составить SQL запрос. Также, допустим, что поля — это объекты, у которых есть свойство name, содержащее имя поля. Всего-то делов! Надо просто пробежаться по всем полям, вытащить имя и составить список:
var names = [];
for (var i = 0, l = fields.length; i < l; i++) {
    names.push(fields[i].name);
}
Похоже, мы пишем ORM на javascript-е, ну ничего, люди делают куда более странные вещи в наши дни. И кстати, мы попали в первую ловушку синтаксиса — «пробежаться» интерпретировали как цикл, и в результате получили пару переменных, сравнение целых чисел, инкремент и квадратные скобки. Чёрт возьми! Это не то, что я имел в виду, когда говорил «пробежаться»! Попробуем ещё раз:
var names = fields.map(function (field) {
    return field.name;
});
Короче, симпатичнее и куда ближе к тому, как я описывал алгоритм: пробежаться по полям — fields.map, получить имя — функция, которая получает имя. Н-да, функция, как бы от неё избавиться? Легко, не будем сдерживать себя, перепишем всё на питоне:
names = [field.name for field in fields]
Одна строчка! Казалось бы, мы достигли идеала, но на самом деле этот вариант в некотором смысле даже хуже предыдущего: его тяжело расширять и практически невозможно повторно использовать. Да ну? Давайте попробуем, предположим нам вдруг понадобилось иногда приписывать алияс таблицы перед именами полей:
names = ["%s.%s" % (alias, field.name) if alias
                 else field.name for field in fields]
Это уже не выглядит так здорово, а что если нам потребуется приписать алияс для поля? И ведь потребуется, не это так что-то ещё. Что ж не будем ждать, когда наш код выйдет из под контроля, отрефакторим его превентивно:
if alias:
    names = ["%s.%s" % (alias, field.name) for field in fields]
else:
    names = [field.name for field in fields]
Ясность вернулась, но появилось дублирование. Это не случайно, списковые выражения, которые мы используем связывают в одну синтаксическую структуру пробежку и операцию над отдельным элементом. Не проблема, просто должно остаться только одно списковое выражение:
if alias:
    get_name = lambda field: "%s.%s" % (alias, field.name) 
else:
    get_name = lambda field: field.name
names = map(get_name, fields)
… или ни одного — как только мы избавились от запутывания пробежки и операции списковое выражение стало ненужным. Тут есть ещё один интересный момент — мы вернулись к тому, что у нас было в javascript-е. Т. е. отсутствие в языке такого сладкого элемента как списковые выражения, привело к написанию более универсального кода. Неправда ли отличный пример «less is more», товарищи?

Итак, я «расправился» с циклом for и питоньими списковыми выражениями, пора идти дальше. Вам нравится обращаться к свойствам объекта через точку? Мне — очень, это кратко (кроме самого объекта и требуемого свойства присутствует только маленькая точка) и экспрессивно (даже человек далёкий от объектов легко поймёт в чём тут соль). Это настолько удобно, что мы не вспоминаем об альтернативах, а в том же питоне есть три способа получить атрибут объекта (прямое обращение к __getattr__ и т.п. — чит, не дающий ничего принципиально нового):
obj.name
getattr(obj, "name")
operator.attrgetter("name")(obj)
Нас интересует последний, самый жуткий вариант, потому что он превращает операцию доступа к атрибуту в функцию. Ту самую, которую мы эмулируем с помощью лямбды. Если бы это был единственный способ получить атрибут, то мы сразу бы написали универсальный, расширяемый и готовый к повторному использованию код:
from operator import attrgetter
names = map(attrgetter("name"), fields)

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

P.S. Я не пишу ORM на javascript-е.
P.P.S. Я не пишу ORM и на питоне, хотя временами я копаюсь в ORM Django.
P.P.P.S. Странная идея, изложенная здесь, пришла ко мне во время чтения Practical Common Lisp. Для тех, кто не в курсе, программа на лиспе представляет собой набор вложенных списков, каждый из которых состоит из “что делать” (имени функции, оператора или макроса) и последующих аргументов, т.е. представляет синтаксическое дерево самой себя. Другими словами в Lisp нет синтаксиса. И как ни странно, это делает программы на нём удивительно гибкими.

UPDATE. Чтобы ответить на большинство возражений, подойду несколько с другой стороны. Заметим, что map(), который я в конце концов использую, тоже абстракция довольно высокого уровня. На самом деле используемые мной абстракции можно выстроить в иерархию:
C-style for + абстрагирование от индексирования = for-in
for-in + возврат результата на каждой итерации = map
map + lambda = списковое выражение.

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

Подытоживая:
1. Отсутствие определённого синтаксиса в языке приводит к написанию более гибкого кода.
2. Этот более гибкий код будет более низкоуровневым.

Второе плата за первое,
from operator import attrgetter
names = map(attrgetter("name"), fields)
— преждевременное обобщение, если у нас есть точка.
Теги:
Хабы:
Всего голосов 135: ↑88 и ↓47 +41
Просмотры 28K
Комментарии 106
Комментарии Комментарии 106

Публикации

Истории