Готовясь к собеседованиям по Go я обратил внимание на то что среди его создателей Кен Томпсон — я смутно помнил что он также стоял у истоков языка C, но без подробностей. На самом деле было примерно так: Мартин Ричардс написал BCPL, Кен Томпсон переделал его в B повыкидывав «ненужное» и улучшив синтаксис, а Деннис Ритчи добавил разнообразие типов чтобы получился язык С который мы уже более-менее представляем.
И вот я решил заглянуть в BCPL — насколько он был похож и в чем отличался. Кратким обзором — сравнением с С я и хочу поделиться! Вы сможете и сами «пощупать» его при желании 🙂
Небольшая предыстория
«Высокоуровневые» языки программирования начались с FORTRAN-а в 1957, за которым вскоре последовали LISP и Algol в 1960. Языков похожих на Фортран с его частыми метками в строках, коммон-блоками и отсутствием рекурсии (изначально) вы сейчас уже пожалуй не найдёте (хотя Бейсик в 1963 году немало его напоминал). Лисп в разных диалектах встречается и до сих пор — в отличие от прочих он был интерпретируемым (и позволял самомодификацию кода и т.п.) — но его скобочный синтаксис всегда сперва удивляет. А вот Алгол — его очень легко перепутать с Паскалем — именно в нём появилась «блочная» структура кода, все эти begin-end (которые в С-подобных языках заменились на фигурные скобки). Фактически Алгол стал «дедушкой» всевозможных языков с «паскальным» и «сишным» синтаксисом.
В 1963 году был придуман CPL (combined programming language) — в котором begin-end попытались заменить более короткими символьными последовательностями. Этот язык однако имел необычную судьбу — он был задуман довольно сложным, для научных вычислений в том числе — и так получилось что полноценный компилятор появился лишь в 1970. Гораздо более простой язык BCPL созданный на основе CPL оказался готов раньше (1967)! Более того, был готов и язык B (1969) — упомянутое упрощение BCPL — а его «типизированная» версия под названием C появилась в 1972. Всё это привело к тому что сам CPL использовался мало и скоро практически исчез.
Мне кажется это добрым молодцам урок — придумывай проще, делай быстрее. Долгие проекты могут опоздать.
Название BCPL таким образом должно было означать что-то вроде «Basic CPL», в смысле простой или базовый — но поскольку про CPL уже начали забывать то неудивительно что Кен Томпсон для своей модификации повыкидывал не только ненужные фичи языка — но и ненужные буквы из названия.
Первый взгляд
get "LIBHDR" // включаем заголовочный файл let start() be $( // вместо main - start, и скобки немного другие writes("Hi, People!") // writes - Write String $)
Программа «hello world» выглядит настолько похоже на привычную в С — мало что можно добавить. Точки с запятой в конце строки необязательны — только если нужно разделить пару стейтментов в одной строке. Скобки вокруг тела функции в данном примере можно было не ставить let start() be writes("...")
— это сработает, ключевое слово «let» используется как для объявления переменных так и функций.
Посмотрим на программу вводящую и складывающую числа:
get "LIBHDR" let start() be $( let a, b = 0, 0 a := readn() b := readn() writen(a + b) $)
Как было сказано let
объявляет переменные — при этом инициализирующие значения обязательны, что не всегда удобно (в данном случае они все равно не нужны, но проинициализировать сразу чтением из readn() нельзя). Очевидно readn()
считывает число а writen(...)
печатает число в консоль. Обратите внимание на оператор присваивания (паскально-алгольный) в противовес знаку равенства при инициализации — напоминает ситуацию в Go…
Но главное — переменные не имеют типа! Это не потому что в языке динамические типы или duck typing — просто тип может быть только одним, хотя и в нескольких ипостасях:
-
либо это целое число, равное по размеру слову процессора (скажем, 32 бита)
-
либо это число — указатель, адрес памяти — на начало массива
-
в массиве же могут лежать текстовые данные, строка — обычно упакованные
-
и конечно указатель может указывать на функцию
Таким образом для всего этого используется один тип — вроде нашего int
.
Вдогонку можно отметить что присваивание нескольких величин одним оператором возможно, например a, b := readn(), readn()
— но это работает не так как в Питоне, а выполняется все равно по очереди — т.е. обменять две переменные так не получится.
Компилятор OBCPL
На случай (мало ли) если уже захотелось попробовать — в сети можно найти несколько реализаций BCPL и для современных машин — одна из по-видимому наиболее эффективных это OBCPL который может быть найден в нескольких чуть различающихся версиях, например https://github.com/SergeGris/BCPL-compiler — я взял отсюда, он легко компилируется под Linux / FreeBSD.
Онлайновых вариантов я не нашёл — поэтому добавил этот же компилятор на сайте CodeAbbey (мой сайт с задачками, теперь опенсорсный) — правда чтобы добраться до «запускалки» нужно залогиниться, но регистрация простая, не требует телефона или валидной почты.
Управляющие конструкции
При взгляде на набор всевозможных условий и циклов становится ясно почему Кен Томпсон решил это дело слегка упростить. Вот условные операторы, их три вместо одного:
-
if A then B
— условный оператор без альтернативы -
test A then B else C
— условный оператор с альтернативой, использует другое ключевое слово (вместо if — test) -
unless A then C
— условный оператор «если-не»
Здесь отметим — скобки вокруг выражений (условия) не требуются. Слово then
почти всегда можно пропустить. Почему понадобился unless
? Дело в том что И/ИЛИ/НЕ в языке есть, но они только битовые! Зато впоследствии он перекочевал например в Perl.
Конечно B
и C
могут быть блоками из нескольких стейтментов, в скобках $( ... $)
— как и в привычных нам языках.
А вот циклы. Их много:
-
while E do C
и такжеuntil E do C
— циклы «пока» и «пока-не» с пред-условием -
C repeat
— бесконечный цикл, как ни странно, ключевое слово после тела -
C repeatwhile E
и конечноC repeatuntil E
— циклы с пост-условием (Е — оно по-прежнему не требует скобок) -
for
N = E1 to E2 by K do C
— цикл «for» с локальной переменной N, причем «by K» — шаг цикла можно пропустить, он конечно равен 1 по умолчанию.
Как и с условиями, тело цикла С может быть написано в «долларовых» скобках, если в нём несколько действий — а слово DO опционально.
С циклами используются такжеbreak
и loop
— последний является аналогом continue
.
Для досрочного возврата из функции используется знакомый return
но если функция возвращает значение то он превращается в resultis
— и сама функция в описании должна использовать знак равенства и ключевое слово valof
вместо be
— пример с факториалом выглядит вот так:
let fact(n) = valof $( resultis n = 0 -> 1, fact(n-1) * n $)
Здесь использован тернарный оператор — он имеет слегка другую форму чем в Си A -> B, C
— вычисляется B или C в зависимости от проверки A на равенство нулю. Вообще valof
и resultis
это конструкция которую можно использовать и просто как отдельный блок а не функцию — может быть довольно удобно!
Массивы и Указатели
get "LIBHDR" let start() be $( let a = vec 9 for i = 0 to 9 a!i := readn() for i = 9 to 0 by -1 $( writen(a!i) wrch('*N') $) $)
Здесь мы объявляем массив с помощью ключевого слова vec
— как видно, оно резервирует память на стеке среди прочих локальных переменных — под требуемое количество ячеек. Немного путает что указывается «последний индекс» а не общий размер (то есть, 9 вместо 10 для десяти ячеек).
Обращение к элементу массива — вместо квадратных скобок — с восклицательным знаком (не удивлюсь если многие клавиатуры тогда не имели ни фигурных ни квадратных скобок). То есть a!i
— И-тый элемент массива А. Функцию wrch(...)
мы ещё не видели — но она очевидно печатает символ (аналог putc
) — специальные символы вместо привычного слеша обозначаются звёздочкой, в данном случае это перевод строки.
Указатели, как и в Си, близко перекликаются с массивами. Символ @
позволяет получить адрес переменной, а !
наоборот «дереференсит» указатель, например:
let a, b = 0, 0 a := 15 b := @a // B теперь указывает на A !b := 51 // по адресу лежащему в B запишем новое число writen(a) // конечно оно оказалось в A
У восклицательного знака таким образом две разновидности — если он «унарный» — перед именем переменной — то это «дереференс». А если он между двумя выражениями — как в случае с элементом массива — это на самом деле «синтаксический сахар»:
a!i берем из массива по индексу, как a[i] в C !(a + i) прибавляем индекс к указателю и дереференсим его, как *(a + i)
Эти две строчки идентичны — то же самое сделано и в си. И отсюда же следует что можно поменят их местами i!a
— что сработает и в Си по крайней мере если предупреждения на этот счет выключены.
На этом же механизме реализованы и прототипы структур. Можно объявить константы и использовать их для дереференса:
let person.age, person.iq = 0, 1 let a = vec 10 a!person.age := 25 a!person.iq := 113
Заметьте что точка — это просто допустимый символ в идентификаторе, к созданию структуры он не относится. К сожалению в таком виде всё-таки нужно аккуратно следить чтобы поля одной структуры в массиве не пересеклись с полями следующей. Автор довольно подробно поясняет эту технику в своей книге.
Динамические массивы в базовый стандарт языка не входят, но присутствуют по словам автора почти в любой реализации. Например в OBCPL для них используются функции getvec
и freevec
(аналоги malloc
и free
).
Как вскользь упоминалось выше, указатели на функции используются легко и непринуждённо, в отличие от Си не приходится мудрить с декларацией типа:
let blaha = 0 blaha := writes blaha("muha")
Заключение
Мы много чего ещё не посмотрели — объявление констант, глобальных и статических переменных, работу со строками (которые записываются в массив «паскальным» способом, со счетчиком длины в начале), разнообразные функции для работы с потоками (файлами) — переключающие input и output по сути. Но мне кажется что для первого знакомства пока хватит 🙂
Язык оказался довольно удачным в том смысле что позволял писать и довольно низкоуровневые вещи, и обеспечивал хорошую переносимость высокоуровневых программ. В частности как я рассказывал ранее, на нём написан первый MUD — исходники можно найти на гитхабе (а поиграть — на british-legends.com)
Мы видим как много идей перешло в Си (и далее) с минимальными изменениями. В то же время впечатляет как много «возможностей для улучшения» оставил автор будущим разработчикам языков B и C.
Идея «одного типа» во времена 8 и 16-битных домашних компьютеров была неудобной — а сейчас например в программировании для ARM как будто опять звучит актуально.
Интересной и важной особенностью языка являлось то что он компилируется в три этапа (часто это три отдельных программы — в том числе в OBCPL):
-
сначала текст разбивается на токены из которых формируется структурное дерево лексем, представляющих программу
-
структурное дерево преобразуется в переносимый O-code — этакий мета-ассемблер из сравнительно небольшого числа операций
-
а уже только O-код на самом деле компилируется в нативный код
Этот подход был впоследствии массово использован и в других языках — получалось что для портирования языка на другую платформу достаточно переписать только третью часть (компиляцию O-кода).
Это кстати подробно описано автором в отдельной главе его книги (BCPL, the language and its compiler) — которая поэтому может быть актуальна для любителей изобретать новые языки даже сейчас.
ссылка на оригинал статьи https://habr.com/ru/articles/848870/
Добавить комментарий