POWERMAN
"In each of us sleeps a genius...
and his sleep gets deeper everyday."

Поскольку меня Inferno привлекает именно как среда разработки, то помимо архитектуры самой системы немалое значение имеет язык программирования.

По большому счёту мне давным давно пофиг, на каком языке писать (я программирую с 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.

Обычные числовые типы можно преобразовывать друг в друга, кроме того строки тоже можно преобразовывать в числа и наоборот. Но все преобразования должны указываться явно, неявных преобразований типов нет.

Строки.

Строки можно преобразовывать в массивы байт, и наоборот.

Кроме этого строки поддерживают срезы, т.е. можно обратиться к конкретному символу или последовательности символов, например: 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…

Что касается их производительности… я не уверен на 100%, но по-моему при передаче чего-то через канал передаётся просто его адрес в памяти, т.е. никакого копирования на самом деле не происходит и всё просто летает.

Составные типы.

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++: http://9fans.net/archive/2004/05/373

Нити.

Для запуска заданной функции в отдельной нити в Limbo используется встроенный оператор spawn.

Ошибки и исключения.

Поддержка исключений есть, как обычных строковых, так и пользовательских типов. К моему сожалению большинство системных и библиотечных функций вместо исключений для возврата ошибок используют tuple: (errcode, result). Безусловно, tuple это большой шаг вперед относительно POSIX-овского возврата информации об ошибке в виде результата -1, но… хотелось бы чтобы вместо этого использовались исключения.

Ссылки.

Ну и на закуску полное описание Limbo на русском: http://powerman.name/Inferno/Limbo.html По сути это примерно на 99% полный пересказ англоязычной доки по Limbo своими словами и иначе структурированный (мне, как Perl-программисту, хотелось сделать упор на типы данных и операции над ними, а то я от типизированных языков успел отвыкнуть).