22.7
Karma
31.3
Rating

Инженегр АСУТП

Субъективное видение идеального языка программирования

+3

Похоже вы описали чуть улучшенный язык D :-)


Далее обстоятельный разбор
lst.map(.x).filter(>0).distinct()

list.map!q{ a.x }.filter!q{ a > 0 }.distinct.fold!q{ a + b }

Так вот, в идеальном языке должен быть тип Unit, который занимает 0 байт

Тут я не понял зачем такое нужно. Пример с HashSet — это особенность данной структуры, что какое-то материальное значение должно быть, чтобы отличать наличие значения от его отсутствия. Unit же нематериален. А минимальное материальное значение — тот самый bool. А если нужна максимально плотная упаковка, то есть BitArray:


bool[100500] hashset;

Этот тип должен быть полноценным типом, и тогда компилятор станет проще, а дизайн языка красивее и логичнее.

Ну как полноценный. Ссылку на него не получить, соответственно везде придётся вставлять проверки в духе "если это особый пустой тип".


Сигнатура функции должна описываться как T => U, где T и U — какие-то типы. Возможно, кто-то из них Unit, возможо, кортеж.

Тут есть один нюанс. Иногда кортеж надо передать как один аргумент, без разворачивания. В D для этого есть разделение на тип Tuple и Compile-time Sequence.


auto foo( Args... )( int a , Args args ) // take argument and sequence
{
    writeln( a + args[0] + args[1] ); // 6
    return args.tuple; // pack sequence to tuple
}

auto bar( int a ) // take argument
{
    return tuple( a , 2 , 3 ); // can't return sequence but can tuple
}

void main()
{
    foo(bar(1).expand); // unpack tuple to sequence
}

любая конструкция что-то возвращала

Идея, конечно, красивая, и в D такого нет, но такой код меня смущает:


val b = {
    val secondsInDay = 24 * 60 * 60
    val daysCount = 10
    daysCount + secondsInDay
    daysCount * secondsInDay
}

Умножение просто висит в воздухе. Сложение тоже висит, но уже ничего не делает.


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

Для этого есть шаблоны и их специализация.


alias Result( Res ) = Algebraic!( Res , Exception ); // alias for simple usage

void dump( Result!int res )
{
    // handle all variants
    auto prefix = res.visit!(
        ( int val )=> "Value: " ,
        ( Exception error )=> "Error: " ,
    );

    writeln( prefix , res );
}

void main()
{
    Result!int( 1 ).dump; // Value: 1
    Result!int( new Exception( "bad news" ) ).dump; // Error: bad news
    Result!int( "wrong value" ).dump; // static assert
}

типы-суммы (Union)

Всё же есть Union (когда одно значение соответствует разным типам одновременно) и Tagged Union (когда единовременно хранится значение какого-то конкретного типа из множества). Тип сумма — это второе.


изменяемая переменная

int foo;

переменная, которую "мы" не можем менять, но вообще-то она изменяемая

const int foo;

Причём менять мы не можем не только само значение, но и любые значения полученные через него. То есть компилятор сам добавляет к получаемым типам атрибут "const".


class Foo { Bar bar; }
class Bar {}

void main()
{
    const Foo foo;
    Bar bar = foo.bar; // cannot implicitly convert expression foo.bar of type const(Bar) to Bar
}

переменная, которую инициализировали и она больше не изменится.

Тут то же самое, но атрибут immutable:


class Foo { Bar bar; }
class Bar { int pup; }

void main()
{
    immutable Foo foo;
    foo.bar.pup = 1; // cannot modify immutable expression foo.bar.pup 
}

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


На мой взгляд, самый красивый подход используется в D. Пишется что-то вроде static value = func(42); и самая обычная функция явно вычисляется при компиляции.

Cтатики выполняются в рантайме, а для исполнения на этапе компиляции нужно использовать, внезапно, enum.


auto func( int a ) { return a + 1; }

enum value1 = func(42);
static value2 = func(42);

pragma( msg , value1 ); // 43
pragma( msg , value2 ); // static variable value2 cannot be read at compile time

Можно сказать, что это всё синтаксический сахар и вместо этого писать так: но это же некрасиво!

Зато понятно, где имя из области вызова, а где из области объекта. В D такого, конечно, нет. Тут наоборот, язык старается кидать ошибку в случае неоднозначности. Например, в случае обращения по короткому имени, которое объявлено в разных импортированных модулях.


