База по языкам программирования: Синтаксический сахар или история развития языков

    Продолжаю выкладывать выдержки из вводного курса нашей компании по промышленному программированию.

    Часть третья: Синтаксический сахар или история развития языков



    В данной части расказывается история развития языков программирования, а так же доступно объясняется что такое ООП и функциональное программирование. Другие части можно найти тут.


    Синтаксический сахар (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), по этому стало более распространённым.
    Метки:
    Поделиться публикацией
    Комментарии 63
    • +1
      Какая-то ностальгия меня настигла по старым временам — захотел чего-то узнать — взял книгу (толстую) умную и напихал в себя знаний, а сейчас прочитал статью на три листа и вроде как все знаешь. Скучно.
      ЗЫ: Хотя откровенности ради нужно сказать, что в бытность школьником/студентом мне очень импонировало чьё-то изречение: «Где, те знания, которые потерялись в информации?»
      • 0
        Это вводный курс, рассчитанный на людей, которые уже что-то знают, но знания которых почему-то не сложились в цельную картинку :)

        Много деталей специально опущено, чтобы напомнить главное и побыстрее перейти к практике :)
        • 0
          «Я забыл больше, чем вы знали»
          • +1
            Зачем людям, у которых нет цельной картины, ломать сознание таким вот бредом дальше?
            • 0
              Давайте перейдём в предметную область: приведите список утверждений, которые вам кажутся бредовыми и не забудьте добавить обоснование этого чувства.
              • +6
                > Вся история развития ЯП — это история повышения сладости синтаксического сахара.

                Бред номер 1. Ибо вы не понимаете, что такое синтаксический сахар и не отличаете его от семантики. Вот do в Haskell — это синтаксический сахар, потому что скрывает сложный синтаксис последовательного связывания действий в монаде. А for в Си — это никакой не синтаксический сахар. Это оператор, семантика которого может быть реализована множеством способов, и в некоторых случаях, совсем не так, как вам кажется.

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

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

                Бред номер 2. Если вы говорите о программировании вообще, то всё началось именно с машинно-независимых языков, например, языка программирования станков Жаккарда. Если вы говорите о языках программирования, то всё началось с великих и ужасных Fortran и Lisp, которые опять же не машинно зависимые. Если вы говорите о языках машинных инструкций… То с этого ничего не началось, это было где-то посрединке.

                Десяток регистров, куда записывались результаты вычислений (где они, эти гигабайты оперативки?), пара регистров смещения (вспомните машину Тьюринга, да-да, регистры обозначали из какого регистра данных брать следующую команду!), и регистр команды, куда нужно было записать очередную операцию (прочитать значение, записать значение, сложить значение двух регистров памяти и т.д.).

                Бред номер 3. Вы понятия не имеете, что такое машина Тьюринга, в которой просто по определению нет регистров.

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

                Бред номер 4. Языки, в которых не использовался стек (тот же Fortran) появились гораздо раньше.

                Ну и так далее. Буквально в каждом абзаце бред. У меня, к сожалению, нет времени, чтобы комментировать статью до конца, но я думаю, этих нескольких примеров публике будет достаточно, чтобы понять, что лучше с образовательными курсами вашей компании не связываться :) Успехов вам в чтении книжек. У Непейводы, кстати, неплохо этот материал изложен, можете ознакомиться.
                • –1
                  Синтаксический сахар — это вообще, по определению, синтаксическая конструкция, которая заменяет собой другую синтаксическую конструкцию того же самого языка программирования.

                  Окэй, а в тексте что написано? Невнимательность вас погубит:
                  «Синтаксический сахар — общее обозначение дополнений к синтаксису ЯП, которые делают использование языка более удобным, но не добавляют ему новых возможностей.»

                  В рамках этого добавление команд pop push в ассемблер являлось синтаксическим сахаром по отношению к этому языку. То, что эти команды в дальнейшем входят в синтаксический стандарт языка на суть не влияет. И так практически с каждой языковой фичей. Будете спорить? Тогда конкретный пример синтаксической конструкции, которую имеетязык высокого уровня и которая не появилась ранее в языках-предках, как сахар.

                  Если вы говорите о языках программирования, то всё началось с великих и ужасных Fortran и Lisp… Если вы говорите о языках машинных инструкций… То с этого ничего не началось, это было где-то посрединке.

                  Вам не стыдно нести этот бред? В каком году был изобретён фортран? В каком году — лисп? А в каком году был эниак для программирования которого была разработана таблица коммутации, фактически породившая регистровые языки?

                  Вы понятия не имеете, что такое машина Тьюринга, в которой просто по определению нет регистров.

                  Вы опять невнимательны.

                  Языки, в которых не использовался стек (тот же Fortran) появились гораздо раньше.

                  Если не сложно, напишите реализацию на ассемблере части синтаксиса фортрана:
                  CALL f(a,b...)
                  Мне очень интересно понаблюдать, как вы это сделаете без стека.

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

                    2. pop и push — это инструкции процессора, и это не синтаксических сахар, это изменение семантики ассемблера, который теперь умеет генерировать новый код. Макроассемблер можно называть синтаксическим сахаром. Короче, опять у вас всё тут понамешано в кучу, и кони, и люди, и препроцессоры, и прочее.

                    3. Бредите вы, потому что вы говорите, в тексте: «Всё началось». Но история программирования началась совсем не с ЭВМ, а с ткацких и токарных станков, и произошло это в более дремучем году, чем вы думаете. Если вы говорите о языках программирования, не ассемблерах, которые не считались языками, а назывались автокодами, то их история началась с Fortran и Lisp. Ассемблеры — это нечто посрединке всего этого праздника программирования.

                    4. И почему я невнимателен? Я комментирую тот абзац, который вы в этом тексте написали. В котором прямо написано: вспомним машину Тьюринга, там регистры. Это даже не ересь, а именно бред.

                    5. А чего в этом сложного? Вы просто говорите, запись активации для функции f расположена по адресу FA. И пишете: mov a, FA[1]; mov b, FA[2]; mov ..., FA[X]. Стек, на самом деле, не вызовы и локальные перемнные позволяет организовывать, а рекурсию и прерывания. Именно в этом его важность.

                    6. И при чём тут ссылка на стековые языки? Forth и Fortran — это абсолютно разные языки.
                    • –1
                      pop и push — это инструкции процессора, и это не синтаксических сахар, это изменение семантики ассемблера, который теперь умеет генерировать новый код

                      pop и push далеко не сразу стали инструкциями процессора, если вы посмотрите на первые машины в архитектуре фон Неймана, то там были регистры, была адресация памяти, но не было стеков. Pop и push появились, как инструкции процессоров в ответ на развитие языковых запросов программистов.

                      Бредите вы, потому что вы говорите, в тексте: «Всё началось».

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

                      Если вы говорите о языках программирования, не ассемблерах, которые не считались языками, а назывались автокодами, то их история началась с Fortran и Lisp. Ассемблеры — это нечто посрединке всего этого праздника программирования.

                      Если не сложно, дайте, пожалуйста ссылку, что ассемблеры начались позже. По моим данным, фортран возник в 1954-57 годах как первый практический язык высокого уровня. Разработал язык IBM, он же и выпустил первый компилятор языка в машинные коды.

                      В котором прямо написано: вспомним машину Тьюринга, там регистры.

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

                      И пишете: mov a, FA[1]; mov b, FA[2]; mov ..., FA[X].

                      Угу, а возврат из подпрограммы как реализуете? К вопросу о рекурсии, кстати, ага.

                      И при чём тут ссылка на стековые языки?

                      Стековый язык — это любой язык, использующий стеки для передачи данных в подпрограммы и возврата из них значений.
                      • 0
                        pop и push далеко не сразу стали инструкциями процессора, если вы посмотрите на первые машины в архитектуре фон Неймана, то там были регистры, была адресация памяти, но не было стеков. Pop и push появились, как инструкции процессоров в ответ на развитие языковых запросов программистов.

                        Ну и вот. На этот раз вы правы. Но как теперь это соотносится с тем, что вся история ЯП — это синтаксический сахар?

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

                        А что такое регистровый язык? Нам на сольфеджио про такой рассказывали, а на лекциях по компьютерным наукам — нет :)

                        Если не сложно, дайте, пожалуйста ссылку, что ассемблеры начались позже. По моим данным, фортран возник в 1954-57 годах как первый практический язык высокого уровня. Разработал язык IBM, он же и выпустил первый компилятор языка в машинные коды.

                        Так вот именно что в машинные коды. Первый транслятор Fortran пробивал дырочки на перфокартах, и был написан путём пробивания дырочек. Там не было ассемблера. Когда появился первый ассемблер точно я не знаю, но вот Fortran и Ассемблеры долго считались конкурирующими подходами к программированию, пока транслятор фотран не уделал какой-то вручную написанный код по скорости выполнения программы. То есть, неверно считать, что Fortran стал развитием какого-то ассемблера.

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

                        Нет. Главное свойство регистра не то, что оно используется для хранения и обработки (перфокарта, или там магнитная лента, тоже могут использоваться), а то, что у него есть номер (или идентификатор), по которому к нему можно получить доступ (ну, мы о программировании говорим, для программирования это именно так, см. машину Минского). У ячеек машины Тьюринга нет номеров (и их даже очень-очень непросто присвоить), поэтому они не являются регистрами.

                        Угу, а возврат из подпрограммы как реализуете? К вопросу о рекурсии, кстати, ага.

                        mov RETHERE, FA[0]; RETHERE:
                        f: ...; jmp FA[0];

                        Проблема рекурсии не в возврате, а в том, что функция может быть вызвана повторно.

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

                        Не правда. Стековый язык — это язык, в котором каждый оператор операнды берёт из стека. Вызовы функций на них, действительно, очень просто описывать. Но существуют языки, которые не используют стеки для этого. Например, Haskell. Или, допустим, JavaScript. Они напрямую оперируют записями активации, динамически их выделяют и т.д.
                        • –1
                          Но как теперь это соотносится с тем, что вся история ЯП — это синтаксический сахар?

                          Ну это же элементарно! Вначале фича появляется, как синтаксический сахар в языке, потом она реализуется как синтаксическая особенность языка более позднего поколения.

                          А что такое регистровый язык?

                          Язык регистровых машин

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

                          en.wikipedia.org/wiki/Assembly_language#Historical_perspective
                          en.wikipedia.org/wiki/FORTRAN_Assembly_Program#FORTRAN_assembly_program

                          ну и так далее.

                          Главное свойство регистра не то, что оно используется для хранения и обработки (перфокарта, или там магнитная лента, тоже могут использоваться), а то, что у него есть номер (или идентификатор), по которому к нему можно получить доступ

                          Собственно, а можно ссылку на определение, где явно указывается, что регистр должен быть именованным? И чем вам не нравится адресация по смещению от текущего положения в МТ?

                          mov RETHERE, FA[0]; RETHERE:
                          f: ...; jmp FA[0];

                          Ага, а теперь цепочку CALL с возвратом управления к вызывающему операнду. Собственно, к вопросу о стековом языке.
                        • +1
                          Как по мне стековый язык — язык в спецификации которого явно упомянут стек для передачи данных и/или управления (в том же FORTH явно описано два отдельных стека — данных и вызовов). Если же просто предъявляются требования для передачи, но механизм их не описан, то язык стековым не будет, даже если использование стека, пускай даже программного, а не аппаратного (микропрограммного) будет наиболее очевидным и эффективным решением. Другими словами, если описано в, например, описании процесса вызова подпрограммы, что как минимум адрес возврата сохраняется именно в стек, то язык стековый, если же описано что он сохраняется где-то и как-то так, чтобы подпрограмма могла вернуть управление, но где и как неспецифицировано, то язык не стековый, несмотря на то, что все его реализации используют именно стек, чаще всего аппаратный.
                          • 0
                            Например, захватывать контекст функции в C с помощью malloc. А что? Поначалу кажется, что неэффективно, но где-нибудь на сильно параллельной машине (в облаке или кластере) может пригодиться :)
                      • +3
                        Вызов функции без стека, в Фортране? Давайте. Предположим, что у нас процессор вообще без стека. Скажем, PDP 11/70, из которого убрали команды JSR и RTS.

                        1) Не забываем, что в Фортране рекурсии нет. Поэтому у каждой функции может быть контекст, который никто не испортит.
                        2) Все аргументы передаются по ссылке. У всех переменных есть адрес. Для каждого вызова функции задается табличка с адресами всех аргументов (да еще и с их числом). Как правило, статическая. В редких случаях (повторная передача параметров) ее приходится менять. Функции передаем только адрес таблички. В регистре R5.
                        3) Из функции надо будет куда-то возвращаться. Раз стека нет, положим адрес возврата в регистр R6.

                        Поехали.
                        Вызывающая функция:
                        ;;; локальные переменные
                        H1$A:  .WORD 0
                        H1$B:  .WORD 0
                        ;;; табличка вызова
                        H1$CALL1: .WORD 2,H1$A,H1$B
                        ;;; место для адреса возврата и для таблички параметров
                        H1$PARAM: .WORD 0
                        H1$RET:  .WORD 0
                        ;;; сама функция H1:
                        H1:
                            MOV R5,H1$PARAM   ;;; сохранили блок вызова
                            MOV R6,H1$RET       ;;; сохранили адрес возврата
                        ....  ;;; что-то делаем
                        ;;; собираемся вызвать функцию
                           MOV #H1$CALL1 ,R5
                           MOV #H1$CALL1$RET,R6
                           JMP F
                        H1$RET:
                           MOV  H1$PARAM,R5
                        .... ;;; делаем что-то еще
                           JMP @H1$RET  ;;; возвращаемся из функции H1
                        


                        Теперь функция F (к сожалению, не помню точно, как ее оформлять на Фортране):
                        PROCEDURE F(IA,IB)
                        IA=IA+IB
                        END

                        ;;; блок описывать не будем, предположим, что компилятор его  соптимизировал
                        F:
                            ADD @4(R5),@2(R5)  ;;; двойная адресация (обращение к ячейке, адрес которой лежит в (R5+4))
                            JMP (R6)  ;;; прыжок по адресу, лежащему в R6.
                        


                        Как-то так. Возможно, повторная передача параметра велась как-то по-другому. Если ошибки в коде — не судите строго, в последний раз я писал на нем (и на Фортране) 25 лет назад, мог что-нибудь забыть.



                        • 0
                          Ага, уели. Я искренне считал фортран стековым, теперь вижу, что это не так. Добавлю в статью этот факт.
                          • 0
                            Скажем, так: фортран можно реализовать без стека. Но далеко не всегда это будет удобно и эффективно: современные процессоры общего назначения более ориентированы на языки, допускающие рекурсию, а значит, требующие эффективной работы с блоком контекста функции, захваченным на стеке.
              • 0
                Как молоды мы были, как молоды мы были…
              • +2
                какое-то противопоставление мастеров-на-все-руки жавщиков и недотёп пхпшников. В наше время уважающий себя жавщие понятия не имеет чем GET отличается от POST и на каждый чих молотит мегабайты ненужного кода с конфигами. Такчт противопостовление неверное. Они друг друга стоят.
                • 0
                  Собственно, а где вы увидели противопоставление? Наши семинары с удовольствием посещают как PHP, так и Java инженеры.
                  • 0
                    Так вот где источник-то БК :)
                • 0
                  — чистый полиморфизм (полиморфизм по сигнатуре)
                  — параметрический полиморфизм (полиморфизм по имени метода)


                  Поясните, пожалуйста, что значит в вашем случае «по сигнатуре» и «по имени метода»?

                  — переопределение (абстрагирование, абстрактные классы)

                  И как же именно метод ПЕРЕопределяется (override?) в случае абстрактного класса?

                  — перегрузка (неполное замещение метода предка методом потомка)

                  Неполное это как? В C++, например, перегрузкой (overloading) называется род полиморфизма, ни коим образом не связанный с наличием потомков.
                  • 0
                    Чистый полиморфизм — это когда когда одна и та же функция выполняется к произвольному списку параметров. В случае чистого полиморфизма есть одна функция (тело кода) и несколько ее интерпретаций.

                    Другой случай — это когда имеется множество различных методов с одним именем, но разным списком параметров (с разными сигнатурами методов). Собственно, это и называется перегрузкой или полиморфизмом ad hoc. Между этими двумя крайностями лежат переопределяемые методы. А абстрактные классы тут не при чём — они только задают API :)
                    • 0
                      Грубо говоря разница лишь в том, что считать сигнатурой функции?
                      • 0
                        Термины, в которых описывается условие match. Используются там типы, или свойства типов, или в предикате фигурируют сами значения. Может быть, в языке вообще типов нет, а есть только void*, или list, или аморфная структура со списком методов…
                        Но еще вопрос — на каком этапе возможно разрешение полиморфизма — компиляция, или только выполнение.
                  • +5
                    А заголовок как я понимаю просто для завлечения?
                    • –1
                      Вы считаете, что это не база? Что же тогда является базой для вас — может быть, структуры данных? Но они появились позже. Может быть, теорию конечных автоматов? Но и они не являются базой по языкам программирования.
                      • +1
                        Нет, я считаю, что термин «синтаксический сахар» в заголовке для завлечения.
                        • –1
                          То есть вы утверждаете, что в языках высокого уровня присутствует некие уникальные возможности, нереализуемые с помощью машинных кодов? Или вы утверждаете, что синтаксический сахар не приводит к повышению уровня абстракции и, как следствие, повышению уровня языка?
                          • +1
                            Да, существуют. Потому что некоторые языки можно компилировать прямо в цифровые схемы, которые не выполняют ни одной машинной инструкции. Ещё раз, вы в одну кучу мешаете синтаксис, семантику и прагматику. Это совершенно разные вещи, мало друг от друга зависят. К повышению уровня абстракции приводит не синтаксический сахар, а семантика языка. Вот в LISP никакого сахара нет, и синтаксис дубовый, однако абстракции в нём можно задрать хоть до уровня почти полноценного AI.
                          • +1
                            В моём комментарии написано ровно одна тема: «заголовок для завлечения». Это значит, что я не увидел в статье ничего про «синтаксический сахар».
                            • 0
                              Ок, надо было более правильно написать аннотацию. Какой бы вы вариант предложили?
                    • +8
                      >Вся история развития ЯП — это история повышения сладости синтаксического сахара.

                      Угу, а интеллект — это просто очень сложный инстинкт :)
                      • +1
                        это сложный недоинстинкт :)
                        • 0
                          После этой фразы можно и не читать дальше :) Сразу всё понятно о статье
                          • 0
                            Спасибо за полноценное и развёрнутое мнение, а так же за уделённое статье внимание :)
                        • +1
                          Непонятно, почему про Тьюринга и Неймана вы написали, а про лямбда-исчисление — нет, коль уж речь и о ФП завели.
                          • 0
                            Кстати, да. Недоработка, в следующую редакцию обязательно внесу дополнение.
                            • 0
                              Closure у вас чуть-ли не главное что есть в функциональном програмировании. Думаю этот раздел требует существенной переработки.
                              Как появилось ООП, лучшего введения чем у Uncle Bob я не видел:
                              skillsmatter.com/podcast/agile-scrum/uncle-bob-expert-insights
                          • +3
                            Статья пригодна для детского сада, не для IT-ресурса. Все комментарии о бредовости некоторых высказываний выше совершенно верны, если учитывать аудиторию, ведь как бы детям на вопрос «почему солнце светит?» не обьясняете принципы протекания ядерных реакций распада, так и айтишникам не надо выдавать тематическую статью, предназначенную, по всей видимости, чтобы заинтересовать школьников младших классов. Ну максимум средних.
                            • +2
                              Хотя кстати мне понравилось, если читать как сказку, а не как достоверную историческую статью.
                            • +1
                              Прочитал статью (и предыдущие две) и осталось неприятное впечатление, что автор считает развитие ЯП да и их самих вредным и ненужным. А ведь наверняка на работе не нолики/единички на реле отстукивает, а пользуется именно развитыми ЯП и тем, что на них написано.
                              • 0
                                Извините, а откуда появилось такое ощущение? Вроде бы я нигде не утверждал, что программирование на высокоуровневых языках — это плохо. Скорее даже наоборот — это позволяет писать код быстрее.
                                • 0
                                  У меня такое чувство тоже появилось. Хотя бы после такого:
                                  Ассемблер так же привязан к архитектуре машины (поскольку его команды повторяют команды процессора), но шаг в пропасть был уже сделан и языки начинали всё больше и больше обрастать кристаллами сахара.
                                  • +1
                                    Я надеялся, что это будет воспринято, как шутка :)
                              • 0
                                > Класс — прикольная штука
                                прочитал: «Класс — прикольная шУТка» =)
                                • 0
                                  «Вся история развития ЯП — это история повышения сладости синтаксического сахара.»

                                  Это, как мне кажется, грубоватая аналогия. Сахар подразумевает, что язык остается тем же, но к нему добавляются альтернативные варианты записи.

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

                                  Так что же тогда получается, С — это одновременно несколько машинных языков?

                                  Тем не менее, сахар — во многом условное определение.
                                  • 0
                                    Там, выше уже придрались к этому
                                    • 0
                                      будем считать, что ответил туда, выше.

                                      Добавлю, что некоторые языки не транслируются в машинный код — типа бейсика или джавы, псевдокод/байткод исполняется виртуальной машиной.
                                      • 0
                                        Про трансляцию тут ничего не говорилось. Байт-код вообще интерпретируется.
                                  • +1
                                    Добавлю и от себя немножко критики:

                                    Так появляются целочисленные типы данных без возможности указать являются ли они отрицательным или нет (byte, unsigned integer, unsigned long integer и т.д.)

                                    Вообще разделение на знаковые и беззнаковые было уже в ассемблере (imul, ja/jg). Да и называть их стоило бы в более привычном для «того времени» формате: byte, word, dword, qword, etc. Это хоть гарантирует понимание размера.

                                    тип boolean с двумя значениями true и false (за которыми, в прочем, стояли те же единичка и нолик)

                                    Не во всех ЯП true/false приводятся к 0 и 1. Например, в VB они описываются как «False becomes 0 and True becomes -1». Т.е. тут лучше говорить о нуле и не нуле (побитовом НЕ).

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

                                    Стек также повсеместно используется для произвольного доступа, если мы знаем адрес текущей вершины стека (в нем удобно складывать как раз те самые локальные переменные). Собственно, пролог push ebp; mov ebp, esp; sub esp, N и эпилог ret N функций это и реализуют.

                                    Еще неплохо было бы написать, что массив — это монотонная последовательность ОДНОТИПНЫХ данных, а не просто «склеенные ячейки».

                                    Но, ведь, ячейки не обязательно бывают заполненными? Так потребовалось обозначение пустой ячейки и возникает тип null

                                    OMG. Это ж уже не массивы, а списки. И null тут можно вытянуть из указателей, но не как обозначение пустой ячейки. Для обозначения пустой ячейки именно в массивах можно было бы добавлять к каждому элементу массива поле «свободно/не задано».

                                    И не дай Ричи тебе записать в массив типа int элемент вида long!

                                    Иногда эти типы могут иметь одинаковый размер, не так ли? Тут бы лучше пример вида записи float для массива byte.

                                    Дабы справиться со строками переменной длины, придумали помечать null-маркером конец строки.

                                    Опять узкий сишный взгляд. Так придумали в погоне за экономией памяти в ущерб скорости работы программы. И так и тащится это «наследие» в некоторых ЯП. В других умно и правильно через хранение длины строки рядом. А еще правильнее (вроде даже в WinAPI есть их поддержка) хранить буфер под строку, и счетчики сколько байт занято и сколько доступно.

                                    ничто не мешало добавить в середину строки null и поиметь с этого кучу лулзов

                                    Больше лулзов можно получить, если забыть терминальный null :)

                                    Ссылка представляет из себя ярлычок (link) на какую-то переменную, под которую выделен блочок памяти.

                                    Да, косвенная адресация. Появилась явно раньше функций. Но за «ярлычок» и «блочок» ловите + :)

                                    Но не тут то было: передача переменных по ссылке дало уникальную возможность конструировать из простых типов данных целые конструкции — структуры данных!

                                    Я что же, не могу передать структуру/массив в функцию по значению? У нас же есть стек.

                                    1. в них есть формальный язык описания структур данных (*.h файлы в C)

                                    Про type в pascal бы тоже написали сразу, раз выше вспомнили о таком ЯП. Причем в нем еще и есть полноценная реализация областей описания и реализации, в отличие от C.

                                    Возможность создавать свои собственные типы данных возбуждает в программистах страстное желание внутри этого типа данных иметь функции для работы с ним.

                                    В C/C++ с этим как раз боролись, объявляя функции работы прямо в структурах. Вполне себе вариант. В прототипном JS с этим вообще все шоколадно.

                                    инкапсуляция, которую определяют как свойство объекта объединять в себе данные и методы работы с этими данными. Философы вообще любят рекурсивные определения.

                                    Но тут нет рекурсии. У нас есть яблоко и мальчик Вова, который умеет есть яблоки — где тут рекурсия? Тут только инкапсуляция :)

                                    Но класс-ориентированное программирование оказалось более удобным для описания API и фреймворков [...], по этому стало более распространённым.

                                    Ну тут скорее взяли количеством, а не качеством.

                                    В целом как-то все наивно, узко и, в общем-то, во многом и не о синтаксическом сахаре, а о причинах, почему C++ выглядит именно так, а не иначе. Многое вообще никакого отношения к синтаксису не имеет.
                                    Стоило бы пойти дальше, к значительно более сладким языкам.
                                    • +1
                                      Спасибо, учту ваши комментарии в следующей редакции лекции :)

                                      Насчёт узости — задача такая: быстро напомнить джуниорам откуда вообще пошли языки и что стоит за ООП и языками, на которых они пишут (у нас пишут на Java и PHP).

                                      Рассказывать системно и последовательно про историю программирования — это явно более 2х часов времени.

                                      Следующие курсы — это уже тактика и стратегия написания промышленного кода. Максимум практики, минимум теории.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.