Разработка

Язык для программирования

Это перевод статьи Артёма Чистякова "The language of programming", породившей интересные дискуссии на HackerNews и Reddit.

Я помню, как изучал свой первый язык программирования. Мы должны были освоить какой-то из диалектов BASIC в рамках обязательной школьной программы по информатике для второго класса. Скрючившись на своих партах под тусклыми флуоресцентными лампами, мы нетерпеливо поглядывали на жужжащие компьютеры IBM, расставленные вдоль стен душной классной комнаты. Это был 1997 год, Россия. Ни у кого из нас не было домашнего компьютера. На доске в меловых разводах учитель написала:

SCREEN 15, 0
PSET (100, 100)
DRAW "R20 D20 L20 U20"
END

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

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

🔲
➡️ 100
⬇️ 100
🖌 "➡️ 20 ⬇️ 20 ⬅️ 20 ⬆️ 20"
🏁

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

Абстрактное программирование

Ещё десять лет, и я — студент, изучающий алгоритмы и структуры данных. Мы пишем на C/C++, и все в какой-то степени знаем английский. Но все наши конспекты на русском, а стандартные названия библиотечных функций представлены без каких-либо намёков на их оригинальные значения. На вступительной лекции профессор представляет "Hello, World!" и другие простые программы на С. Он произносит каждое название функции медленно, записывая его на доске. В его интерпретации getch() звучало как "гэт-чэ", а clrscr() — "ке-ле-эр-эс-хе-йер". Сейчас в это сложно поверить, но перед тем, как во мне что-то щёлкнуло и я увидел "get character" и "clear screen", прошло время. Потом я поделился тем, что заметил со своими однокурсниками, и они были так же удивлены.

Заглядывая в прошлое, я теперь понимаю, как порванная с языком-оригиналом связь лишила нас чего-то полезного. Хотя, такой метод изучения языка программирования — наглядный, потому что он очень близок к абстрактной математике. Это он побудил нас видеть char *strstr(const char *haystack, const char *needle) как Ω(x,y). Даже если функция имела запутывающее название, никто из нас не замечал, потому что оно не слишком отличалось от другой случайной лексемы, которую мы должны были запомнить. Когда вы программируете таким способом, документация — царь, а названия — просто ссылки на концепты, лежащие в основе.

Говоря о названиях, в своих собственных программах мы использовали математическую нотацию, смешанную с латинизированными русскими словами. Благодаря краткости и общей загадочности такого кода, профессиональные программисты в России называют это "говнокодом". Но так часто выглядели программы, написанные в академии. И назло профессионалам они работают точно так же, как "чистый" код.

Пример моего "говнокода" из 2006.

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

  • Изучение иностранного языка — сложно. В противовес распространённому мнению, английский выучить нелегко.
  • Связь программирования и присвоения имён очень сильна, даже в сравнении с другими сферами знаний.

Давайте рассмотрим каждую причину.

Английский — сложный

В заметке "Английский был для меня болезненной темой 15 лет" создатель Redis, Сальваторе Санфилиппо (Salvatore Sanfilippo) рассказывает о многолетней борьбе с попытками выражать мысли на английском. Пост написан человеком, который построил один из фундаментальных элементов современного комплекса технологий для веб:

...мы разрабатывали новые TCP/IP атаки, но не были способны, чёрт возьми, написать об этом пост на английском. Это было в 1998, и я уже чувствовал себя невероятно ограниченным тем, что не мог общаться, читать техническую документацию, написанную на английском и не убивать моральные силы процессом чтения… так что мой мозг использовал примерно 50% энергии только на чтение, а ещё меньше оставалось на понимание того, что я читаю.

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

Я догадываюсь, что в среднем относительно крупный API, собранный носителем английского, будет более выразительным, чем такой же от не носителя со средним уровнем. Я практически убедился в этом, когда впервые столкнулся с Clojure. Рич Хики (Rich Hickey) известен своим продуманным использованием словаря. В Clojure простой *примитивный (simple) это не лёгкий *нетрудный (easy), коллекции (collections) это не последовательности (sequences), а функции с именами вроде reify или transduce — обычное дело.

Даже вооружившись тезаурусом или обратным словарём, носитель иностранного языка будет мучиться, чтобы соответствовать семантическому мастерству Рича. Но в реальности можно убедиться, что Google Translate и соглашения по именованию могут неплохо помочь и случайные ошибки, вроде  isHidedvisiblesunexisting — не критичны для звёздочной оценки на GitHub.

В целом, неанглоязычный может уверенно называть разные объекты на английском, но у него возникнут сложности с описанием специфического качества предмета, или с отчётливым присвоением конкретного типа взаимодействия. Представьте маленьких детей, которые учатся говорить. Они с лёгкостью назовут множество предметов в доме, но не будут использовать слова, вроде "сопоставлять" (juxtapose) или "перемежаться" (intersperse) для описания комплексного действия или называть что-то, что в случайном порядке включается и выключается словом "скачкообразный" (intermittent) (я правильно написал?..).

Имена на каждом шагу

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

