Язык BCPL из которого получился C
Готовясь к собеседованиям по Go я обратил внимание на то что среди его создателей Кен Томпсон — я смутно помнил что он также стоял у истоков языка C, но без подробностей. На самом деле было примерно так: Мартин Ричардс написал BCPL, Кен Томпсон переделал его в B повыкидывав «ненужное» и улучшив синтаксис, а Деннис Ритчи добавил разнообразие типов чтобы получился язык С который мы уже более-менее представляем.
И вот я решил заглянуть в BCPL — насколько он был похож и в чем отличался. Кратким обзором — сравнением с С я и хочу поделиться! Вы сможете и сами «пощупать» его при желании :)
Небольшая предыстория
«Высокоуровневые» языки программирования начались с FORTRAN-а в 1957, за которым вскоре последовали LISP и Algol в 1960. Языков похожих на Фортран с его частыми метками в строках, коммон-блоками и отсутствием рекурсии (изначально) вы сейчас уже пожалуй не найдёте (хотя Бейсик в 1963 году немало его напоминал). Лисп в разных диалектах встречается и до сих пор — в отличие от прочих он был интерпретируемым (и позволял самомодификацию кода и т.п.) -, но его скобочный синтаксис всегда сперва удивляет. А вот Алгол — его очень легко перепутать с Паскалем — именно в нём появилась «блочная» структура кода, все эти begin-end (которые в С-подобных языках заменились на фигурные скобки). Фактически Алгол стал «дедушкой» всевозможных языков с «паскальным» и «сишным» синтаксисом. В 1963 году был придуман CPL (combined programming language) — в котором begin-end попытались заменить более короткими символьными последовательностями. Этот язык однако имел необычную судьбу — он был задуман довольно сложным, для научных вычислений в том числе — и так получилось что полноценный компилятор появился лишь в 1970. Гораздо более простой язык BCPL созданный на основе 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.
Интересной и важной особенностью языка являлось то что он компилируется в три этапа (часто это три отдельных программы — в том числе в OBCPL):
сначала текст разбивается на токены из которых формируется структурное дерево лексем, представляющих программу
структурное дерево преобразуется в переносимый O-code — этакий мета-ассемблер из сравнительно небольшого числа операций
а уже только O-код на самом деле компилируется в нативный код
Этот подход был впоследствии массово использован и в других языках — получалось что для портирования языка на другую платформу достаточно переписать только третью часть (компиляцию O-кода).
Это кстати подробно описано автором в отдельной главе его книги (BCPL, the language and its compiler) — которая поэтому может быть актуальна для любителей изобретать новые языки даже сейчас.