По большому счёту мне давным давно пофиг, на каком языке писать (я программирую с 1989 года, и за это время перепробовал кучу языков). Но… всё таки на одних языках работать приятнее, чем на других — и здесь дело не в том, что одни языки лучше других, а в том, что для разных стилей мышления лучше подходят разные языки.
Переход от Perl к Limbo — очень контрастный. Языки совершенно разные: Perl — не типизированный вообще, Limbo — сильно типизированный; в Perl нет нормальной поддержки нитей и асинхронности приходится добиваться через мультиплексирование, Limbo — чуть ли не вынуждает писать именно многопоточные программы (если вы смотрели презентацию Роба Пайка, то там был прикольный пример с многопоточным поиском простых чисел); etc. И, тем не менее, Limbo мне очень понравился и писать работающий код я на нём начал практически сразу.
Я уже не очень хорошо помню C, но попробую описать Limbo именно в плане отличий от C — думаю, так будет проще для большей части аудитории (и ни слова про PHP! :)).
Общая информация
Про такие особенности Limbo как схожесть синтаксиса с C, высокая портабельность байт-кода, заточенность под параллельное программирование, динамическая подгрузка/выгрузка модулей, проверку типов и границ массивов в том числе и в процессе выполнения и наличие сборщика мусора я уже упоминал.
Ещё можно добавить, что для Limbo написано значительное кол-во разнообразных библиотек (идут в комплекте с Inferno), облегчающих работу с графикой, математикой, базами данных, etc.
Для понимания примеров стоит добавить, что объявление типа переменной делается в паскалевском стиле:
:— объявление=— присваивание:=— объявление с одновременным присваиванием, тип определяется по типу присваиваемого объекта
Типы данных
Помимо обычных числовых типов, структур и union, Limbo поддерживает строки и несколько более специфических типов данных: списки, массивы, tuples и каналы. (Ещё есть специальный тип «модуль», я его упоминал ранее когда описывал интерфейсы, но с точки зрения особенностей языка он интереса не представляет.) Все эти типы данных это first-class variables, т.е. их можно сохранять в переменных, передавать через каналы, etc.
Обычные числовые типы можно преобразовывать друг в друга, кроме того строки тоже можно преобразовывать в числа и наоборот. Но все преобразования должны указываться явно, неявных преобразований типов нет.
Строки
string можно преобразовывать в массивы байт, и наоборот.Кроме этого строки поддерживают срезы, т.е. можно обратиться к конкретному символу или последовательности символов, например:
my_string[5:15].Списки
list это последовательность элементов одного типа оптимизированная для стеко-подобных операций (добавить элемент в начало списка, получить первый элемент списка, получить остаток списка (кроме первого элемента)).Для работы со списками есть три оператора:
::— создание нового списка, левый операнд это один элемент, правый это список элементов того же типаhd— возвращает первый элемент списка не меняя сам списокtl— возвращает список состоящий из второго и последующих элементов заданного списка — т.е. «выкусывает» первый элемент
Пример:
l : list of int; l = 10 :: 20 :: 30 :: nil; # создаём список из 3-х элементов l = 5 :: l; # добавляем в начало ещё один i := hd l; # получаем int равный 5, список не изменился l2 := tl l; # получаем новый список 10 :: 20 :: 30 :: nil l2 = tl l2; # удаляем из него первый элемент
Почему работа со списками ограничена такими операциями понятно — их очень просто эффективно реализовать, и работать такие списки будут очень быстро. И, действительно, достаточно часто необходимо работать со структурами, которые вполне вписываются в имеющуюся функциональность.
Массивы
array содержит фиксированное кол-во элементов одного типа.Размер массива указывается при его создании/инициализации, а не при объявлении типа переменной — т.е. массивы можно динамически создавать в любой момент (когда стал известен требуемый размер массива).
Фактически в Limbo только два способа динамически выделить память: создать array указав требуемый размер через переменную, и добавить новый элемент в начало list.
Естественно, массивы тоже поддерживают срезы.
Tuples (кортежи)
tuple это что-то вроде списка из 2-х и более элементов любых типов. И это не просто список, а такой же тип данных, как и другие — тип самого tuple фактически определяется по тому, каких типов элементы и в каком порядке он содержит. Пример:i_s : (int, string); i_s = (5, "five"); # тип i_r_s_s это (int, real, string, string) i_r_s_s := (5, 0.5, "five", "comment");
Причём tuple можно «разбирать» на составляющие присваивая его в список обычных переменных:
# создаёт переменные i типа int и s типа string и # инициализирует их значениями 5 и "five" (i, s) := i_s;
Кстати, обмен значений двух переменных на Limbo делается примерно так:
(i, j) = (j, i);
Каналы
Каналы (
chan) позволяют организовывать IPC между локальными процессами передавая атомарно объекты заданного типа.Чтение/запись канала это блокирующая операция. Операторы чтения/записи выглядят как стрелки:
c := chan of int; # создаёт канал c <-= 10; # отправить в канал i := <-c; # принять из канала int <-c; # принять и проигнорировать значение c = nil; # уничтожить канал
Каналы бывают буферизированные (размер буфера вы указываете примерно так же, как размер массива). Запись в буферизованные каналы не блокируется пока не будет заполнен буфер. Буфер работает как FIFO очередь.
Для мультиплексирования каналов в Limbo есть целых два средства — можно читать из массива каналов, а можно использовать специальный оператор alt для выбора канала.
alt { i := <-inchan => sys->print("received: %d\n", i); outchan <-= "message" => sys->print("message sent\n"); }
Собственно каналы это единственный способ IPC в Limbo, они используются и для передачи данных, и для синхронизации потоков, в общем полная замена всяким mutexes, semaphores, shared memory, etc…
Что касается их производительности… при передаче чего-то через канал передаётся просто его адрес в памяти, т.е. никакого копирования на самом деле не происходит и всё просто летает.
Составные типы
cool : array of chan of (int, list of string);
это массив хранящий каналы, по которым передаются tuple состоящие из int и списка строк. Размер массива здесь не определяется, он будет задан в процессе выполнения, при инициализации массива.
Unicode
Limbo использует UTF8 для I/O, и UTF16 для представления строк в памяти.
Т.е., например при считывании исходника модуля с диска в нём может использоваться UTF8 в комментариях, строках и символьных константах.
Если есть массив байт (
array of byte) и он конвертируется в строку, то байты из массива обрабатываются как UTF8 и конвертируются в строке в UTF16; а при преобразовании строки в массив байт происходит обратное преобразование и в массиве оказывается UTF8.Функции
Функциям можно передавать параметрами ссылки на функции.
ООП
Объекты симулируются через тип данных структура (
adt), элементами которых помимо обычных типов данных могут быть функции. На самом деле это, безусловно, очень кастрированное ООП — наследования нет, ничего нет, населена роботами. (с) :) Впрочем, вру. Полиморфизм — есть. Но немного странный, больше напоминает templates в C++: смотрите сами.Нити
Для запуска заданной функции в отдельной нити в Limbo используется встроенный оператор spawn.
Ошибки и исключения
Поддержка исключений есть, как обычных строковых, так и пользовательских типов. К моему сожалению большинство системных и библиотечных функций вместо исключений для возврата ошибок используют tuple:
(errcode, result). Безусловно, tuple это большой шаг вперед относительно POSIX-овского возврата информации об ошибке в виде результата -1, но… хотелось бы чтобы вместо этого использовались исключения.Ссылки
Ну и на закуску полное описание Limbo на русском. По сути это примерно на 99.9% полный пересказ англоязычной доки по Limbo своими словами и иначе структурированный (мне, как Perl-программисту, хотелось сделать упор на типы данных и операции над ними, а то я от типизированных языков успел отвыкнуть).