Программисты могут всячески изощряться, когда дают названия разным объектам. Шеф (Chef) — фреймворк по управлению конфигурацией, использует целый набор кулинарных метафор: книга с рецептами, кухня, нож и другое. Шторм (Storm) — интерактивная вычислительная система, использует в работе такие сущности, как потоки, смерчи и молнии. С менее практичной стороны, Heroku (облачная платформа) развлекает всех сгенерированными серверными хостовыми названиями, вроде “flexile-sentry.heroku.com”. Некоторые спорят о том, на самом ли деле метафоры и забавные названия помогают разработчику совершенствоваться или просто запутывают лежащие в основе концепты, но это выходит за рамки темы этой статьи. Суть в том, что разработчики проводят много времени, придумывая имена разным штукам. Иногда, я бы сказал, это отрицательно влияет на их продуктивность.

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

Из руководства “How to use layers in Illustrator”.

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

Джулия работает над иллюстрацией.

Illustrator не просит называть заранее файл и не ругается, когда у вас есть два слоя с одинаковыми именами. Начинаешь с названных автоматом элементов и переименовываешь их, когда потребуется. Теперь сравните это с генерацией свежего приложения в Ruby on Rails. Требуемое имя аргумента используется для модуля приложения, ключа сеанса и вставляется в сгенерированные шаблоны.

$ rails new experiment-2
...

$ grep -r -i experiment .
./app/views/layouts/application.html.erb:  <title>Experiment2</title>
./config/application.rb:module Experiment2
./config/initializers/session_store.rb:Rails.application.config.session_store :cookie_store, key: '_experiment-2_session'

Если позже вы решите переименовать experiment-2 в SpaceElevator — удачи. Даже самый продвинутый IDE в Ruby, RubyMine, не может провернуть такой трюк как простое (казалось бы) переименование проекта. И это только верхушка айсберга. Если вы думаете, что "бриллианты вечны", вам никогда не приходилось объяснять схему 10-летней базы данных новому работнику. Худшие названия никогда не оставят вас в покое.

Ну вот, теперь всё наглядно: присваивать имена — сложно. Ещё сложнее на чужом языке. Менять названия позже — ещё хуже, а иногда практически невозможно. Как с этим справляться? Меня интересуют конкретно две проблемы:

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

Меньше имен = меньше именований

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

  1. Структура функции
  2. Сокращённая форма синтаксиса для лямбда-функций.
  3. Соглашение о наименованиях и "математическая" нотация.

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

