Восхождение по бесконечной лестнице абстракций

Читать в полной версии →

Это перевод статьи Climbing the infinite ladder of abstraction от Алексис Кинг.

Я начала программировать ещё в начальной школе.

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

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

Кирпичная стена невыразительности

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

public String getName() {
  return this.name;
}

public void setName(String name) {
  this.name = name;
}

Это немного дурацкий пример, учитывая, что геттеры и сеттеры Java на этом этапе — что-то вроде боксёрской груши, но я их писала и раздражалась из-за них! Я изучила объектно-ориентированные шаблоны разработки и зарылась в книги, форумные ветки, блоги и вопросы на Stack Overflow о том как структурировать код, чтобы избежать спагетти, но как бы я ни старалась, всё что я писала, выглядело подозрительно похожим одно на другое.

Всё было максимально выводящим из себя, потому что с какой бы стороны я не подходила к проблеме, в результате получался стереотипный, тяжёлый бардак. Основная причина по которой я начала программировать, была избавиться от подобных проблем, так что оставалось? Всё очевиднее становилось одно — Java нужно бросать и пробовать что-то другое. Я начала изучать два совершенно разных языка программирования, JavaScript и Objective-C — оба мне понравились по разным причинам.

Когда я изучила JavaScript, мне открылись замыкания и функции первого класса — они меня заворожили. Через jQuery я узнала о его способности собирать приятные в использовании API и избавилась от скучного, "тяжёлого" чувства, которое распространял везде Java. Благодаря Objective-C я узнала о силе более динамической объектной системы с интересным синтаксисом и способностью справляться с "передачей сообщения" на более высоком уровне, чем в Java.

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

Открытие Lisp

Последующие несколько лет я училась ценить маленькое, простое ядро JavaScript, несмотря на неприязнь к его объектной системе и слабые способности в интуитивно-понятном моделировании данных. Я тщательно разобралась в его истории и обнаружила, что на его дизайн сильно повлиял малоизвестный, ограниченный язык Scheme и ещё более неизвестный язык Self, и я стала иногда задумываться: что, если объединить идеи этих языков, исключив некоторые компромиссы на которые пошёл JavaScript.

Эта идея жила в моём сознании пару лет, и хоть я и пыталась играть со Scheme пару раз, он был слишком недоступен для меня. Я привыкла к языкам с мощными, простыми в использовании IDE и когда я оказалась наедине с исполняемым файлом в командной строке и довольно скудной документацией, я потерялась — с чего начать. Если даже я смогу выполнить вычисление в REPL, куда дальше идти? Я начала программирование с игр, потом — веб-сайтов. Что мне было делать со Scheme?

Язык (а вернее, его неудачная экосистема) оказался для меня в том возрасте слишком пугающим, но идея гомоиконности Lisp запала мне в душу. Я начала писать свой язык программирования, высокодинамичный Lisp с прототипичной объектной системой, который называется Sol. Я работала над ним около года, и когда закончила, у него был не хилый комплект функциональности: в нём были лямбды, макросы, полнофункциональная объектная модель и модульная система в стиле CommonJS, в комплекте с возможностью динамически импортировать произвольные расширения С. Это был самый большой проект, над которым я когда-либо работала, и когда я закончила, была вполне довольна.

К сожалению, он также был плачевно медленным.

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

Путь в сложность

Приблизительно три года спустя, сегодня, я работаю на Haskell и провожу большую часть свободного времени программируя на Racket. Эти языки оставили во мне отпечаток и хоть я и изучила с того времени намного больше, постоянно нахожу себя противящейся основному потоку и возвращаюсь к функциональному программированию, гигиеничным макросам, и, возможно, самой мощной системе типов в мире в production-ready языке программирования.

Я начала понимать кое-что ещё: языки, к которым я привыкла — очень сложные.

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

Почему?

Иногда я говорю со своими коллегами и они ужасаются типам терминов, которые я использую. "Зачем вам может когда-либо понадобиться нечто названное монадой?" — спрашивают они в недоумении. "Макросы запутывают" — спорят они. "Явное — лучше".

Конечно, я не согласна, но почему? Чем я сдалась? Если мои друзья-программисты не понимают то, что я пишу, есть ли в этом какая-то польза?

Годами я искала язык программирования, который ликвидирует шаблонность, который позволит мне выражать идеи сжато и чисто, который позволит превратить сложные задачи в простые, и я открыла абсолютно разные подходы для решения этих вопросов. У Racket есть макросы, а у Haskell его навороченная система типов. Обе эти концепции на световые расстояния далеки от того, где я была десять лет назад, строча повторяющийся Java-код, который в результате производил очень мало, а мои задачи не были решены.

Racket так мало знает о моей программе — он не может понять, что я подразумеваю, основываясь на типе того, чем я оперирую, потому что он (почти) динамически типизированный. Я до сих пор должна разбираться сама и записывать те вещи, которые кажутся лишними, потому что компьютер не достаточно умный, чтобы выяснить "очевидное". Точно так же Haskell очень ограничен — компилятор не может проследить происхождение условий, которые я могу решить в уме за секунды, а его синтаксис не расширяемый, как у Racket. Каждый день я всматриваюсь в нагромождения монадных вычислений, и правда — чего я достигла?

