Функциональные типы и объекты

Теперь, когда более или менее раскрыты основные дизайнерские решения для функций в Neo, можно рассмотреть устройство функциональных типов и объектов. Рассмотрим следующие вопросы:
- Выбор синтаксиса для функциональных типов
- Лямбды и блоки
- Синтаксис для частичного применения

Функциональный тип - это некий достаточно абстрактный тип данных, инкапсулирующий некоторую функцию и некоторые данные, связанные с этой функцией. Определение похоже на определение "класса", но в некотором смысле "обратное" ему (класс - это "набор данных и функций для работы с этими данными", т.е. в классе как-бы первичны данные, а в функциональном типе - функция). На самом деле функциональный тип, конечно же, тоже является классом, на него распространяются все возможности классов (такие например, как наследование).

Если некоторая функция имеет в качестве аргумента делегат, то в нее может быть передан
- собственно делегат
- функция
- метод класса
- лямбда-функция
- функтор

При этом все перечисленные объекты преобразуются и передаются именно в форме делегата. По способу передачи делегаты подобны объектам-строкам (всевозможным std::string, CString, TString и т.д.), то есть могут передаваться как по значению, так и по ссылке.

Функциональный тип однозначно описывается двумя списками: списком аргументов и списком возвращаемых значений. Среди множества вариантов я выбрал наиболее компактный: два списка, разделенные оператором лямбды "=>".
void=>void t1;
int=>int t2;
(int,int)=>float t3;
(char,bool)=>(double,string) t4;
Внутри самого объекта может быть что угодно: просто указатель на функцию, указатель на метод и this класса, дополнительные параметры функции и/или метода класса. Если рассматривать, к примеру, переменную t2 типа int=>int, то этой переменной можно присвоить:
- обычную функцию, например такую
def Foo(int x) int { return x+10; }
t2 = Foo;
- метод класса
class MyClass {
public def Method(int x) int { return x*2; }
};
MyClass obj;
t2 = obj.Method;
- лямбда-функцию
t2 = x => x+123;

Как видно, все эти функции совместимы по сигнатуре с "int=>int". Что произойдет, если попытаться присвоить функцональной переменной другую, несовместимую по сигнатуре функцию?

Если функция имеет меньше аргументов, чем наш функциональный тип, то присваивание возможно всегда: аргументы, передаваемые в функциональный тип, просто не будут использоваться.
def Bar() : int { return 100; }
t2 = Bar;
t2(33); // в Bar аргумент не передается

Если же функция имеет больше аргументов, то возникает необходимость осуществить частичное применение функции - то есть указать часть аргументов, которые будут сохранены в самом функциональном объекте (аналогично тому, как сохраняется указатель this). В различных языках эта возможность синтаксически реализована по-разному. В основом, используют символ-заполнитель "_" (подчеркивание) для указания аргументов, которые должны остаться аргументами. Например, в Nemerle

def f = WriteLine(_);
f("Частичное применение функции"); 


Но такой подход имеет ряд недостатков. В первую очередь, нет возможности получить функцию без параметров (она синтаксически не отличается от вызова). Также, в синтаксисе Neo символ подчеркивания позволяет обращаться к значениям аргументов по умолчанию.

Для решения этой проблемы я ввел новый синтаксис частичного применения. Он аналогичен вызову функции, но вместо круглых скобок используются фигурные.

def Baz(int x, y, z) int { return x+2*y - 3*z; }
t2 = Baz{_, 20,22};

В фигурных скобках подчеркивание уже означает "использовать аргумент как аргумент делегата" (для обращения к значению по умолчанию можно воспользоваться контекстной областью видимости "._", об этом как нибудь в другой раз).

Частичное применение для возвращаемых значений рассмотрю в другой раз.

Синтаксис функций - 2: аргументы

Мы определили, как лучше всего описывать функции. Но функция - это довольно сложный объект, обладающий многими фичами. Рассмотрим следующие вопросы:
- Передача аргументо в фукнцию (по порядку и по именам)
- Аргументы функций по умолчанию

Существует два способа передачи аргументов в функцию: по порядку и по именам. Практически во всех языках программирования реализован как правило, только один способ - передача по порядку. Тем ни менее, есть языки, где реализован также и второй способ.