Аналогично extension методы. Синтаксический сахар, но довольно удобный.

Причём вызывать так можно любую функцию.


struct Foo {}

auto type( Val )( Val val )
{
    return typeid( val );
}

void main()
{
    import std.stdio : dump = writeln;
    Foo().type.dump; // Foo
}

Call-by-name семантика

Для этого есть модификатор lazy


void log( Val )( lazy Val val )
{
    if( !log_enabled ) return;

    // prints different values
    val.writeln;
    val.writeln;
}

void main()
{
    log( Clock.currTime );
}

Фактически компилятор понижает этот код до передачи замыкания:


void log( Val )( Val val )
{
    if( !log_enabled ) return;

    // prints different values
    val().writeln;
    val().writeln;
}

void main()
{
    log( ()=> Clock.currTime );
}

Ко-контр-ин-нон-вариантность шаблонных параметров

Тут похоже у всех языков всё ещё плохо, и D не искючение.


Имхо, в идеале язык должен позволять явно разрешать неявные преобразования, чтобы они использовались осознанно и только там где необходимо. Например, то же преобразование из int в long.

Этого очень не хватает, да. У компилятора есть свои правила безопасных неявных преобразований, типа int->long, но расширять он их не даёт. Поэтому приходится обкладываться перегрузками операторов, шаблонами и делегированием в духе:


void foo( int a ) {}

struct Bar {
    auto pup() { return 1; }; // define pup as getter
    alias pup this; // use pup field for implicitly convert to int
}

void main()
{
    foo( Bar() );
}

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


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

Теоретически можно 3 байта отвести на тип, что позволит иметь в рантайме 16М типов. И 5 на смещение, что позволит адресовать 8TB памяти с учётом выравнивания. Пока что должно хватить, но насчёт будущего не уверен.


примитивные типы "притворяются" объектами, так что всё с чем мы работаем выглядит однообразно

Более того, сами типы могут выглядеть как объекты.


long.sizeof.writeln; // 8

есть поля класса, проперти и методы. 3 сущности! Они друг с другом не очень сочетаются, можно объявить несколько методов с одним именем, но разной сигнатурой, но почему-то нельзя объявить проперти с тем же именем.

В D проперти — не более чем методы с 0/1 аргументом.


struct Foo
{
    protected int _bar; // field
    auto bar() { return _bar; } // getter
    auto bar( int next ) { _bar = next; } // setter
} 

а те же inline могут быть аннотациями, которые не влияют на логику исполнения кода и лишь помогают компилятору.

Так и всякие "inline, forced_inline__, noinline, crossinline" тоже не более чем аннотации, помогающие компилятору и не влияющие на логику. Только проблема в том, что программисты не умеют ими пользоваться и компилятору приходится на них забивать, так что помощи от них по итогу никакой. Впрочем, в D есть 3 стратегии, которые программист может переключать через pragma:


  1. По умолчанию на откуп компилятору.
  2. Не инлайнить.
  3. Попытаться заинлайнить, а если не получится — выдать ошибку.

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

Этого в D нет, но есть в его идейном последователе — Nim. Однако, в D есть мощная система шаблонов, позволяющая покрыть большую часть потребностей. И возможность генерировать код на этапе компиляции, правда в виде строки, что не очень удобно и годится лишь для мелочей. В принципе, трансформеры аст не так уж сложно реализовать библиотекой, ведь на этапе компиляции можно прочитать исходник, распарсить его, прогнать через трансформер, сериализовать и выполнить. Но видио это никому ещё не было нужно. Синтаксис D не самый простой, но и не то чтобы сильно сложный.


разрешить объявлять внутри функций какие-то локальные функции или классы

Это всё можно, даже импортировать другие модули можно локально.


void main()
{
    {
        import std.stdio;
        writeln( "Hello!" );
    }
    writeln( "Hello!" ); // error
}

список из не менее чем одного элемента или число, которое делится на 5, но не делится на 3, но я плохо представляю, как подобное можно доказывать в достаточно сложной программе.

Вот у меня тут есть пара примеров:



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


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

В D для этого есть структура scoped, создающая объект на стеке и обеспечивающая вызов деструктора при выходе из скоупа.


{
    auto foo = scoped!Foo();
}
// foo is destructed here

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

Поэтому в D можно работать как с ним, так и без него. Многие приятности стандартной библиотеки, правда, зависят от GC.