Готовясь к собеседованиям по 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) - которая поэтому может быть актуальна для любителей изобретать новые языки даже сейчас.
Комментарии (8)
victor_1212
08.10.2024 07:48+1небольшое дополнение про историю - первым языком высокого уровня вероятно таки был Plankalkul (Zuse 1945-46), интересно что история Algol 60 тоже слегка связана с Германией, работы над языком там начали Samelson, Bauer (Munich), Bottenbruch (Darmstadt), Rutishauser (Zurich) примерно в 1957, проект получил название Algol 58, т.к. был представлен на конференции в Цюрихе в 1958, конечно германские представители были знакомы с работами Zuse, в US для этого языка сначала использовали другое название IAL (International Algebraic Language), быстро добавили операторы I/O и на этой основе сделали JOVIAL, который стали использовать при разработке систем реального времени следующие 20-30 лет (до появления ADA)
RodionGork Автор
08.10.2024 07:48Спасибо, но я не очень понимаю в каком виде Plankalkul существовал. Дизайн языка якобы был написан но не опубликован (?) Описание Z4 достаточно туманно чтобы понять можно ли было на ней создать компилятор, но похоже что нет. Языки без реализации трудно категоризировать - ну вот как с самим CPL ситуация - он вроде был, а вроде его и нет.
Хотя сам язык действительно очень любопытный, особенно его упомянутая двухмерная структура изначальная.
Допускаю что и другие языки могут претендовать на роль первого, т.к. некоторые языки уже существовали до фортрана - вопрос во многом в том как определить их "высокоуровневость".
Спасибо за историческую справку по Algol - на этот счет я не особо был в курсе.victor_1212
08.10.2024 07:48+1Heinz Rutishauser (один из создателей Algol 58/60) был учеником Zuse, в 1952 он опубликовал важную работу типа "Automatische Rechenplanfertigung ..." в которой впервые обсуждал теорию компиляторов, до этого примерно в 1949-50 Zuse начал вести семинар в Мюнхене по своим работам, в том числе по Plankalkul, на котором начали обсуждение компиляторов, кое-какие важные идеи, типа использование стека и пр. возникли в то время, Siemens поддерживала эти работы
smplcd
08.10.2024 07:48+1например https://github.com/SergeGris/BCPL-compiler - я взял отсюда, он легко компилируется под Linux / FreeBSD.
Очень приятно, что мой крайне старый репозиторий нашли и он оказался кому-то полезен :)
Хочу, на всякий случай, обратить внимание, что данный компилятор мной был взят отсюда: https://www.nordier.com/
А конкретно по этой ссылке можно скачать архив исходников компилятора, на котором базируется мой репозиторий: https://www.nordier.com/software/distfiles/obcpl-0.9.8.tar.xz
Автор проделал большую работу над тем, чтобы портировать компилятор 1980-х годов на современные Linux и FreeBSD, стоит отдать ему должное.
RodionGork Автор
08.10.2024 07:48Занятно, насколько земля круглая :) Да, всё верно, ссылка на сайт Роберта Нордье у Вас упомянута. Позже я полазил и по его сайту, но Ваш действительно в гугле выпал первым когда я искал - ну и вроде это более-менее "топовая" реализация. Там нашлись нюансы с которыми ещё хочу поразбираться на досуге (flush потока например и некоторые сообщения об ошибках) - но в целом очень добротная штука!
unreal_undead2
Правило хорошего тона в C/С++ - инициализировать всегда. А вот синтаксис для инициализации нескольких разных переменных удобен, было бы неплохо иметь возможность написать auto i, ch = 0, 'a';
RodionGork Автор
Отличие в том что в C++ необязательно константой инициализировать - то есть сразу чтение можно было бы вызвать (если бы такая функция была) в инициализаторе. А здесь в любом случае две строчки.