Передача аргументов по порядку очевидна - аргументы просто перечисляются через запятую. Это стандартный синтаксис, применяемый практически во всех ЯП.
Передача по имени встречается гораздо реже.
Обычно для связывания имени и значения используют какие-то спецсимволы, типа стрелок -> => или чего-то подобного. Например,  в C# используется двоеточие:

foo( "Hello world",  x: 10.2, y: 29.3);

В Ada используется стрелочка:

foo(X => 5, Y => 3 * 45);

Сама по себе передача по имени - отличная идея, но вместо использования таких спецсимволов я решил ввести расширить понятие областей видимости, предоставив инструмент доступа к так называемой "контекстной" области видимости.

Любая функция - это область видимости. Имя функции также является именем пространства имен, а это значит, что по идее, возможен доступ к внутреннему содержимому функции через оператор "точка" (более того, такой доступ возможен для публичных статических переменных, объявленных внутри функции).

def Foo()
{
 public static int x;
};
Foo.x = 10;

В точке вызова функции создается стековый фрейм, условно соответствующий понятию "объект функции". То есть при вызове функции доступны также ее локальные переменные.

def Foo(int x, y) {}
Foo(Foo.x=10, Foo.y=20);

Фактически, это и есть передача аргументов по имени. Для сокращения синтаксиса я ввел понятие "контекстная область видимости". В отличие от прямой области видимости (все доступно напрямую, без каких-либо квалификаторов доступа), контекстная область доступна как-бы через некий "квалификатор по умолчанию", определяемый контекстом. В нашем случае этот контекст - вызов функции (круглые скобки), и поэтому окончательный вариант синтаксиса такой

Foo(.x=10, .y=20);


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



Возможность задания значений аргументов по уомлчанию встречается в языках гораздо чаще, чем передача по имени. Синтаксис вполне традиционный - указание значений аргументов при описании функции.



def Foo(int x = 0, int y = 0) {}



Если при вызове функции не указывать аргумент - будет взято значение по умолчанию.

В традиционных языках типа C++ и C# можно не указывать только крайние справа аргументы. В большинстве случаев этого достаточно, но для универсальности синтаксиса я ввел возможность обращаться к аргументу по умолчанию в любой позиции списка аргументов. Для этого используется универсальный символ - placeholder "_" (подчеркивание). Например, вышеописанная функция Foo() может быть вызвана следующим образом:
Foo(); // Foo(0,0), оба аргумента по умолчанию

Foo(1); // Foo(1,0), последний аргумент по умолчанию

Foo(1,2); // обычный вызов

Foo(_,3); // Foo(0,3), первый аргумент по умолчанию а второй задан явно

Foo(.y=3); // то же самое, но с передачей по имени

Foo(.y(3)); // то же самое, но инициализация в синтаксисе
конструктора

Foo(.y(_)); // забавный вариант - фактически все аргументы по умолчанию, но таким вот хитрым способом

Foo(1, _+4); // использование значения аргумента по умолчанию в арифметическом выражении
Foo(.y(_+4)); // тоже, но с передачей по имени


Последние два примера интересны тем, что в отличие от всех других языков, в Neo имеется возможность использовнания значений аргументов по умолчанию не напрямую, а в составе выражений. Для этого используется символ "_". При рассмотрении понятия "атрибуты" будет показано, как получить значения по умолчанию любых аргументов любых функций в любом месте программы (в Neo возможно и такое!). В большинстве же случаев такого синтаксиса будет вполне достаточно.











Синтаксис объявления функций

Говоря о функциональном программировании, невозможно не рассмотреть основы ФП - функции, их представление в виде кода.
С точки зрения дизайна языка, существует два классических способа объявления функции: си-подобный и функциональный.



1. классический Си-подобный:


возвращаемый_тип имя_функции(список_аргументов)