(defn frequent-terms
  "Split s into a sequence of lower-case terms, remove articles
   and punctuation, return only terms that occur more than n times."
  [s n]
  (->> s
       clojure.string/lower-case
       (re-seq #"\w+")
       (remove #{"a" "an" "the"})
       frequencies
       (keep #(when (> (val %) n) (key %)))))

(frequent-terms
  "How much wood would a woodchuck chuck if a woodchuck could chuck wood.
   As much wood as a woodchuck would if a woodchuck could chuck wood."
  3)

; => ("woodchuck" "wood")

Чтобы реализовать эту довольно сложную функцию, мне нужно было придумать единственное имя: имя функции. Я назвал вводную строку s, потому что есть соглашение, используемое всеми строковыми функциями в Clojure и, следуя другому общему соглашению, n обозначает единственное число, принимаемое функцией. Собирая несколько функций более высокого порядка, я избегаю явную итерацию и её побочные продукты: индексы и временные переменные. Даже когда я ввожу лямбда-функцию (#(when (> (val %) n) (key %))), синтаксис Clojure не требует назвать её аргумент, позволяя обращаться к нему с помощью символа % (к нескольким аргументам может быть доступ через %1%2 и так далее).

Некоторые композиции функций очень сложно выразительно (наглядно) назвать. Математики избежали этой проблемы, встроив греческий алфавит в свою стандартную нотацию. Хоть я бы и не рекомендовал составлять свой API из греческих букв, локальная вспомогательная функция часто может обозначаться как f или g без особого ущерба, как в примере ниже.

(letfn [(f [x] (Math/pow (Math/sin x) 2))]
  (f (transduce (map f) + [1 2 3 4 5])))

Какого x?

Я считаю, что Clojure имеет почти идеальный баланс между лаконичностью и экспрессивностью, но должен признать: я был смущён этими "математическими" названиями не раз. Пока вы не познакомитесь с идиомами наименований в Clojure, понимать смысл определения функций может быть болезненным. Я бы всё же заменил sn, и f на stringmin-occurrences, и squared-sine, но ради эксперимента давайте рассмотрим, как ещё можно усовершенствовать удобное чтение, а не сдаться и просто дать всему кустарные названия.

Очевидный ответ — мы можем заложить мета-информацию. Возвращаясь к примеру в Illustrator: сгенерированные названия слоёв различны для разных типов объектов. В языках программирования эту роль могут (в какой-то степени) играть указания типов и аннотации. Давайте посмотрим на программу, написанную на Haskell:

repeat n x | n > 0 = x : repeat (n - 1) x
           | otherwise = []

Функция repeat принимает число n и значение x и возвращает список, содержащий n копий x. Haskell обычно не требует у вас уточнить типы, но вы можете решить добавить информацию о типах, чтобы описать свои предположения компилятору и другим людям, читающим ваш код. Описание типа данных для функции repeat может выглядеть так:

;         n      x    (return value)
repeat :: Int -> a -> [a]

Эта сигнатура типа сообщает читающему, что n — это целое число,  x — значение любого типа a, а возвращаемое значение — это список значений с типом a. Хотя существует бесконечное множество различных реализаций  repeat, соответствующих этой сигнатуре типа (например, возврат a повторов n + k, где k ∈ ℕ), компилятор Haskell может поймать множество недопустимых реализаций. Например, при строковой x функция не сможет создать список кортежей или вернуть случайное значение (или считать возвращённое значение из файла). Всё это проверяется до запуска программы. На первый взгляд это может показаться беспроигрышной ситуацией, но реальность не чёрно-белая.

В теории, чем сильнее типовая система языка, тем больше информации доступно статически, а поэтому для такого языка можно создать более мощные инструменты рефакторинга и проверки кода. Конечно, инструменты Java, Scala и C#, встроенные в IDEA и Visual Studio, более надёжны, чем их Ruby-аналоги. В то же время парадокс в том, что разработчики на Haskell стараются воздерживаться от использования любых инструментов, кроме компилятора, а лисперы получают многие из тех же преимуществ (и некоторые другие), подключая свои редакторы к интерактивной среде (REPL). Система типов не делает магическим образом код в «математической» нотации самодокументированным. Хоть это и лучше «простого именования». Например, сравните следующее определение С-функции strstr, ищущей подстроку, с другой:

# 1.
char * strstr(const char *a, const char *b);

# 2.
char * strstr(const char *haystack, const char *needle)

Вариант 2 сразу делает порядок аргументов понятным любому англоговорящему, знакомому с идиомой “needle in a haystack” (иголка в стоге сена). В то же время, мы снова возвращаемся к проблеме, когда 1 и 2 одинаково озадачивают человека, не владеющего английским.

Ограничения обычного текста

Куда нас это приводит? Функциональное программирование и продвинутые системы типов безусловно сокращают количество наименований, которые нам нужно выдумывать и запоминать, чтобы создавать полезные программы. Но всегда существуют функции, вроде strstr, которые выглядят загадочно, если вы не знаете контекста естественной языковой семантики или невнимательно читаете документацию. Англоговорящий в этот момент, возможно, поинтересуется, почему бы этим несчастным иностранцам не написать языки программирования с их собственным словарём или алфавитом? Такое существует, но подобные попытки обречены на изоляцию в профессиональной сфере, построеной на интеграции идей.

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

Кириллические имена функций в формулах MS Excel

Не буду врать, это выглядит возмутительно даже для меня. Но не для моего отца, инженера-строителя, который ни слова не знает на английском. Он пугающе разборчив в формулах Excel и широко использует их в документах из сотни страниц, обросших фильтрами, условиями и пивот-таблицами. Позже по этим расчётам строятся дороги и мосты. Он не знает, что означает IF, но постоянно использует ЕСЛИ. Что круто, так это если он отправит вам одну из этих электронных таблиц, и вы откроете её в оригинальном MS Excel, каждая формула отобразится на английском и будет работать именно так, как было задумано отцом.

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

Некоторые программисты встают в защитную позу, когда дело касается обсуждения альтернатив обычному тексту. В "библии" многих современных программистов, книге "Прагматичный программист" (The Pragmatic Programmer), которую я читаю и которой делал ревью в своём блоге, описанию простого текста, как сюрикэна прагматичных программистов, отведён целый раздел. Но чем больше распространяются продвинутые IDE (которые уже скрывают некоторые детали реализации) и всё шире адаптируются нетекстовые программные среды (вроде MIT Scratch или Unreal Blueprints), кажется, всё более и более правдоподобным, что такой практичный инструмент программирования для каждого, который объединит все эти идеи, уже не за горами.

Подытожим

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

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

Пожалуй, существенные изменения в этой области требуют выхода за рамки традиционных инструментов программирования (например, текстовых редакторов и CLI) и движение в сторону исключительно визуальных, интерактивных окружений. Как бы абсурдно это ни звучало, чтобы программирование стало "инструментом мышления", как его пропагандируют, оно должно учитывать уроки Excel и Illustrator, оно должно оставаться открытым к новым идеям, а не отвергать их как "непрактичные". Другими словами наши компьютеры должны обучаться универсальному языку — эмпатии.

Перевод: Наталия Басс


От редактора: Мы на Хекслете часто публикуем переводы статей. Важно помнить:

  1. Мнение автора статьи может отличаться от мнения администрации и сотрудников Хекслета.
  2. Цель перевода – показать мнение. Поэтому одна статья может визуально противоречить другой: это просто разные мнения. Мы оставляем на вашу ответственность возможность анализировать и делать выводы для себя.
Natalia Bass 19 июля 2017
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →