Продолжаю выкладывать выдержки из вводного курса нашей компании по промышленному программированию.
Часть третья: Синтаксический сахар или история развития языков
В данной части расказывается история развития языков программирования, а так же доступно объясняется что такое ООП и функциональное программирование. Другие части можно найти тут.
Синтаксический сахар (syntactic sugar) — общее обозначение дополнений к синтаксису ЯП, которые делают использование языка более удобным, но не добавляют ему новых возможностей.
Вся история развития ЯП — это история повышения сладости синтаксического сахара.
Машинные языки
Всё началось с машинно-зависимых языков — языков, учитывающий структуру и характеристики определённых компьютерных платформ. Те, кто программировал на калькуляторах помнят, как составлялись на них программы.
Десяток регистров, куда записывались результаты вычислений (где они, эти гигобайты оперативки?), пара регистров смещения (вспомните машину Тьюринга, да-да, регистры обозначали из какого регистра данных брать следующую команду!), и регистр команды, куда нужно было записать очередную операцию (прочитать значение, записать значение, сложить значение двух регистров памяти и т.д.).
Архитектура фон Неймана
Даже архитектура этих устройств не всегда соответствовала архитектуре фон Неймана — стандартной для современных компьютеров.
Собственно, архитектура фон Неймана подразумевает отделение памяти от процессора и хранение в памяти изменяемых программ. Калькуляторы же обычно являлись устройствами с фиксированным набором выполняемых программ.
Собственно, переход к архитектуре фон Неймана породил возможность задавать автоматически выполняющиеся программы из внешнего источника — поначалу из перфолент и перфокарт.
Люди так и программировали, пробивая отверстие в карточке, соответствующее определённому регистру, таким образом, побитово задавая значения в них. Много историй связано с тем, как программа, набитая на сотнях перфокарт в буквальном смысле этого слова рассыпалась, когда неуклюжие техники роняли эти стопки картона на пол.
Ассемблер
На машинных кодах программировать было не очень удобно, по этому при первой же возможности появился ассемблер — язык, повторяющий машинные операции, но с человекопонятными командами и возможностью ручкой по бумаге описать алгоритм не как набор битов, а как какой-то более осмысленный текст.
Ассемблер так же привязан к архитектуре машины (поскольку его команды повторяют команды процессора), но шаг в пропасть был уже сделан и языки начинали всё больше и больше обрастать кристаллами сахара.
Стековые языки
Первой ласточкой стало использование стеков данных. Стек появился для решения задачи временного хранения произвольных данных. Конечно, данные можно сохранять и в регистре, однако в этом случае нужно помнить имя каждого регистра, данные из которого хочется получить.
Характерностью стека является особый порядок получения из него данных: в любой момент времени в стеке доступен только верхний элемент, т.е. элемент, загруженный в стек последним. Выгрузка из стека верхнего элемента делает доступным следующий элемент, по аналогии с автоматным рожком — первый засунутый туда патрон достать можно только последним.
Сейчас это может казаться дико неудобным, но это позволило создавать подпрограммы.
Перед вызовом подпрограммы, мы заполняем специально именованный стек данными. Подпрограмма, зная, в каком порядке помещены в стек параметры, может забрать их оттуда и использовать при своем выполнении, а по выполнении поместить результаты своего труда в тот же или в другой стек. Кроме того, основная программа имеет возможность сохранить свои данные в стеке до передачи управления подпрограмме. После возврата контроля программа просто восстанавливает свои значения из стека и не обращает внимание на то, что данные в регистрах процессора могли подпрограммой перетираться.
Макроассемблер
Следующим шагом был макроассемблер. Макроассемблер — это программа для макропроцессора, который в свою очередь являлся транслятором с языка более высокого уровня (макроассемблера) в машинный код. Стало возможным создавать свои команды для, например, использования стека.
Рождаются команды работы со стеком (push, pop), команды копирования стеков данных.
Макроассемблер порождает языки более высокого уровня, за командами которых стоят десятки, а то и сотни команд процессора. FORT, ALGOL, BASIC начинают свой путь…
Модульные языки
Вкусив запретного плода расширенного синтаксиса, программисты не остановились и возжелали модульности: ведь это так удобно — вызывать отдельно написанный модуль программы и не вникать в его алгоритм. Главное — это знать как он принимает на вход данные и как возвращает результат.
Ассемблер пополняется командами, облегчающими именование и подключение модулей, передачу и возврат управления при вызове различных подпрограмм. Развиваются интерфейсы обмена данными. Возникает понятие структуры данных.
Процедурные языки
Логичным добавлением к модульнуму языку послужило понятие процедуры или подпрограммы. Подпрограмма обладает двумя важными особенностями:
1. она именованна, т.е. мы можем вызвать подпрограмму по имени
2. вызвав подпрограмму, мы точно знаем, что она вернёт управление в то же место, откуда была вызвана
К примеру, в BASIC подпрограмма вызывалась как GOSUB :Label:.
Функция
Не хватало только одного: хотелось, чтобы переменные материнской программы (из которой вызывалась подпрограмма) не портились. А то ведь как оно было? Все переменные в глобальном пространстве, начнёшь их же использовать в подпрограмме — она их и затирает.
Так было изобретено понятие функции и локальных переменных: мы вызываем именованную подпрограмму и передаём туда какие-то значения. Подпрограмма воспринимает переданные значения, как локальные именованные переменные.
С развитием функция обросла возможностью возвращать результат: до этого, ведь, как было — возвращаемое значение записывали в одну из глобальных переменных.
Функция обладает следующими особенностями:
1. она именованна
2. туда передаются параметры
3. переданные параметры доступны как именованные параметры только внутри функции, вне функции они не видны
4. функция может использовать свои локальные именованные параметры, не видные вне этой функции
5. функция может возвращать результат работы
Введение в синтаксис функции гармонично дополняет процедурные языки программирования.
Функциональные языки
Естественным желанием было дополнить функции возможностью наблюдать при вызове локальные переменные функции-родителя в вызываемой функции.
Для решения этого гении сумеречного разума порождают понятие контекста исполнения: это область именованных переменных, доступных функции во время выполнения. Эта область данных делается наследовательно-расширяемой: при при вызове дочерней функции она создаёт свой контекст, пополняемый переменными, объявляемыми внутри функции-дочки. При этом вне функции-дочки эти переменные не видны. Зато будут доступны при вызове функции-внучки, функции-правнучки и так далее.
Возможность наследовать контекст исполнения называют замыканием (closure).
Возможность полноценной работы с контекстом исполнения порождает функциональные языки программирования.
Окончательно их оформляет добавление возможности передавать функцию как параметр для вызова другой функции, а так же возвращать функцию в качестве результата выполнения подпрограммы.
Типы данных
В то же самое время мысль программистов не стояла на месте. Программисты изобретали типы данных.
Первоначально, ведь как было, данные были доступны исключительно в бинарном виде — нолики, да единички.
Людям же, для решения практических задач, удобнее оперировать абстракциями более высоких уровней. Так появляются целочисленные типы данных без возможности указать являются ли они отрицательным или нет (byte, unsigned integer, unsigned long integer и т.д.).
Потом, как развитие их — типы данных с возможностью записи отрицательного числа (кодировавшегося первым битом, в связи с чем возникали забавные казусы неравенства +0 и -0). В довершении для более удобной работы с плавающей точкой возникли типы float и double float (как нетрудно догадаться, double float — это тот же float, но с возможностью записать больше знаков как до, так и после запятой).
Интересно байтовое представление типа float — в принципе, для того, чтобы передать число нам необходим тот же integer с возможностью указать отрицательное число и или нет и указанием, через сколько разрядов от начала числа необходимо поставить точку.
Для логических операций, в принципе, вполне хватало того же нуля и единички, но для большей кузявости их обернули в тип boolean с двумя значениями true и false (за которыми, в прочем, стояли те же единичка и нолик).
Следующим типом остро потребовавшийся программистам стал массив. Массив данных кардинально отличался от стека возможностью свободного доступа не только к последнему засунутому элементу, а вообще к любому по номеру. Массив представлялся программистам как склеенные ячейки, внутри которых лежат данные, по этому изначально массив задавался сразу определённого размера и размер этот изменить было невозможно.
Но, ведь, ячейки не обязательно бывают заполненными? Так потребовалось обозначение пустой ячейки и возникает тип null. По факту, вначале он представлял из себя символ с кодом 0x0, что приводило к весёлым казусам, когда в эту самую ячейку требовалось записать нулевое значение, а потом прочитать его и интерпретировать именно как null, а не как unsigned integer со значением 0.
Для объявления массива резервировался фрагмент памяти (буфер) с указанием, сколько ячеек будет расположено в этом фрагменте, а так же какие элементы будут в нём размещаться. И не дай Ричи тебе записать в массив типа int элемент вида long! В лучшем случае повреждались последующие за ним элементы, в худшем — возникало переполнение буфера и могли повредиться другие, не относящиеся к массиву данные, расположенные сразу же за выделенным буфером памяти.
Строки, кстати, вначале появились именно как массивы символов (пришлось ввести ещё один типа данных — char, по сути соответствовавший byte). Из-за этого длину строк приходилось объявлять заранее.
Дабы справиться со строками переменной длины, придумали помечать null-маркером конец строки. Т.е., как и ранее строка представляла из себя массив, но длина этого массива задавалась сразу достаточно большой, чтобы вместить любую строку (640кб памяти хватит любым программам, ага). Строка начиналась с начала массива, а конец её помечался как null-байт, то что шло после null строкой не считалось.
Хорошая на бумаге идея помечать конец строки null-маркером при ближайшем рассмотрении оказалось ужасной: ничто не мешало добавить в середину строки null и поиметь с этого кучу лулзов. Так началась эра C-strings.
Ссылки
Организация работы с данными, как с буфером памяти породило интересную возможность при вызове функции передавать туда не сами данные, а ссылку на них.
Ранее ведь как было? В функции передавались значения переменных, значения эти копировались в именованные переменные функции дабы избежать порчи оригинальных данных.
Но ведь внутрь функции можно передать просто значение адреса выделенного фрагмента памяти и далее зачитать из него переменную любого доступного типа данных! Так появился ещё один тип данных — ссылка.
Ссылка представляет из себя ярлычок (link) на какую-то переменную, под которую выделен блочок памяти. В ЯП появляются методы работы с переменными как по значению (напрямую с этим блоком памяти), так и по ссылке (считываем из переменной указатель, потом идём по нему и уже там меняем значение в памяти).
Структуры данных
Казалось бы, за что боролись на то и напоролись: изолировали-изолировали переменные внутри функций, чтобы их не портить, а теперь даём в руки ружьё из которого ещё и в ногу можно выстрелить!
Но не тут то было: передача переменных по ссылке дало уникальную возможность конструировать из простых типов данных целые конструкции — структуры данных!
К примеру, стало возможно организовать ссылку на массив из ссылок на массивы из… Так это же целое дерево построить можно!
Естественно, сам по себе такой массив никакой практической ценности не несёт, поскольку всё это можно организовать и с помощью простых типов данных, но если добавить в программу функции типа addNode, removeNode, работающие с деревом и передавать в эти функции ссылку на структуру данных, то получается рабочая и весьма соблазнительная конструкция.
Структурные языки
Так это же получается, что программист сам может создавать свои типа данных, удобных для его программы — достаточно только создать структуру данных и описать функции работы с ними!
Так появляются структурные языки программирования. В них немедленно добавляют возможность описания нового типа данных, возможность как-то именовать этот тип и задавать для него какие-то операции.
К примеру, строку можно представить уже не просто массивом, а двусвязанным списком с функциями конкатенации через оператор + и доступом к произвольному символу через оператор [].
Начинается немедленный бурный рост структурных языков (Pascal, C), обладающих следующими особенностями:
1. в них есть формальный язык описания структур данных (*.h файлы в C)
2. в них есть возможность дать описанной структуре название (BTree)
3. в них есть возможность обозначить операции работы с этой структурой данных
Объект
Возможность создавать свои собственные типы данных возбуждает в программистах страстное желание внутри этого типа данных иметь функции для работы с ним.
Как ответ на чаяния этих светлых умов рождается концепция Объекта. Объект — это уже не просто тип данных, не просто ссылка, по которой хранится структуированная информация, но ещё и функции по обработке этой информации, доступные по этой же ссылке.
Под это подводится вселенская философия:
“Объект — некоторая сущность в виртуальном пространстве, обладающая определённым состоянием и поведением, имеющая заданные значения свойств (атрибутов) и операций над ними (методов).”
Инкапсуляция
Глубокие философские исследования позволяют осознать, что объект обладает таким свойством, как инкапсуляция, которую определяют как свойство объекта объединять в себе данные и методы работы с этими данными. Философы вообще любят рекурсивные определения.
Суть инкапсуляции же проста: объект — не объект, если состояние его (т.е. данные, которые он содержит) можно поменять не применяя методов объекта. При этом считается, что публичные переменные объекта, доступные всем и вся для изменения — это как бы тоже методы по изменению внутреннего состояния этого самого объекта.
Собственно, $object->property = 12345; считается эквивалентным методу $object->setProperty(12345);, ведь без указания в операции имени объекта $object доступа к переменной $property напрямую не получить.
Наследование
Ещё до философов Объекта программисты при работе со структурами данных очень сильно захотели и придумали, как расширять структуры данных, наследовать схему структуры-родителя в структуре-детёныше.
Создание же Объекта, объединяющего данные с функциями породило интересную инженерную задачу, как бы так извернуться: и структуру унаследовать, и функции унаследовать, да ещё бы и новые возможности в наследника добавить.
А всё дело в том, что есть у тебя в Объекте-родителе функция, есть в Объекте-наследнике функция, делают они разное, а вот чтоб имена у них — опа — одинаковые. Решение этой задачи назвали полиморфизмом.
Полиморфизм
Философы и тут подсуетились, дав определение: “Полиморфизм — возможность объектов с одинаковой спецификацией иметь различную реализацию.”. Под спецификацией тут понимается названия-сигнатуры методов работы с объектом (включая публичные переменные).
Реализаций полиморфизма множество, вот некоторые из них:
— чистый полиморфизм (полиморфизм по сигнатуре)
— параметрический полиморфизм (полиморфизм по имени метода)
— переопределение (абстрагирование, абстрактные классы)
— перегрузка (неполное замещение метода предка методом потомка)
Абстрагирование
Философская мысль тоже не стояла на месте. Изучив свойства наследования, философы поняли, что его можно заменить абстрагированием.
Абстрагирование это такая вещь… Как бы объяснить? Вот есть у вас Объект — отлично, это что-то материальное. А ещё есть представление о том, каким этот объект может быть: какие методы он должен выставлять, что эти методы должны делать, но без конкретики, так, абстрактно (напоминает заказчиков, не правда ли?). Собственно мы только что описали интерфейс объекта или абстрактного предка заветы которого можно отлить в реальность кода.
ООП
Собственно, ООП — Объектно Ориентированное Программирование. Это умение некоторых программистов работать с объектами в рамках всех трёх концепций: инкапсуляции, наследования и полиморфизма. Ну или инкапсуляции, абстрагирования и полиморфизма.
Никакого отношения к модели MVC парадигма OOP не имеет (в отличие от мнения некоторых PHP программистов). ООП — это просто работа с данными и методами обработки данных, как с наследуемыми объектами.
В отличие от парадигмы процедурного и структурного программирования, где если и есть объекты, то они не наследуемы. Ну или и объектов-то нет, все данные передаются в массивах, структурах, выделенных буферах памяти.
Класс-ориентированное программирование
Объектное программирование требует создания множества объектов (как ни странно). Соответственно требуются как-то организовать иерархию объектов, как-то скучковать их.
В ответ на эти чаяния была разработана концепция класс-инстанс. Что такое класс? Класс — это набор методов и функций без данных. Сам по себе класс — это нечто нерабочее, для работы нужны данные. Собственно, для того чтобы получить рабочий объект необходимо инстанциировать класс — сказать “создай мне объект с теми функциями, что описанные в этом классе и данными, которые я тебе сейчас скажу”.
Фактически класс — это такой синтаксический кусище сахара, который позволяет не просто описать API объекта (как это делает интерфейс), но и задать функции по обработке данных.
Система классов позволяет формально описать свойства объекта, правила наследования свойств объектов, правила доступа к данным объекта. Использование классов задаёт парадигму класс-ориентированного программирования.
Класс — прикольная штука, но не необходимая для ООП, поскольку бывают объектно-ориентированные языки прекрасно обходящиеся и без классов.
Прототипное программирование
Другим способом задавать наследование является прототип. При прототипном программировании нет инстансов объектов, объект существует в уникальном виде. Но для каждого объекта можно задать прототип или прототипы — список объектов, свойства и методы которых он будет наследовать.
Исторически модель наследования через прототипы, которую разделяют такой язык, как JavaScript, является более старой, чем через описание классов. Но класс-ориентированное программирование оказалось более удобным для описания API и фреймворков (а как известно, каждый половозрелый Java программист обязан написать свой фреймворк, так же как половозрелость PHP программиста определяется по самописной CMS), по этому стало более распространённым.
Автор: ymik