Такой способ достаточно компактен, более чем привычен (C, C++, Java, C#), но в плане дизайна не лишен некоторых недостатков. И хотя в простейших случаях они могут не проявляться, следующий способ в любом случае открывает больше возможностей.



2. классический функциональный


ключевое_слово имя_функции (список_аргументов) возвращаемые_значения



Способ применяется в PHP, JavaScript, Python, Ruby, Go, Rust, Scala, Nemerle. Даже в C++11 подобный способ появился как альтернатива традиционному определению функций (и там это вызвано вполне прагматическими причинами). Практически во всех современных языках разработчики выбирают именно этот способ, и это не случайно.
Особенностью этого способа является то, что объявление функции начинается с ключевого слова. Это удобно для различных парсеров, особенно для IDE. Кроме того, это удобно для программиста - как минимум, проще искать функции в коде. Такой способ позволяет унифицировать внешнйи вид объявления функций с другими объявлениями - переменных, структур, перечислений, классов и т.д. В случае, если функция шаблонная, вполне логично и иногда даже необходимо, чтобы возвращаемый тип был определен после (и на основе) принимаемых (это причина ввода нового синтаксиса в С++).


Кроме того, появляется возможность определить несколько ключевых слов для определения разных типов функций (особенно это касается методов классов).


Несмотря на все это, во многих языках объявление функций способом 2 получается более громоздким, чем "сишный" вариант. Это связано с тем, что 1) выбирают слишком длинные ключевые слова (function, procedure), и 2) вводят много "мусорного" синтаксиса (всякие двоеточия, стрелочки и т.п.).


Таким образом, каждый метод предваряется некоторым ключевым словом:
def — для впервые объявленных функций 
redef — для переопределенных (в дочерних классах)
virtual - для виртуальных
override - для переопределенных виртуальных


Кроме того, по аналогичным причинам имеет смысл использовать ключевые слова для конструкторов и деструкторов:
init - конструкторы
final — деструкторы



Никаких декоративных элементов (двоеточий, стрелок) в определении функции не используется. Также можно сократить длину за счет отказа от указания типа для каждого аргумента. Если несколько перечисляемых один за другим аргументов имеют один и тот же тип, то достаточно указать этот тип один раз в самом начале.


def Func2(int x, y) char
{
}



Дополнительно следует сказать про имена конструкторов и деструкторов. Если любой метод обязан иметь имя, то конструкторы и деструкторы - методы init и final могут как иметь произвольные имена, так и не иметь их. Имена, если они есть, подчиняются общим правилам именования методов. Опциональная возможность именования конструкторов и деструкторов дает дополнительную возможность самодокументирования кода. Варинат с именами:


init InitDefault() {}
init InitFromStr(string s) {}


или по-старинке:



