Готовясь к собеседованиям по 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) - которая поэтому может быть актуальна для любителей изобретать новые языки даже сейчас.
Автор: RodionGork