![](https://habrastorage.org/getpro/habr/upload_files/2a4/fda/41d/2a4fda41d91acdfe845ca0d0c2b2139c.jpg)
Мой интерес к дизайну языков программирования приводит меня иногда к интересным, но почти неизвестным в широких кругах проектам. Один из таких проектов - язык C∀ (CForAll), разрабатываемый в University of Waterloo. C∀ является расширением ISO C и обеспечивает обратную совместимость с C. Помимо исправления некоторых недостатков Си (которые можно исправить без нарушения обратной совместимости), в C∀ есть некоторые весьма интересные и оригинальные фичи: некоторые расширения классических управляющих операторов, альтернативный синтаксис объявления квалификаторов, кортежи и множественные операции, оригинальное расширение ссылок, полиморфизм, сопрограммы и т.д.
Данная статья представляет собой краткий конспект официального руководства по языку, с некоторыми моими комментариями.
Лексика
Первое мелкое, но важное улучшение - подчеркивания в числовых литералах. При этом несколько подчеркиваний подряд ставить нельзя.
2_147_483_648; // decimal constant
56_ul; // decimal unsigned long constant
0_377; // octal constant
0x_ff_ff; // hexadecimal constant
0x_ef3d_aa5c; // hexadecimal constant
3.141_592_654; // floating constant
10_e_+1_00; // floating constant
0x_ff_ff_p_3; // hexadecimal floating
0x_1.ffff_ffff_p_128 l; // hexadecimal floating long constant
L_"\x_ff_ee "; // wide character constant
Если требуется объявить идентификатор, совпадающий с ключевым словом, то используется вот такой синтаксис:
int ``otype = 3; // make keyword an identifier
double ``forall = 3.5;
Выражения и управляющие операторы
Оператор возведения в степень
Во многих языках пытаются его ввести, и везде придумывают разные операторые символы (^, ** и т.п.). На этот раз обратный слэш. Приоритет, как и полагается, выше чем у умножения.
x\y; // pow(x,y)
x\=y; // x=pow(x,y)
Улучшенный оператор множественного выбора
По недостаткам "сишного" оператора switch
проходятся все кому не лень. Всем известная особенность - в большинстве случаев нам не нужно выполнение всех последующих case-блоков после того, как был выполнен необходимый нам блок. Для предотвращения выполнения последующих блоков используется break
, однако программисты иногда забывают его поставить. Во всех современных языках уже перешли на match
, лишенный этого недостатка. Кроме того, в сишном switch
можно вставлять недостижимый код перед первым case
, и накладывать на switch другие блоки кода (Duff’s device). Все это запрещено в новом операторе choose
. В нем break
не требуется, а если нужно явно перейти к следующему блоку - предлагается использовать оператор fallthru
(или fallthrough
- первый раз вижу, чтобы два ключевых слова обозначали одно и то же).
Внутри case можно объявлять несколько паттернов через запятую; применять диапазоны; и несколько диапазонов через запятую.
switch ( i ) {
case 1, 3, 5: // ...
case 10 ∼ 15: // ...
case 1 ∼ 5, 12 ∼ 21, 35 ∼ 42: // ...
}
Диапазоны
Кстати, вот синтаксис диапазонов
1 ∼ 5 // полуоткрытый диапазон [M,N) : 1,2,3,4
1 ∼ =5 // закрытый диапазон [M,N] : 1,2,3,4,5
1 -∼ 5 // полуоткрытый диапазон [N,M) : 4,3,2,1
1 -∼=5 // закрытый диапазон [N,M] : 5,4,3,2,1
1 ∼ @ // диапазон до бесконечности
1 ∼ @ ∼ 2 // диапазон с указанием шага: 1,3,5,7,...
Диапазоны используются не только в case, но и в циклах. По сути эти диапазоны - частный случай List Comprehension ("списковых включений" или "генераторов списков", т.е. неких языковых конструкций, которые изнутри - вычисляемые выражения, а снаружи их можно применять вместо списков значений).
Улучшенные циклы
Некоторые маленькие улучшения в циклах
Цикл for по количеству элементов и по диапазонам
for ( i; 5 ) // typeof(5) i; 5 is comparison value
for ( i; 1.5 ∼ 5.5 ∼ 0.5 ) // typeof(1.5) i; 1.5 is start value
for ( 5 ) // for ( typeof(5) i; i < 5; i += 1 )
Циклы без аргументов - бесконечные циклы
(напомню, в С/С++ можно было написать for(;;)
, а для while все равно указывать аргумент true
)
while ( / * empty * / ) // while ( true )
for ( / * empty * / ) // for ( ; true; )
do ... while ( / * empty * / ) // do ... while ( true )
Метки для break и contunue
Как в Java - операторы могут передавать управление на метки. Такие переходы не могут образовывать цикл, и не могут вести внутрь управляющих структур; допускаются только выходы наружу из вложенных блоков.
Блок else для циклов
Как в Python - управление передается в блок else
, если внутри цикла не сработал break (т.е. выход из цикла был естественный, по условию самого цикла).
Оператор with
Оператор создает новую область видимости и представляет поля своего аргумента как локальные переменные внутри своего блока. Позаимствован из языка Pascal, и обобощает возможность С++ неявно обращаться к полям указателя this
. Может применяться к функциям или как отдельный блок. Может раскрывать сразу несколько составных объектов. При этом могут возникнуть конфликты имен, которые разрешаются обычным способом - явным уточнением.
void f( S & this ) with ( this ) {
c; i; d; // this.c, this.i, this.d
}
struct Q { int i; int k; int m; } q, w;
struct R { int i; int j; double m; } r, w;
with ( r, q ) {
j + k; // unambiguous, r.j + q.k
m = 5.0; // unambiguous, q.m = 5.0
m = 1; // unambiguous, r.m = 1
int a = m; // unambiguous, a = r.i
double b = m; // unambiguous, b = q.m
int c = r.i + q.i; // disambiguate with qualification
(double)m; // disambiguate with cast
}
Обработка исключений
Про обработку исключений написано немного, но заявляется, что поддерживаются как обычные исключения с раскруткой стека, так и исключения с возобнолением. Добавлен и отсутствующий в C++ оператор finally.
// E - тип исключения
void f() {
// ...
throwResume E{}; ... // resumption
// ...
throw E{}; // termination
}
try {
f();
}
catch( E e ; boolean-predicate ) { // termination handler
// recover and continue
}
catchResume( E e ; boolean-predicate ) { // resumption handler
// repair and return
}
finally {
// always executed
}
Альтернативный синтаксис объявления квалификаторов
Синтаксис объявления переменных С/С++ славится своей сложностью - особенно когда дело касается типов и объектов со множеством квалификаторов, что-то вроде массива указателей на функции, принимающих массивы указателей и возвращающих указатели на массивы. Какие-то квалификаторы относятся к типу, какие-то - к объекту; одни пишутся перед именем переменной, другие - после. Это приводит к различным неоднозначностям, которые я возможно опишу в другой статье.
В современных языках для упрощения парсинга и борьбы с неоднозначностями обычно применяют форму с начальным ключевым словом (var
, let
и т.п.) и "типом справа" по отношению к объектам. В С∀ решили сохранить "тип слева", но разместили все квалификаторы перед типом. В результате синтаксис стал однозначным, чтение и запись - простыми и естественными. Вот прямо так: "указатель на массив из 10 указателей на целое": *[10]*int
. Да, теперь все квалификаторы относятся к типу, и исчезла возможность объявлять разные квалификаторы для разных переменных в одной строке; но и в других языках (в которых "тип справа") этой возможности нет. Фактически, это исключительная особенность С/С++, не так уж и часто используемая.
Еще примеры. Читается очень просто, не так ли?
const * [ 5 ] const int y; // const pointer to array of 5 const integers
const * const int x; // const pointer to const integer
static * const int y; // internally visible pointer to constant int
Что интересно, в С∀ в целях обратной совместимости оставили и старый синтаксис деклараций. Понятно что это криво, но здесь интересна сама идея того, как можно упростить сложные декларации, а не конкретная реализация.
Указатели и ссылки
В большинстве языков указатели требуют явного разыменования. В некоторых языках, таких как Алгол68, осуществляется неявное разыменование указателей в выражениях.
p2 = p1 + x; // compiler infers *p2 = *p1 + x;
Неявное разыменование дает более компактный код. Оно подходит, если работа со значениями значительно более востребована, чем работа с адресами. Для сравнения:
* p2 = (( * p1 + * p2) * ( ** p3 - * p1)) / ( ** p3 - 15);
p2 = ((p1 + p2) * (p3 - p1)) / (p3 - 15);
Для поддержки неявного разыменования в С∀ введены ссылки. Они похожи на ссылки C++, но отличаются в некоторых аспектах.
int x, y, & r1, & r2, && r3;
&r1 = &x; // r1 points to x
&r2 = &r1; // r2 points to x
&r1 = &y; // r1 points to y
&&r3 = &&r2; // r3 points to r2
r2 = ((r1 + r2) * (r3 - r1)) / (r3 - 15); // implicit dereferencing
Инициализация ссылок осуществляется конструкцией &r = &v
. Важно, что инициализация синтаксически отличается от присваивания r = v
. В С++ для обоих действий используется обычное присваивание, хотя по смыслу действия были разные - в первом случае брался адрес переменной и сохранялся в ссылке (неявном указателе), во втором - значение переменной записывается по адресу, хранимому в ссылке.
Ссылки могут быть двойными, тройными и т.д. (конструкция && r3
). Это полностью аналогично указателям: ссылка на ссылку означает, что переменная - неявный указатель, хранящий адрес другого неявного указателя. Указатели и ссылки взаимозаменяемы, поскольку оба содержат адреса. Отличается только синтаксис
int x, *p1 = &x, **p2 = &p1, ***p3 = &p2, &r1 = x, &&r2 = r1, &&&r3 = r2;
*** p3 = 3; // change x
r3 = 3; // change x, ***r3
** p3 = ...; // change p1
&r3 = ...; // change r1, (&*)**r3, 1 cancellation
* p3 = ...; // change p2
&&r3 = ...; // change r2, (&(&*)*)*r3, 2 cancellations
&&&r3 = p3; // change r3 to p3, (&(&(&*)*)*)r3, 3 cancellations
Ссылки могут иметь квалификаторы. Сама ссылка & является квалификатором и подчиняется тем же правилам, что и квалификатор указателя и массива.
В противоположность С∀ , ссылки С++ являются неизменяемыми однократно инициализируемыми. Также С++ не поддерживает массивы ссылок.
Смысл инициализации ссылок отличается от присваивания, поскольку она происходит для пустого (неинициализированного) объекта (т.е. до инициализации ссылка никуда не указывает). Поэтому имеет смысл только семантика адреса, т.е. само инициализирующее значение должно быть адресом. Маловероятно, что присвоение значения x указателю осмысленно. Следовательно, при инициализации ссылки требуется адрес, и излишне требовать явного взятия адреса объекта, которым инициализируют ссылку.
int * p = &x; // assign address of x 9
int * p = x; // assign value of x 10
int & r = x; // must have address of x
По той же причине не требуется оператор взятия адреса при передаче по ссылке в функцию. Также если возвращаемое значение - ссылка, то при присваивании результата оператор ссылки не требуется.
int & f( int & r ); // reference parameter and return
z = f( x ) + f( y ); // reference operator added, temporaries needed for call results
Можно получать адреса и ссылки литералов и выражений. Причем эти адреса и ссылки могут быть не только константными, но и изменяемыми. Компилятор сам создает необходимые временные объекты и использует их адреса.
void f( int & r );
void g( int * p );
f( 3 );
g( &3 ); // compiler implicit generates temporaries
f( x + y );
g( &(x + y) ); // compiler implicit generates temporaries
Подводя итоги: интуитивно понятно, что авторы сделали полностью симметричную систему ссылок и указателей, с той лишь разницей, что у указателя - явное разыменование при доступе к значению, а у ссылки соответствующая lvalue-операция "взятия адреса" при изменении самой ссылки. Выглядит, по правде говоря, мозгодробительно. Но зато ссылка стала first-class объектом (пусть и специфическим). Стоило ли делать вот именно так? Насколько часто востребована операция неявного разыменования, если все равно используются различные "умные указатели"?
Перечисления
Кроме обычных, в С∀ доступны нецелочисленные (по сути - объектные) перечисления. Базовый тип указывается в круглых скобках после ключевого слова enum.
enum( double ) Math { PI_2 = 1.570796, PI = 3.141597, E = 2.718282 }
enum( char * ) Name { Fred = "Fred" , Mary = "Mary" , Jane = "Jane" };
enum( [int, int] ) { T1 = [ 1, 2 ], T2 = [3, 4] }; // tuples
enum( S ) s { A = { 5, 6 }, B = { 7, 8 } }; // struct S { int i, j; };
Перечисления одного типа могут "наследоваться" - точнее, "включаться" одно в другое.
enum( char * ) Name2 { inline Name, Jack = "Jack" , Jill = "Jill" };
enum Name3 { inline Name2, Sue = "Sue" , Tom = "Tom" };
Разумеется, имена в базовом перечислении и в перечислении-наследнике должны быть уникальными.
Структуры
Безымянные поля структур
Можно вставлять в структуры любое количество безымянных полей. Это может быть полезно, например, для формирования каких-то структур с зарезервированными полями.
struct {
int f1; // named field
int f2 : 4; // named field with bit field size
int : 3; // unnamed field for basic type with bit field size
int ; // disallowed, unnamed field
int * ; // disallowed, unnamed field
int ( * )( int ); // disallowed, unnamed field
};
Вложенные структуры
В Си можно описывать одну структуру внутри другой, но она все равно будет располагаться в глобальном пространстве имен. В С∀ исправили эту странность - имя объемлющей структуры стало пространством имен для вложенной. В отличие от С++, в С∀ для доступа ко вложенным сущностям используется точка, а не ::
Встраивание
Одну структуру можно встроить в другую так, как это реализовано в Go (и даже лучше - используется ключевое слово inline
). Это очень простая и в то же время мощная концепция, прямо готовая для proposal'а в очередной стандарт С и/или С++... Удивительно - почему ее сразу не сделали в Си?
struct Point { double x, y, x; };
struct ColoredPoint {
inline Point; // anonymous member (no identifier)
int Color;
};
ColoredPoint cp;
cp.x = 10.3; // x from Point is accessed directly
cp.Color = 0x33aaff;
Кортежи
Множественный возврат из функций
В Си и в большинстве языков программирования функция возвращает только одно значение; но иногда нужно больше. Чтобы вернуть больше, применяют агрегацию (возврат структуры с несколькими полями) или возврат через аргументы по указателю/ссылке. В С∀ сделана попытка реализации непосредственного возврата из функции нескольких значений. Для этого используются квадратные скобки:
[ char, int, double ] f()
{
return [ 'c', 123, 3.14 ];
}
Использование нескольких возвращаемых значений:
int quot, rem; 37
[ quot, rem ] = div( 13, 5 );
Каждый элемент кортежа может быть чем угодно, в т.ч. и другим (вложенным) кортежем.
Передача сразу нескольких значений в функцию
При передаче в функцию кортеж разворачивается сразу в несколько значений.
printf( "%d %d\n" , qr ); // print quotient/remainder
Объявление объектов кортежей
[ double, int ] di;
[ double, int ] * pdi;
[ double, int ] adi[10];
Можно объявить и сразу инициализировать, например возвратом из функции
[int, int] qr = div( 13, 5 );
Доступ к отдельным элементам
Похоже на реализацию в Swift: используются константные номера элементов и оператор "точка" (а для указателей и "стрелка").
[int, double] x;
[char * , int] f();
[int, double] * p;
int y = x.0; // access int component of x
y = div(20,7).1; // access int component of functions return
p->0 = 5; // access int component of tuple pointed-to by p 39
y = [ f(), x ].1.0; // access first component of second component of tuple expression
Флаттернизация и структуризация
Кортежи не имеют жесткой структуры и могут при необходимости структурироваться и деструктурироваться. Функция, принимающая несколько аргументов, может принять соответствующий кортеж, и наоборот - функция, принимающая кортеж, может принять несколько аргументов. Т.е. кортежи могут неявно раскрываться и наоборот, неявно формироваться в соответствии с контекстом. Например, есть функция и ее вызов:
int f(int, [double, int]);
f([5, 10.2], 4);
Сначала список аргументов раскрывается в 5, 10.2, 4
, затем структурируется в 5, [10.2, 4]
.
Присваивание
Предусмотрено "массовое" и "множественное" присваивание
[y, x] = 3.14; // mass assignment
[x, y] = [y, x]; // multiple assignment
При множественном присваивании размеры кортежей должны совпадать. Оба вида присваивания распараллеливаются.
Множественный доступ к полям структур
Еще одна красивая фича:
struct S { char x; int y; double z; } s;
s.[x, y, z] = [ 3, 3.2, ' x ' ];
f( s.[ y, z ] );
Функции
Именованные аргументы
Функцию вида void foo( int x, int y, int z ) {...}
можно вызывать с именованными аргументами:
foo( z : 3, x : 4, y : 7 );
Аргументы по умолчанию
Фича как в С++ и множестве других языков. Однако, можно пропускать аргументы не только в конце списка, но и в любом другом месте, просто поставив нужное количество запятых.
void p( int x = 1, int y = 2, int z = 3 ) {...}
p(); // rewrite ⇒ p( 1, 2, 3 )
p( 4, 4 ); // rewrite ⇒ p( 4, 4, 3 )
p( , 4, 4 ); // rewrite ⇒ p( 1, 4, 4 ) -- можно пропускать в начале!
p( 4, , 4 ); // rewrite ⇒ p( 4, 2, 4 ) -- и в середине
p( , , 4 ); // rewrite ⇒ p( 1, 2, 4 )
p( , , ); // rewrite ⇒ p( 1, 2, 3 )
Вложенные функции
В С∀ функции можно объявлять внутри других функций. Такие вложенные функции не являются first-class объектами, т.е. это не полноценные "лямбды" с "замыканиями", их нельзя возвратить из функции в качестве результата и они не захватывают объекты из объемлющего контекста. Но тем не менее вложенные функции могут обращаться к локальным переменным объемлющих. Такая реализация ближе всего к вложенным процедурам Pascal.
int foo() {
int i = 7;
void bar( int j ) {
i += j;
}
bar(10);
return i;
}
Постфиксные функции
Альтернативный синтаксис вызова, при котором аргумент указывается перед именем функции. Обычно используется для преобразования базовых литералов в пользовательские литералы, где ?`
обозначает имя постфиксной функции, а
`
обозначает вызов постфиксной функции. Например, следующий код преобразует литералы, представляющие физические величины, в другие единицы измерения.
double ?`ft(double f) { return f / 3.28084; }
printf("100 feet == %f meters\n", 100`ft);
Постфиксные функции могут быть и с несколькими аргументами, в этом случае постфикс применяется к кортежу.
Перегрузка
Перегрузка функций
Язык поддерживает перегрузку функций. Что интересно, заявлена поддержка перегрузки по возвращаемому значению.
int f();
double f();
f(); // ambiguous
(int)f(); // choose "int f()"
Перегрузка операторов
Также можно перегружать операторы. Используется специальный синтаксис со знаками вопроса, обозначающими операнды. Оператор {} - это "конструктор", используемый для инициализации объектов.
type Complex = struct {
double real;
double imag;
}
void ?{}(Complex &c, double real = 0.0, double imag = 0.0) {
c.real = real;
c.imag = imag;
}
Complex ?+?(Complex lhs, Complex rhs) {
Complex sum;
sum.real = lhs.real + rhs.real;
sum.imag = lhs.imag + rhs.imag;
return sum;
}
Перегрузка переменных
Да, такое тоже возможно. Несколько переменных разных типов с одним именем:
int pi = 3;
float pi = 3.14;
char pi = .p.;
Полиморфизм
Одна из ключевых особенностей С∀ - это перегружаемые параметрически-полиморфные функции, обобщенные с помощью оператора forall (язык назван именно по этому ключевому слову). Внешне - что-то вроде шаблонной функции:
forall( otype T ) T identity( T val ) { return val; }
int forty two = identity( 42 );
Эту функцию можно применить к любому полному объектному типу (otype
). C∀ передает размер и выравнивание типа, представленного параметром otype
, а также оператор присваивания, конструктор, конструктор копирования и деструктор. Если эта дополнительная информация не нужна, например, для указателя, параметр типа может быть объявлен как тип данных (dtype
). Еще бывает ftype
(функциональный тип) и ttype
(тип-кортеж).
Поддерживается что-то вроде концептов. Например, это работает для всех типов, поддерживающих сложение:
forall(otype T | { T ?+?(T,T); }) T twice (T x) { return x+x; }
int val = twice(twice(3));
Уcловия концептов можно группировать в трейты
trait sumable( otype T ) {
void ?{}( T &, zero_t ); // constructor from 0 literal
T ?+?( T, T ); // assortment of additions
T ?+=?( T &, T );
T ++?( T & );
T ?++( T & );
};
и затем использовать
forall( otype T | sumable( T ) ) // polymorphic, use trait
T sum( T a[ ], size_t size ) {
T total = 0; // instantiate T from 0 by calling its constructor
for ( i; size ) total += a[i]; // select appropriate +
return total;
}
int sa[ 5 ];
int i = sum( sa, 5 ); // use int 0 and +=
Паралеллизм
Заявлена поддержка стековых и бесстековых сопрограмм, а также потоков. Интересное решение - имя main с аргументом соответствующего типа используется как имя главной функции (точки входа) сопрограммы или потока.
Генераторы
Генераторы - бесстековые сопрограммы (т.е. они используют стек вызывающей стороны). Ключевое слово suspend используется для приостановки корутины и возврата управления в вызывающий контекст, и ключевое слово resume - для возобновления выполнения корутины с того места, где она вернула управление.
generator Fibonacci {
int fn; // used for communication
};
void main( Fibonacci & fib ) { // called on first resume
int fn1, fn2; // retained between resumes
fib.fn = 0;
fn1 = fib.fn; // 1st case
suspend; // restart last resume
fib.fn = 1;
fn2 = fn1;
fn1 = fib.fn; // 2nd case
suspend; // restart last resume
for () {
fn = fn1 + fn2; fn2 = fn1; fn1 = fn; // general case
suspend; // restart last resume
}
}
int next( Fibonacci & fib ) {
resume( fib ); // restart last suspend
return fib.fn;
}
Корутины
Стековые сопрограммы также поддерживаются
#include <fstream.hfa>
#include <coroutine.hfa>
// match left/right parenthesis: ((())) match, (() mismatch
enum Status { Cont, Match, Mismatch };
coroutine CntParens {
char ch; // used for communication
Status status;
};
void main( CntParens & cpns ) with( cpns ) { // coroutine main
unsigned int cnt = 0;
for ( ; ch == '('; cnt += 1 ) suspend; // count left parenthesis
for ( ; ch == ')' && cnt > 1; cnt -= 1 ) suspend; // count right parenthesis
status = ch == ')' ? Match : Mismatch;
}
void ?{}( CntParens & cpns ) with( cpns ) { status = Cont; }
Status next( CntParens & cpns, char c ) with( cpns ) { // coroutine interface
ch = c;
resume( cpns );
return status;
}
int main() {
CntParens cpns;
char ch;
for () { // read until end of file
sin | ch; // read one character
if ( eof( sin ) ) { sout | "Mismatch"; break; } // eof ?
Status ret = next( cpns, ch ); // push character for checking
if ( ret == Match ) { sout | "Match"; break; }
if ( ret == Mismatch ) { sout | "Mismatch"; break; }
}
}
Мониторы
Конструкции, аналогичные структурам, но с неявной (генерируемой компилятором) защитой полей от одновременного доступа из разных потоков.
monitor Account {
const unsigned long number;
float balance;
};
Потоки
#include <fstream.hfa>
#include <thread.hfa>
thread T {
int id;
};
void ?{}( T & t ) { t.id = 0; }
void ?{}( T & t, int id ) { t.id = id; }
void main( T & t ) with( t ) { // thread starts here
sout | id;
}
int main() {
enum { NumThreads = 5 };
T t[ NumThreads ]; // create/start threads
T * tp[ NumThreads ];
for ( i; NumThreads ) {
tp[i] = new( i + 1 ); // create/start threads
}
for ( i; NumThreads ) {
delete( tp[i] ); // wait for thread to terminate
}
}
Что дальше
Это очень краткий обзор, упускающий многие детали и не рассматривающий некоторые вопросы. Страница проекта. На сайте есть ссылка на гитхаб. Также в сети есть несколько публикаций, посвященных разным аспектам языка.
Все это можно проверить (что я и делал в некоторых непонятных случаях): исходники компилятора и инструкции для сборки доступны на гитхабе. Единственная особенность - сборка зачем-то требует рута, нужна запись в /usr/local/bin и возможно еще куда-то. Наверное это можно исправить, но я не специалист в сборочных скриптах make, да и под виртуалкой как-то без разницы.
Лично мне всегда интересно посмотреть на альтернативные языки программирования, альтернативные и расширенные реализации привычных вещей. То, что язык не слишком сильно отличается от самых распространенных языков - для меня плюс, не нужно ломать мозги для адаптации к новому синтаксису, а можно спокойно изучать новые идеи в привычном окружении. Надеюсь что вам тоже понравилось.