init() {}
init(string s) {}


 если имя есть, то на функцию-конструктор можно сослаться, можно вызывать ее для переинициализации объекта, взять ее адрес, можно вызвать один конструктор из другого и т.д. Дополнительные преимущества такого подхода - имя класса освобождается от дополнительной нагрузки (в C++ и C# имена конструкторов и деструкторов совпадают с именем класса). Если предположить, что мы имеем дело с языком, в котором каждая программная сущность — объект, то снимается неоднозначность: имя класса — уникальный идентификатор объекта класса, имя конструктора — уникальный идентификатор объекта-функции.


Функциональное программирование 3 - лямбда функции и замыкания

В предыдущей статье были рассмотрены различные варианты синтаксиса для передачи одной функции в другую. Понятно, что можно заранее объявить функцию, и затем передать ее. Но довольно часто было бы удобнее описать передаваемую функцию прямо в списке передаваемых аргументов.
Функции, объявляемые "по месту их использования", называются лямбда-функциями (или лямбда-выражениями). В отличие от обычных функций, лямбды могут не иметь имени.

В C# разных версий лямбды эволюционировали до современного вида, пожалуй самого удобного из всех известных мне вариантов.

int[] ary = { 1, 2, 3 };
var x = 2;
var ary1 = ary.Select(elem => elem * x;);


Выражение "elem => elem*x" - типичная лямбда-функция. В нем до оператора => перечисляется список аргументов, после - собственно выражение, результат вычисления которого является возвращаемым значениям лямбды. Все лямбды передаются как делегаты.

В других языках синтаксис лямбд более громоздкий. Обычно используется ключевое слово типа function, lambda и т.д., после которого указывается список аргументов, а затем - тело функции. В C# роль определяющего ключевого слова играет оператор =>.

В данном примере лямбда-функции есть еще одна интересная особенность:  лямбда ссылается на локальную переменную x, объявленную вне самой лямбды. Такие функции называются замыканиями.

В общем случае замыкание — это функция, определенная в теле другой функции (т.е. не только лямбда, но и просто обычная вложенная функция). При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции.

Интересно, что вложенные функции существовали еще во времена Turbo Pascal. Уже тогда вложенные функции имели доступ к локальным переменным объемлющих функций (т.е. они как-бы являлись замыканиями), и были доступны только внутри объемлющих функций. Их реализация была довольно простой, потому что в Паскале не было делегатов. Дело в том, что с делегатами и замыканиями связана одна особенность: за счет делегатов вложенная функция может иметь время жизни дольше, чем объемлющая. Такое может быть, например, если объемлющая функция возвращает вложенную как результат своей работы.

В классическом Си можно попытаться возвратить, например, адрес локальной переменной. Адрес будет возвращен, но любые действия с ним после выхода из функции приведут к ошибке, так как стекового фрейма с этой локальной переменной уже не существует. Примерно такая же ситуация может возникнуть и с возвратом делегата, ссылающегося на вложенную функцию, которая использует локальную переменную объемлющей функции. Для решения этой проблемы можно воспользоваться разными подходами, например - размещение замкнутых переменных в динамической памяти со сборщиком мусора. В любом случае, замкнутые переменные уже не должны быть стековыми.

Функциональное программирование 2 - о синтаксисе

Прежде чем переходить к возможностям функциональных объектов, хотелось бы рассмотреть варианты синтаксиса для делегатов. Кроме рассмотренного уже синтаксиса указателей на функции, в разных языках программирования существуют разные по степени удобства варианты синтаксиса для делегатоподобных объектов.

Рассмотрим передачу делегатов в функции.

В C#, прежде чем использовать делегат как аргумент функции, нужно сначала объявить тип делегата. Например так:

delegate int MyDelegate(string s);

Здесь MyDelegate - тип, значениями которого могут быть любые методы, принимающие string и возвращающие int. Использовать его можно например так:

public void Process( MyDelegate  func)
{
    foreach (string s in someList)
    {
       func(b);
    }
}

В D тип делегата можно не объявлять, а объявлять прямо по месту использования.
int foo(int delegate(int x) dg)
{
    return dg(10) + 1;
}

В ObjC 2.0 существует аналогичное понятие "блоки". Подобно C# (и Си) можно определить тип блока (очень похоже на указатель на функцию):

typedef void (^MyBlock)(int);

Хотя можно определять блок и без типа. Например, функция  logBlock в качестве аргумента принимает другую функцию, принимающую int и возвращаюшую NSString*

void logBlock( NSString * ( ^theBlock )( int ) )
{
    NSLog( @"Block returned: %@", theBlock() );
}

В скриптовых языках все несколько проще. Объявлять типы не надо (так как это языки как правило с динамической типизацией).

function foo( callback )
{
    alert( callback() );
}
function bar()
{
    var str = 'hello, world';
    foo ( function() { return str; } );
}
bar();

В этом примере мы видим еще одну возможность функционального программирования - лямбда-функции, т.е. функции, определенные прямо в месте их использования (передачи в качестве аргумента). Такая возможность есть во многих языках с элементами ФП, но ее мы рассмотрим далее.

Во всех рассмотренных случаях (а можно рассматривать и другие языки) построение делегата достаточно громоздко и неудобно. Фактически, эти варианты мало чем отличаются от синтаксиса обычных указателей на функции, с их нагромождением скобок. Между тем, существуют и более компактные варианты. Например, Nemerle:

public static void Sort[T](ary : array[T], comparison : T * T -> int);

или Scala:

def sum(f: Int => Int, a: Int, b: Int): Int

В обоих примерах тип делегата задавался максимально просто и естестенно: "список входных параметров", "стрелочка", "список возвращаемых параметров". Ничего лишнего, все максимально наглядно и прозрачно. Именно такой вариант синтаксиса и взят за основу в Neo.



Функциональное программирование 1 - функциональные типы

Попробую собрать лучшие идеи ФП, возникшие у меня при разработке Neo, в нескольких статьях.
Итак, часть 1 - функциональные типы.
Фунциональный тип - это тип, представляющий функцию. В функциональном программировани, как известно, функцию можно передать в другую функцию как аргумент и возвратить из функции как результат. Это значит, что для функций нужны специальные "функциональные типы" (ФТ). Рассмотрим историю ФТ на примере различных языков программирования.

Простейшим ФТ является указатель на функцию. Указатели на функцию существуют в языке Си. Их синтаксис таков:
int (*pfunc)(float f);
Внутри такого объявления описывается имя переменной (pfunc), тип возвращаемого значения и аргументы ФТ. В принципе, этого могло бы быть достаточно, так как таким способом можно передать любую функцию. Но во многих случаях это бывает неудобно. Собственно, удобство - как раз то, из-за чего и появлились языки высокого уровня всесто Ассемблера.

Часто вместе с функцией хочется передать некоторые аргументы этой фунцкии.
Наиболее частый случай - функция является методом класса. Любой нестатический метод имеет один неявный аргумент, традиционно называемый "this". Это указатель (ссылка) на объект того класса, от которого вызывается метод. Указатель на функцию - это чистый адрес функции, места для каких-либо аргументов там нет. Для того, чтобы передать вместе с указателем еще и аргумент, вводится понятие "делегат".

Термин "делегат" возник в C# (хотя там понятие немного расширено - в C# это список указателей на функции с аргументами this). Я считаю, что список - это список, а разработчики C# немного намудрили. В языке D, к примеру, делегат - это именно одиночное значение.

Формально, делегаты могут быть указателями на глобальные функции (если они поддерживаются ЯП), статические методы или на нестатические методы классов. То есть это структуры данных, имеющие поле - указатель на функцию, и поле для указателя this. Что интересно, в C++ Builder какой-то довольно старой версии появилось ключевое слово __closure, с помощью которого можно было создавать как раз такие объекты.

Но не всегда передачи одного this достаточно. В самом деле, чем указатель this отличается от других параметров? Что делать, если хочется сохранить вместе с указателем на функцию и часть обычных параметров этой функции? В функциональном программировании это называется "частичное применение": когда новая функция создается на основе существующей путем частичного указания аргументов. Самое интересное, что такое можно сделать и на старом добром Си, просто определив новую функцию, которая вызывает старую:

int Sum(int x, int y) { return x+y; }
int Inc(int x) { return Sum(x,1); }

Но интересный вопрос - почему-бы не сделать такое с помощью делегатов?
Если пользоваться терминологией C#/D, то такие объекты уже правильнее называть именно "функциональными объектами". Они действительно полноценные объекты ООП - имеют поля (указатель на функцию и поля, которые нужно передать в качестве аргументов этой функции). Теоретически, они могут иметь собственные методы, т.е. функциональные типы могут являться полноценными классами. Здесь они пересекаются с функторами - классами, у которых переопределен оператор "круглые скобки" (). За счет этого с такими объектами можно работать как с обычными функциями.

По сути, функторы и делегаты - разные проявления одного большого понятия "функциональные объекты", возможности которых выходят далеко за рамки простого объединения возможностей функторов и делегатов. Об этом далее.

О синтаксисе

Каким должен быть синтаксис языка программирования?
Я считаю, что синтаксис должен быть кратким, чистым и си-подобным.

Краткость - это удобство программирования. Программы с длинными ключевыми словами и многословными конструкциями выглядят тяжело и неподъемно. В большинстве случаев программист знает, что он хочет написать, и заставлять его набивать текст  не есть хорошо (даже в современных IDE с автодополнением!).

Чистота синтаксиса - это особое, плохо формализуемое свойство, которое, тем ни менее, можно выразить так: все конструкции языка подчиняются небольшому, компактному набору "правил"; изучив одну конструкцию, можно предположить, как должна выгдядеть другая, подобная ей. Не должно быть исключений. Например, в Си аргументы операторов заключаются в круглые скобки. Это правило едино для всех операторов: if, for, while, switch, catch. Если в далеком будущем, в какой-то новой версии С++ появится еще какой-нибудь оператор, можно быть уверенным, что его аргументы будут заключены именно в круглые скобки.

Си-подобие - это своего рода стандарт де-факто. Думаю, больше 90% кода - т.е. код на самых распространенных языках, таких как C, C++, C#, Java, Objective C, PHP, Perl, JavaScript - построены на си-подобном синтаксисе.


Цели проекта Neo - 2

Продолжу о целях.
Первое - разработка своего собственного языка программирования. Задача сама по себе крайне интересная и непростая (даже без разработки компилятора). И, что самое главное, включающая в себя очень большую и важную подзадачу - исследование и изучение множества других языков программирования.

Второе - исследования возможностей современных сред разработки (IDE). Я не ставлю себе задачу "разработать IDE", но в процессе разработки языка мне пришла в голову мысль, что в описание языка было бы неплохо включать "рекомендации" для разработчиков IDE. Для этого есть причины, и думаю, что этим причинам будет посвящена отдельная запись в блоге.

Третье - изучение проектирования компиляторов и разработка собственного компилятора. Здесь сразу две подзадачи: во-первых, в процессе разработки компилятора (как и любого другого проекта, кстати) возникают идеи по улучшению самого языка; во-вторых, в будущем имеет смысл переписать компилятор на свой собственный язык (http://en.wikipedia.org/wiki/Bootstrapping).

К числу других задач относятся - изучение "стандартных библиотек" и "фреймворков" (таких как STL, MFC, VCL, QT, Java, .NET) и попытка выделить из них нечто "самое лучшее". Исследование и сравнение фреймворков - также интересная тема, которой я почти не касался.

Цели проекта Neo

Когда я попытался сформулировать цели данного проекта, в голову пришло интересное определение: создать современный продвинутый язык для старого доброго программирования.

Что такое "старое доброе программирование"?
На самом деле, у каждого это что-то свое. Возможно, то, с чего программист начинал свой путь. Или то, в чем удалось достичь наиболее выдающихся результатов. Или что-то еще. Но, в любом случае, это то, чем приятно заниматься.

Итак, что бы я хотел создать:
1. Это язык в том числе и для системного программирования. Т.е. включающий в себя достаточно низкоуровневые возможности.
2. Это максимально си-подобный язык. Несмотря на множества различных синтаксисов, сишный мне как-то ближе. Да и большинству программистов, думаю, тоже.
3. Это язык, компилируемый в машинный код. Точнее, в машинные коды - под множество различных аппаратных и системных платформ.  Я с интересом изучаю современные скриптовые языки, но это не то, что бы я хотел создать. И это не фреймворк типа .NET или Java.
4. Это не академическая разработка, а скорее "хакерская". Я не ставлю целью "обезопасить" программиста от возможных ошибок, "научить" его чему-то... Но в то же время я не хочу, чтобы люди ломали мозг над чем-то сложным и непривычным. Это скорее попытка собрать и объединить все самое лучшее, что мне известно, учесть даже мельчайшие мелочи, на которые обычно мало обращают внимания. Сделать нечто идеальное:) ну или по крайней мере, стремящееся к идеалу.
Вот как-то так.


Немного истории

Идея написать свой язык программирования у меня есть уже давно. Можно сказать - с первого курса института, когда нам преподавали язык Си, и я невольно сравнивал его с уже известным мне из школы Паскалем. Тогда это были единственные языки, которые я знал, но уже тогда часто возникали мысли: "в Паскале это есть, а в Си нет... а вот если бы добавить такую возможность из Паскаля в Си!".
Затем появились Java, C#, Perl, PHP, Python и другие языки. На просторах интернета я находил и скачивал различную документацию по менее известным языкам - самым разным, например Ada, Sather, С--...
С самого начала и сейчас основным моим языком программирования является С++. Си-подобный синтаксис сразу понравился мне гораздо больше паскалевского (и, надо полагать, не только мне - практически все мейнстримовые языки имеют именно си-подобный синтаксис).

Сейчас за основу разработки я беру в первую очередь следующие языки
С,С++,C#,D,Java - основы основ. 90% синтаксических решений в той или иной степени взяты из этих языков.
Pascal, Delphi, а также Ada, Oberon, Modula - скорее как дань уважения, но кое-что интересное там есть; кроме того, в Ada есть языковая поддержка параллельности
ObjectiveC, Smalltalk - объектная модель "сообщений". В особенности мне нравится ObjC
Alef, Go - в связи с разработкой Гуглом нового языка обратил внимание и на его предшественников. Очень интересные решения в области параллельности, ну и синтаксические решения в Go очень даже ничего
Assemblers - да, кое что взял и из Ассемблера напрямую
Comega - экспериментальная разработка Microsoft, весьма интересная
Perl, PHP - тоже скорее дань уважения. Но кое что интересное там есть.
Python, Ruby - множество интересных решений в разных парадигмах программирования.
Scala, Nemerle - новые и весьма продвинутые языки. Nemerle - потрясающая система метапрограммирования, в которой, тем ни менее, меня далеко не все устраивает.