Пол Грэм, «Хакеры и художники», глава 10: «Programming Languages Explained»
Книга «Хакеры и Художники», глава 10.
Эта глава есть только в книге, на сайте Пола Грэма она отсутствует.
Кстати, осталось совсем чуть-чуть и будет готова последняя глава книги, тем самым будет полный русский хабраперевод «Хакеров и художников». (Предыдущий перевод — Пол Грэм: «The Other Road Ahead».)
За помощь с переводом спасибо Щекотовой Яне.
Языки программирования «в разрезе»
У любой машины есть набор действий, которые на ней можно выполнить.
Иногда этот набор сильно ограничен. Со своим электрическим чайником я могу совершать только два действия: включать и выключать его. Мой CD проигрыватель будет уже посложнее. Помимо включения и выключения, я могу повышать и убавлять на нем громкость звука, воспроизводить и приостанавливать песни, перейти к следующей или предыдущей композиции, а также воспроизводить все это в случайном порядке.
Как и любое другое устройство, у компьютера есть список действий, которые он может выполнить. Например, на каждом компьютере можно сложить два числа. Полный перечень допустимых операций компьютера заключен в его машинном языке.
Машинный язык
Когда были изобретены компьютеры, все программы приходилось писать как последовательность машинных инструкций. Вскоре, их начали писать в несколько более удобной форме под названием язык ассемблера. В языке ассемблера список команд тот же самый, только используются более удобные для программистов имена. Вместо обращения к инструкции сложения как 11001101, хотя именно так машина ее и вызывает, вам нужно написать add.
Проблема с машинным языком/языком ассемблера состоит в том, что большинство компьютеров могут выполнять только очень простые вещи. Например, предположим, что вам нужно, чтобы компьютер воспроизвел короткий звуковой сигнал 10 раз. Сомневаюсь, что у компьютера есть инструкция для осуществления какого-либо действия n раз. Поэтому, если бы вам понадобилось, чтобы компьютер что-то сделал 10 раз, используя настоящие машинные инструкции, вам бы пришлось написать нечто похожее на:
put the number 10 in memory location 0
a if location 0 is negative, go to line b
beep
subtract 1 from the number in location 0
go to line a
b …rest of program…
Если вам приходится выполнять столько работы только для того, чтобы заставить машину пропикать 10 раз, то только представьте, сколько труда уходит на написание чего-то наподобие текстового процессора или программы для работы с электронными таблицами.
И, кстати, взгляните еще раз на программу. Она и вправду воспроизведет короткий звуковой сигнал 10 раз? Неа, 11. В первой строке мне надо было указать 9, а не 10. Я намеренно поместил в наш пример ошибку, чтобы продемонстрировать важный аспект по поводу языков. Чем больше вам приходится писать для выполнения тех или иных действий, тем сложнее разглядеть ошибки.
Высокоуровневые языки
Представьте, что вам нужно создавать программы на языке ассемблера, но у вас есть некий помошник для выполнения всякой «грязной» работы. Поэтому вы можете просто написать что-то типа
do times 10 beep,
а ваш помощник напишет это за вас на языке ассемблера (но без ошибок).
На самом деле, именно так большинство программистов и работает. За исключением того, что помощник — это не человек, а компилятор. Компилятор — это программа, которая переводит программы, написанные в удобной форме, как та, однострочная, о которой говорилось выше, в простой язык, понятный для аппаратного обеспечения.
Этот более удобный язык, который вы «скармливаете» компилятору, и называется высокоуровневым. Он позволяет составлять программы на основе мощных команд, таких как «do что-то n раз» вместо пресловутого «add два числа.»
Когда вы создаете программы на основе более обобщенных концепций, вам не нужно использовать их все. Длина программы, написанной на нашем выдуманном высокоуровневом языке, составляет всего лишь пятую часть от длины исходной. И если в ней была допущена ошибка, то ее будет легко найти.
Еще одним преимуществом высокоуровневых языков является то, что они делают вашу программу более переносимой. На разных компьютерах машинные языки немного отличаются. Как правило, нельзя взять программу, написанную на машинном языке для одного компьютера, и запустить ее на другом. Если вы пишете программы на машинном языке, вам придется их все переписать для того, чтобы иметь возможнось запускать их на новом компьютере. Если же вы используете высокоуровневый язык, то все, что нужно будет переписать, это компилятор.
Но компиляторы не единственные, кто может работать с высокоуровневыми языками. Вы также можете воспользоваться интерпретатором, который просматривает по одному сегменту программы за раз и выполняет соответствующие машинные команды, вместо того, чтобы сразу перевести весь кусок в машинный язык и запустить его.
Open Source
Высокоуровневый язык, который вы «скармливаете» компилятору, также известен как исходный код, а результат его трансляции на машинный язык называется объектным кодом. Когда вы покупаете коммерческое ПО, вам обычно предоставляется только объектный код. (Объектный код так сложно читать, потому что он хорошо зашифрован для защиты коммерческой тайны компании). Но в последнее время появился альтернативный подход: ПО с открытым исходным кодом (open source), когда вам предоставляется исходный текст программы, который при желании можно модифицировать.
Разница между этими двумя моделями существенна. Open source предоставляет вам куда больше контроля. Когда вы пользуетесь открытым ПО и хотите понять, что оно делает, то вы можете прочесть исходный код и выяснить это. А при желании, вы можете даже внести изменения в программу и перекомпилировать ее.
Одной из причин, по который вам захочется это сделать, является исправление бага. Например, вы не можете исправить ошибки в Microsoft Windows, потому что у вас нет исходного кода. (Теоретически вы могли бы взломать объектный код, но практически это довольно сложно осуществить. А также это запрещено лицензионным соглашением). Это может стать настоящей проблемой. При обнаружении новой дыры в защите Windows, вы вынуждены ждать, когда Microsoft выпустит исправления. А дыры в защите по крайней мере быстро исправляют. Если этот баг время от времени парализует ваш компьютер, то, возможно, вам, придется подождать, пока не будет выпущен следующий релиз с исправлениями.
Но преимущества open source не сводятся только к тому, что у вас есть возможность внести исправления при необходимости. Это могут все. ПО с открытым исходным кодом — это как документ, представленный для экспертной оценки. Многие умные люди изучили исходники открытых операционных систем Linux и FreeBSD и уже нашли большинство ошибок. В то время как Windows надежна ровно настолько, насколько это может обеспечить отдел QA в больших компаниях.
Приверженцев open source иногда воспринимают как чудиков, выступающих против самой идеи права собственности в целом. Некоторые таковыми и являются. Но я точно не выступаю против этого права, а ведь я бы очень неохотно устанавливал ПО, исходных кодов которого у меня бы не было. Среднему конечному пользователю может и не понадобится исходный код его текстового процессора, но когда вам действительно нужна надежность, то это уже веская причина для того, чтобы использовать open source.
Языковые войны
Большинство программистов, бОльшую часть времени, пишут на языках высокого уровня. Лишь некоторые используют сейчас язык ассемблера. Компьютерное время сильно подешевело, в то время как время программиста дорого как никогда, поэтому редко возникают ситуации, когда время, потраченное на решение проблем, возникших при написании программ на языке ассемблера, окупается в дальнейшем. Это порой необходимо в некоторых критических частях, например, компьютерной игры, где вам нужно управлять оборудованием на микроуровне, чтобы выжать последние крупицы мощности для ускорения.
Fortran, Lisp, Cobol, Basic, C, Pascal, Smalltalk, C++, Java, Perl, и Python — все это высокоуровневые языки. И это только те, которые наиболее известны. Сущесвуют буквально сотни различных языков высокого уровня. И в отличие от машинных, которые все предлагают сходный набор инструкций, эти высокоуровневые языки предоставляют вам довольно разные принципы для построения программ.
Так какой же использовать? Ну, по этому поводу ведется куча споров. Часть проблемы кроется в том, что если вы пользуетесь языком довольно длительное время, вы начинаете на нем думать. Поэтому любой значительно отличающийся язык, кажется жутко неповоротливым, даже если в сущности в нем нет ничего неправильного. Зачастую суждения неопытных программистов об относительных преимуществах языков программирования искажаются под действием этого впечатления.
Некоторые компьютерщики, возможно, из желания показаться более опытными, скажут вам, что все языки по сути своей одинаковы. «Я программировал на языках всех типов, — сказал суровый старый хакер за стойкой бара, — и неважно, каким пользоваться. Что действительно важно, так это правильно ли ты мыслишь.» Или что-то в этом духе.
Конечно же, это чушь. Существует огромная разница между, скажем, Fortran I и последней версией Perl. Или, если уж на то пошло, между ранней и последней версиями Perl. Но суровый старый хакер может и сам верит в то, что говорит. Можно писать одни и те же примитивные паскалеподобные программки практически на каждом языке. Если вы питаетесь только в McDonald«s, то вам покажется, что еда в каждой стране одна и та же.
Некоторые продвинутые программисты предпочитают тот язык, к которому привыкли, и недолюбливают все остальные. Другие говорят, что все языки одинаковы. Но истина находится где-то посередине между этими двумя крайностями. Языки действительно отличаются друг от друга, но сложно сказать наверняка, какие из них лучше. Ситуация все еще находится в развитии.
Уровень абстракций
Как высокоуровневые языки являются более абстрактными по отношению к языкам ассемблера, так и некоторые из этих высокоуровневых языков более абстрактны, чем другие. Например, C довольно низкоуровневый, практически переносимый язык ассемблера, в то время как Lisp обладает очень высоким уровнем абстракций.
Если программировать на высокоуровневых языках лучше, чем на языке ассемблера, то можно ожидать, что чем выше уровень языка, тем он лучше. Обычно да, но не всегда. Язык может быть очень абстрактен, но предлагает не те абстракции. Думаю, что именно это и происходит, например, в Prolog. В нем реализованы невероятно мощные абстракции для решения около 2% задач, а все остальное время вы очень усердно стараетесь использовать эти абстракции неподобающим образом, и пишете программы как на языке Pascal.
Еще одна причина, по которой вам бы захотелось использовать низкоуровневый язык, это продуктивность. Если вам нужен супер-быстрый код, то лучше придерживаться машинных инструкций. Большинство операционных систем написаны на C, и это не совпадение. Поскольку аппаратное обеспечение становится все быстрее, то создавать приложения на языках такого же низкого уровня как С уже не так критично и важно, но всем все еще хочется, чтобы операционные системы работали как можно быстрее. (Или, вероятно, всем хочется всегда быть начеку на случай возможной атаки на переполнение буфера.[1])
Ремни безопасности или наручники?
Самый большой спор в области проектирования языков, вероятно, тот, который ведется между теми, кто думает, что язык должен ограждать программистов от совершения глупостей, и тех, кто полагает, что программистам должно быть разрешено делать то, что они захотят. Java относится к первому лагерю, а Perl — ко второму. (И неудивительно, ведь министерство обороны США помешано на Java.)
Сторонники либеральных языков высмеивают лагерь оппонентов, называя такие языки «B&D» (bondage and discipline — рабство и дисциплина), с довольно дерзким намеком на то, что те, кому нравится программировать на таких языках, просто напросто неудачники. Я не знаю, как противоборствующая сторона называет языки подобные Perl. Возможно, они не из тех людей, которые придумывают обидные прозвища для оппонентов.
Данная полемика сводится к нескольким более мелким, потому что существует несколько способов оградить программистов от совершения глупостей. Одним из наиболее активно обсуждаемых в данный момент вопросов является статическая и динамическая типизация. В языках со статической типизацией вы должны знать типы значений, которые может принимать каждая переменная во время написания программы. В динамической типизации, можно любой переменной присвоить любое значение, какое только захотите.
Приверженцы статической типизации аргументируют ее тем, что она способствует предотвращению ошибок и помогает компиляторам генерировать быстрый код (оба довода верны). Приверженцы динамической типизации считают, что статическая типизация ограничивает вас в том, какие программы можно писать (что также верно). Я предпочитаю динамическую типизацию. Ненавижу язык, который указывает мне, что делать. Но, кажется, некоторым умным людям нравится статическая типизация, поэтому вопрос все еще остается открытым.
OO
Еще одной большой темой на данный момент является объектно-ориентированное программирование. Оно представляет собой другой способ организации программ. Предположим, вы хотите написать программу для нахождения площади двумерных фигур. Сначала программа будет иметь представление только о круге и квадрате. Одним из способов реализации будет написание одного куска кода, в пределах которого вы проверяете, что у вас запрашивают: круг или квадрат. А затем используете соответвствующую формулу для поиска площади. Объектно-ориентированный способ написания программ подразумевает создание двух классов, круг и квадрат, а затем привязать к каждому классу фрагмент кода (который называется метод) для нахождения площади данного типа фигуры. Когда вам нужно вычислить площадь чего-либо, вы спрашиваете, к какому классу это что-то принадлежит, извлекаете соответствующий метод, и запускаете его, чтобы получить результат.
Эти два случая могут показаться очень схожими. И в самом деле, то, что в действительности происходит при запуске кода, довольно похоже. (Неудивительно, т.к. вы решаете одну и ту же задачу) Но код в итоге может выглядеть по-разному. В объектно-ориентированной версии, код расчета площади квадрата и круга может даже быть представлен в разных файлах, один файл содержит все, что связано с кругом, а другой — все, что связано с квадратом.
Преимущество объектно-ориентированного подхода состоит в том, что, если вы захотите изменить программу для расчета площади, например, треугольника, то вы просто добавите еще один кусок кода для этого, и вам даже не нужно будет думать обо всем остальном. Недостаток, как заявили бы критики, заключается в том, что добавление без учета того, что уже реализовано, приведет в программировании к таким же результатам, как и в строительстве.
Споры по поводу ОО программирования не такие ясные как по поводу статической и динамической типизации, где вам просто приходится выбирать одну из двух. А при обсуждении объектно-ориентированности языка можно говорить только о ее степени. И действительно, существует два значения ОО: некоторые языки объектно-ориентированы, т.к. они позволяют программировать в этом стиле, а другие, т.к. они вынуждают вас так делать.
В последнем варианте мало преимуществ. Несомненно, язык, который позволяет сделать x по крайней мере так же хорош, как и тот, что вынуждает вас делать таким образом. Поэтому, касательно языков, мы, по крайней мере, можем обойти этот вопрос. Конечно, пользуйтесь языком, который позволяет писать ОО программы. А хотите ли вы этого на самом деле — это уже другой вопрос.
Возрождение
Я думаю, все, кто связан с языками, согласятся с тем, что в последнее время появилось огромное количество новых языков программирования. До 1980 года, только университеты могли позволить себе оборудование, необходимое для разработки таких языков, и поэтому многие языки были спроектированы профессорами или исследователями в крупных компаниях. Сейчас даже школьник может позволить себе нужное «железо».
Вдохновленные в основном примером Ларри Уолла, создателя Perl, многие компьютерщики думают, почему я не могу спроектировать свой собственный язык? Те, кому удается обуздать мощь open source сообщества, обладают доступом к огромному количеству кода, написанного в сжатые сроки.
В результате появляется такой тип языка, который можно назвать неустойчивым: язык, чье ядро не очень хорошо спроектировано, но который обладает чрезвычайно мощными библиотеками для решения специфических задач. (Представьте автомобиль марки «Zastava» (также известный как «Yugo») с реактивным двигателем, прикрученным к крыше). Для незначительных, ежедневных задач, на решение которых программисты тратят уйму времени, библиотеки, возможно, важнее ядра языка. Поэтому такие странные гибридные решения довольно полезны, и, соответственно, становятся популярны. Автомобиль «Zastava» с прикрученным к крыше реактивным двигателем может быть и работал бы, пока вы не попытались бы выполнить на нем поворот [2].
Также в результате мы получим огромное разнообразие, которое всегда наблюдалось в программных языках. Fortran, Lisp, and APL отличаются друг от друга так же сильно, как морская звезда, медведь и стрекоза, а ведь все были созданы до 1970. А новые открытые языки естественно продолжили эту традицию.
Кажется, что я узнаю о новом языке каждые два дня. Джонатан Эриксон назвал это «эпохой возрождения языков программирования». А еще люди иногда употребляют фразу «языковые войны». Но противоречия здесь нет. В эпоху Возрождения произошла куча войн.
И действительно, многие историки считают, что эти войны были побочным продуктом тех сил, что создали эпоху Возрождения [3]. Причиной этих сил в Европе, возможно, был тот факт, что она была разделена на некоторое число мелких, конкурирующих государств. Они располагались достаточно близко друг к другу, чтобы идеи могли кочевать из одной в другую, но в то же время были достаточно независимы, чтобы ни одному правителю не удалось наложить запрет на нововведения, как однажды это сделал суд Китая, запретивший разработку больших океанских кораблей.
Поэтому, возможно, все это и к лучшему, что программисты живут в постбиблейском мире. Если бы мы все использовали один и тот же язык, он бы наверняка был не самым лучшим.
Примечания к главе 10
1 Самый распространенный способ взлома компьютеров использует некоторые особенности языка C. В C, когда вы отводите кусок памяти (буфер) для входных данных, вы предполагаете, что он будет размещен рядом с памятью, содержащей адрес возврата текущего выполняющегося кода. Адрес возврата — это область в памяти кода, который будет выполняться, когда текущий код завершит свою работу. Это, по сути, следующий пункт в списке дел компьютера.
Поэтому если кто-то захочет взломать ваш компьютер, и узнает, что вы используете 256-байтный буфер для хранения некоторых входных данных, тогда, просто посылая больше 256 байт, он может переписать адрес возврата. Когда текущий код завершится, управление перейдет к любой области памяти, которая была заранее определена злоумышленником. А область, которую он определит, будет началом буфера, содержащего программу на машинном языке на его выбор. Бинго: теперь на вашем компьютере выполняется его программа.
В высокоуровневых языках это было бы невозможно, но в C, каждый раз, когда вы принимаете входные данные извне и не проверяете на допустимую длину, вы создаете брешь в защите. Атака, которая использует данную уязвимость, называется атакой на переполнение буфера.
Существуют и другие способы получения доступа к управлению компьютером через атаку на переполнение буфера, но перезапись адреса возврата — это классика.
Любопытно, что угоны самолетов также являются атаками на переполнение буфера. В типичном авиалайнере, помещение для пассажиров и кабина пилота расположены рядом, прямо как области данных и кода расположены в смежных областях в программе на С. Осуществляя «переполнение» в сторону кабины пилота, угонщики в действительности способствуют переходу из области данных в область кода.
2 Заметка для хакеров: это всего лишь метафора. Не пытайтесь сесть за руль машины «Zastava» с прикрученным к крыше реактивным двигателем. Возможно, феномен реактивной машины далеко не новый. Fortran также обязан своей популярностью, в основном, своим библиотекам.
3 Cipolla, Carlo, Guns, Sails, and Empires: Technological Innovation and the Early Phases of European Expansion 1400–1700, Pantheon, 1965.
Хакеры и художники
- Why Nerds Are Unpopular
Their minds are not on the game.
оригинал, перевод часть 1, часть 2 - Hackers and Painters
Hackers are makers, like painters or architects or writers.
оригинал, перевод часть 1, часть 2, альтернатива - What You Can’t Say
How to think heretical thoughts and what to do with them.
оригинал, перевод - Good Bad Attitude
Like Americans, hackers win by breaking rules.
оригинал, перевод - The Other Road Ahead
Web-based software offers the biggest opportunity since the arrival of the microcomputer.
оригинал, перевод часть 1 - How to Make Wealth
The best way to get rich is to create wealth. And startups are the best way to do that.
оригинал, перевод - Mind the Gap
Could «unequal income distribution» be less of a problem than we think?
оригинал, перевод - A Plan for Spam
Till recently most experts thought spam filtering wouldn’t work. This proposal changed their minds.
оригинал, перевод - Taste for Makers
How do you make great things?
оригинал, перевод - Programming Languages Explained
What a programming language is and why they are a hot topic now. - The Hundred-Year Language
How will we program in a hundred years? Why not start now?
оригинал, перевод - Beating the Averages
For web-based applications you can use whatever language you want. So can your competitors.
оригинал, перевод - Revenge of the Nerds
In technology, «industry best practice» is a recipe for losing.
оригинал, перевод 1, 2, 3 - The Dream Language
A good programming language is one that lets hackers have their way with it.
оригинал, перевод часть 1, часть 2 - Design and Research
Research has to be original. Design has to be good.
оригинал, перевод