Совершенствоваться, но никогда не владеть

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

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

С другой стороны, языки вроде Haskell и Racket склонны размывать границу. Мне кажется, что у меня довольно чёткое понимание ядра Haskell, но насколько я разбираюсь в ленивых вычисления? Осознаю ли я полноценно семейства типов? Что насчёт TypeInType? Я вынуждена признать, что не полноценно понимаю Haskell, а ещё слабее — большую часть расширенной теории категорий, с помощью которой сформированы некоторые из его самых мощных библиотек. Racket умудряется размывать границу между языком и библиотекой ещё сильнее, и хоть я и вижу себя приличным Racket-пользователем, я не имею никакого чёткого понимания всей запутанности макросов Racket-системы.

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

Каждый раз, когда я взбираюсь на следующую ступень абстракции, все, кто чуть ниже меня (хотя мы все — на огромной высоте!) оказываются в замешательстве. В худшем случае люди могут считать, что причина их замешательства в их собственной неадекватности или недостатке опыта. Это ужасно, особенно когда я знаю об этом, а к тому времени, когда их непонимание вскроется, я уйду играть с новой игрушкой: комонадами, типами семейств или classy lenses. Цикл продолжится и никто никогда не будет по-настоящему удовлетворён — я всегда буду искать новую абстракцию, которая упростит задачу, и те, кто в паре шагов от меня будут еле справляться.

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

В конце-концов всё это имеет (хотя бы небольшое) значение

Было бы здорово подумать об этом и сказать: "Давайте, наконец, разорвём порочный круг. Давайте прекратим обманывать себя, что способы решения выдуманных нами проблем, решают что-то в действительности". Было бы отлично, если бы я могла себе это сказать… но, к сожалению, не могу.

Самый страшный момент — я думаю, что всё это действительно того стоит.

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

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

Большинство программистов, даже те, которые не видели BASIC, могут разобраться, что делает этот отрезок кода:

10 INPUT "What is your name: "; U$
20 PRINT "Hello "; U$

С другой стороны некоторые, вероятней всего, не смогут понять этот:

-- | A class for categories.
--   id and (.) must form a monoid.
class Category cat where
    -- | the identity morphism
    id :: cat a a

    -- | morphism composition
    (.) :: cat b c -> cat a b -> cat a c

Меньшинство новых программ написаны на BASIC и множество на Haskell.

Даже один из самых популярных, быстрорастущих языков программирования в мире, JavaScript, язык, который рассматривается относительно доступным в сравнении с Haskell, вряд ли может быть понятным для программиста незнакомого с его синтаксисом:

export const composeWithProps = curry((a, parentProps, b) => {
  const composed = childProps =>
    createElement(a, parentProps, createElement(b, omit(['children'], childProps), childProps.children));
  // give the composed component a pretty display name for debugging
  composed.displayName = `Composed(${getDisplayName(a)}, ${getDisplayName(b)})`;
  return composed;
});

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

Именно эта деталь — суть моего страха: всегда ли мы думаем о том, для кого делаем оптимизацию? У меня нет моральной проблемы с написанием ёмкого кода для закалённых программистов, в конце-концов краткость — один из основных способов делать код более удобным для чтения (многословность — враг понимания). Однако, когда эта ёмкость стоит понимания новичков, всё обесцвечивается. Нет ничего неправильного в том, чтобы писать высоко-оптимизированный код для саморазвития и понимания, а группа способных на это людей может образовать очень продуктивную команду. Но важно понимать, что другие скорее всего не поймут и без желания тратить время и деньги на образование, умные исполнительные люди будут с трудом вникать в концепции и скорее всего мало будут в них заинтересованы.

Реактивный антиинтеллектуализм и поиск умеренности

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

Математическая нотация — не идеальна, не идеальней плотного Haskell, тяжело метапрограммного Руби, или полного IIFE JavaScript. Но она служит определённой цели и иногда подробное разъяснение не принесёт ни практически осуществимого, ни теоретического улучшения. Программисты не воспримут мягко, если их попросить написать весь код в стиле прозы, и им не понравится, если им скажут, что использование функций высокого порядка вроде map должно быть запрещено, потому что они слишком сбивают с толку и не достаточно самоочевидны.

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

Абстракция — это то, что делает программирование возможным и безусловно это то, что делает большую часть современных технологий возможными. Это то, что позволяет людям вести машину, не зная как работает двигатель внутреннего сгорания и что даёт возможность просматривать веб-страницы не разбираясь детально в работе сетевых протоколов. В программировании абстракция служит той же цели. Конечно, как и у всех инструментов, у абстракции могут быть разные цели: среднестатистический пользователь не освоит Photoshop за день, но продвинутый пользователь не удовлетворится Paint.

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

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

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