Дальнейший текст — моя точка зрения. Возможно, она позволит кому-то по-новому взглянуть на дизайн языков программирования или увидеть какие-то преимущества и недостатки конкретных фич. Я не буду лезть в частные подробности типа "в языке должна быть конструкция while", а просто опишу общие подходы. P.S. У меня когда-то была идея создать свой язык программирования, но это оказалось довольно сложным процессом, который я пока не осилил.


Влияние предыдущего опыта


На написание статьи меня вдохновила вот эта статья. Автор придумал свой язык программирования, и этот язык своим синтаксисом и особенностями оказался подозрительно похожим на Free Pascal, на котором и была написана реализация ВМ для языка. И это не совпадение. Языки программирования, на которых мы раньше писали, загоняют мышление в рамки языка. Мы сами можем не замечать этого, но сторонний наблюдатель с иным опытом может посоветовать что-то неожиданное или сам научиться чему-то новому.


Рамки мышления немного раздвигаются после освоения нескольких языков. Тогда в языке А вам может захотеться иметь фичу из Б и наоборот, а ещё появится осознание сильных и слабых стороны каждого языка.


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


Мой опыт: когда-то я начинал с паскаля, впоследствии познакомился с Java, Kotlin, C++, Python, Scheme, а основными языком считаю Scala. Как и в вышеописанном случае, мой "идеальный" язык имеет много общего со Scala. По крайней мере, я отдаю себе отчёт в этом сходстве)


Влияние синтаксиса на стиль кода


"Писать на фортране можно на любом языке"


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


Python:


filtered_lst = [elem for elem in lst if elem.y > 2]
filtered_lst = list(filter(lambda elem: elem.y > 2, lst))

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


Scala:


val filteredLst = lst.filter(_.y > 2)

Имхо, это близко к идеалу. Ничего лишнего. Если бы в питоне можно было объявлять лямбды более коротким способом, хотя бы it => it.y > 2, то генераторы списков оказались бы не очень нужными.


Самое интересное, что подход как в скале хорошо масштабируется в цепочку вызовов типа lst.map(_.x).filter(_>0).distinct() Мы читаем и пишем код слева направо, элементы идут по цепочке преобразований тоже слева направо, это удобно и органично. Вдобавок, среда разработки по коду слева может давать адекватные подсказки.


В питоне в строчке [elem for elem in среда разработки до последнего не подозревает, какой же тип у элемента. Большие конструкции приходится читать справа налево, из-за чего эти самые большие конструкции в питоне обычно не пишут.


... = set(filter(lambda it: it > 2, map(lambda it: it.x, lst))))

Это же ужасно!


Подход с lst.filter(...).map(...) в питоне мог бы существовать, но он убит на корню динамической типизацией и неидеальной поддержкой сред разработки, которые далеко не всегда догадываются о типе переменной. А подсказать, что в numpy есть функция max — всегда пожалуйста. Поэтому и дизайн большинства библиотек подразумевает не объект с кучей методов, а примитивный объект и кучу функций, которые его принимают и что-то делают.


Ещё один пример, уже на java:


int x = func();
final int x = func();

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


let x = 1;
let mut x = 1;

Получается, что синтаксис языка реально важен, и должен быть по возможности простым и лаконичным. Язык должен изначально создаваться под часто используемые фичи. Антипримером можно назвать С++, где по историческим причинам определение класса раскидывается по паре файлов, а объявление простой функции может не влазить в строчку благодаря словам типа template, typename, inline, virtual, override, const, constexpr и не менее "коротким" описаниям типов аргументов.


Статическая типизация


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


Но это не так. От типов никуда не убежать, и если мы их не указываем, то это не значит, что их нет. Если писать код без жёстких рамок, то в нём становится очень просто наплодить ошибок и сложно доказать корректность происходящего. С ростом программы это усугубляется, и что-либо большое, как мне кажется, на динамических языках не пишут.


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


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


Так что статическая типизация — must have для современных языков. Из плюсов статической типизации стоит отметить большую строгость программы, выявление ошибок на этапе компиляции, а так же больший простор для оптимизации и анализа кода. Из минусов — типы иногда надо писать, но автовывод типов снижает эту проблему.


Unit, void и отличия функции от процедуры


В паскале/delphi есть разделение на процедуры (не возвращающие значений) и функции (что-то возвращающие). Но никто не запрещает нам вызвать функцию, а возвращаемое значение не использовать. Хм. Так в чём же разница между функцией и процедурой? Да ни в чём, это инерция мышления. Своеобразное легаси, переползшее в Java, С++ и ещё кучу языков. Вы скажете: "есть же void!" Но проблема в том, что void в них это не совсем тип, и если залезть в шаблоны или дженерики, то это отличие становится заметным. Например, в Java HashSet<T> реализовано как HashMap<T, Boolean>. Тип boolean — просто заглушка, костыль. Он там не нужен, в HashMap не требуется значение, чтобы сказать, что ключа нет. В С/С++ тоже есть нюансы с sizeof(void).


Так вот, в идеальном языке должен быть тип Unit, который занимает 0 байт и принимает только одно значение (не важно какое, оно одно, и если у вас есть Unit, то это оно). Этот тип должен быть полноценным типом, и тогда компилятор станет проще, а дизайн языка красивее и логичнее. В идеальном языке можно будет реализовать HashSet<T> как HashMap<T, Unit> и не иметь никакого оверхеда на хранение ненужных объектов.


Кортежи


У нас есть ещё кое-какое историческое наследие, пришедшее, наверно, из математики. Функции могут принимать много значений, а возвращают только одно. Что за ассиметрия?! Так сделано в большинстве языков, что приводит к следующим проблемам:


  • Функции с переменным числом аргументом требуют специального синтаксиса — усложняется язык. Сделать универсальную функцию-прокси становится сложнее.
  • Чтобы возвратить сразу несколько значений, приходится объявлять специальную структуру или передавать изменяемые аргументы по ссылке. Это неудобно.

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


Есть некоторые шаги навстречу типа std::tuple в с++, но как мне кажется, подобное должно быть не в стандартной библиотеке, а существовать прямо в системе типов языка и записываться, например, как (T1, T2). (кстати, на тип Unit можно смотреть как на кортеж без элементов). Сигнатура функции должна описываться как T => U, где T и U — какие-то типы. Возможно, кто-то из них Unit, возможо, кортеж. Честно говоря, я удивлён, что в большинстве языков это не так. Видимо, инерция мышления.


Раз уж мы можем возвращать Union, можно полностью отказаться от разделения выражение/инструкция и сделать, чтобы в языке любая конструкция что-то возвращала. Подобное уже реализовано в относительно молодых языках типа scala/kotlin/rust — и это удобно.


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

Enums, Union и Tagged Union


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


Во-первых, язык должен поддерживать легковесное объявление типов-перечислений. Желательно, чтобы в рантайме они превращались в обычные числа и от них не было никакой лишней нагрузки. А то получается всякая боль и печаль, когда одни функции возвращают 0 при успешном завершении или код ошибки, а другие функции возвращают true (1) при удаче или false (0) при фейле. На надо так. Объявление типа перечисления должно быть насколько коротким, чтобы программист прямо в сигнатуре функции мог написать, что функция возвращает что-то из success | fail или ok|failReason1 | failReason2.


Кроме того, оказываются очень удобными типы-перечисления, которые могут содержать значения. Например, ok | error(code) или Pointer[MyAwesomeClass] |null Такой подход позволит избежать кучи ошибок в коде.


В общем виде это можно назвать типами-суммами. Они содержат одно из нескольких значений. Разница между Union и Tagged Union состоит в том, что мы будем делать в случаях совпадающих типов, например int | int. С точки зрения простого Union int | int == int, так как у нас в любом случае инт. В общем-то с union в си так и получается. В случае с int | int tagged union ещё содержит информацию, какой у нас int — первый или второй.


Маленькое отступление


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


List(x) = Unit | (x, List(x))

Ну почти как списки в лиспе.
Если заменить тип-сумму на сложение (неспроста же он так называется), кортеж интерпретировать как произведение, то получится:


f(x) = 1 + x * f(x)

Ну или другими словами, f(x) = 1 + x + x*x + x*x*x + ..., а с точки зрения типов-произведений (кортежей) и типов-сумм это выглядит как


List(x) = Unit | (x, Unit) | (x, (x, Unit)) | ...  = Unit | x | (x, x) | (x, x, x) | ...

Cписок типа x = это пустой список, или один элемент x, или кортеж из двух, или ...


Можно сказать, что (x, Unit) == x, аналогией в мире чисел будет x * 1 = x, так же (x, (x, (x, Unit))) можно превратить в (x, x, x).


К сожалению, из этого следует, что теоремы для обычных чисел можно выразить на языке типов, и, подобно теоремам, которые не всегда легко доказываются (если вообще доказываются), взаимоотношения типов тоже могут быть довольно сложными. Возможно, поэтому в реальньных языках такие возможности сильно ограничены. Впрочем, это не является непреодолимым препятствием — например, язык шаблонов в С++ является Тьюринг-полным, что не мешает компилятору переваривать адекватно написанный код.


Короче, типы-суммы в языке нужны, и они нужны прямо в системе типов языка, чтобы нормально сочетаться с типами-произведениями (кортежами). Там получится целый простор для преобразований типа (A, B | C) == (A, B) | (A, C)


Константы


Возможно, это звучит неожиданно, но неизменяемость можно понимать по разному. Я вижу аж четыре степени изменяемости.


  1. изменяемая переменная
  2. переменная, которую "мы" не можем менять, но вообще-то она изменяемая (например, в функцию передают контейнер по константной ссылке)
  3. переменная, которую инициализировали и она больше не изменится.
  4. константа, которую можно найти прямо во время компиляции.

Разница между пунктами 2 и 3 не совсем очевидна, приведу пример: допустим, в С++ нам в объект передали указатель на константную память. Если мы где-то внутри класса сохраним этот указатель, то у нас нет никаких гарантий, что в течение жизни объекта содержимое памяти по указателю не изменится.
В некоторых случаях нам нужен именно третий тип неизменяемости — например, при чтении объекта из нескольких потоков или при вычислении чего-то, основанного на свойствах полученного объекта. Именно третий тип неизменяемости позволит компилятору проводить какие-то хитрые оптимизации. Пример использования — final поле в java.


Лично мне кажется, что нюансы с изменяемостью 1-2 типа можно решить с помощью интерфейсов и отсутствующих геттеров/сеттеров. Например, у нас есть неизменяемый объект, который содержит указатель на изменяемую память. Вполне возможно, что мы захотим иметь несколько "интерфейсов" для использования объекта — и тот, который не даст менять только объект и тот, который, например, закроет доступ к внешней памяти.
(Как нетрудно догадаться, тут на меня повлияли jvm языки, в которых нет слова const)


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


Фишечки котлина


Если кто-то использовал gradle, то, возможно, при взгляде на неработающие build файлы вас посещала мысль типа "wtf? Что мне делать?"


android {
    compileSdkVersion 28
}

Это просто код на языке Groovy. Объект android просто принимает замыкание { compileSdkVersion 28}, и где-то в дебрях андроид-плагина этому замыканию присваивается объект, в контексте которого реально будет запущено наше замыкание. Проблема тут в динамичности языка groovy — среда разработки не подозревает о том, какие поля и методы возможны в нашем замыкании и не может подсветить ошибки.


Так вот, в котлине есть хитрые типы, и это можно было бы реализовать как-то так


class UnderlyingAndroid(){
     compileSdkVersion: Int = 42
}

fun android(func: UndelyingAndroid.() -> Unit) ....

Мы уже в сигнатуре функции говорим, что принимаем что-то, работающее с полями/методами класса UnderlyingAndroid, и среда разработки сразу же подсветит ошибки.


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


android { it =>
    it.compileSdkVersion = 28
}

но это же некрасиво! А если мы вложим друг в друга несколько таких конструкций? Подход как в котлине + статические типы позволяют делать очень лаконичные и удобные DSL. Надеюсь, рано или поздно всю gradle перепишут на котлин, удобство использования вырастет в разы. Я бы хотел иметь такую фичу в своём языке, хотя это и не критично.


Аналогично extension методы. Синтаксический сахар, но довольно удобный. Совсем необязательно быть автором класса, чтобы добавить к нему очередной метод. А ещё их можно вкладывать в области видимости чего-нибудь и таким образом не засорять глобальную область. Ещё одно интересное применение — можно навешивать эти методы на существующие коллекции. Например, если коллекция содержит объекты типа T, которые поддерживают сложение с самими собой, то можно добавить коллекции метод sum, который будет только в том случае, если T это позволяет.


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


Это опять же синтаксический сахар, но это удобно, и вдобавок позволяет писать ленивые вычисления. Например, в коде типа map.getOrElse(key, new Smth()) второй аргумент принимается не по значению, а потому новый объект будет создан только если в таблице нет ключа. Аналогично, функции типа assert(cond, makeLogMessage()) выглядят намного приятнее и удобнее.


Вдобавок, никто не заставляет компилятор делать именно анонимную функцию — например, можно заинлайнить функцию assert и тогда это превратится просто в if (cond) { log(makeLogMessage())}, что тоже неплохо.


Я не скажу, что это must have фича языка, но она явно заслуживает внимания.


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


Всё это нужно. "Входные" типы можно делать шире, "выходные" типы можно сужать, с некоторыми типами ничего нельзя делать, а некоторые можно игнорировать. Имхо, в современном языке нужно иметь поддержку этого прямо в системе типов.


Явные неявные преобразования


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


Где хранить типы объектов?


Их можно вообще не хранить. Например, в С все типы известны на этапе компиляции и во время выполнения у нас этой информации уже нет. Можно хранить вместе с объектом (так сделано в языках с виртульными машинами, а так же для виртуальных классов в С++). Лично мне кажется более интересным третий подход, когда тип (указатель на табличку с методами) хранится прямо в указателе.


Значения, ссылки, указатели


Язык должен скрывать от программиста подробности реализации. В С++ при написании шаблонов возникают проблемы, так как T в шаблоне может оказаться каким-нибудь неожиданным типом. Это может быть значение, указатель, ссылка или rvalue-ссылка, некоторые приправляются словом const. Не могу сказать, как надо сделать, но точно вижу, как делать не надо. Что-то близкое к идеалу по удобству есть в Scala и Kotlin, где примитивные типы "притворяются" объектами, так что всё с чем мы работаем выглядит однообразно и не нагружает мозг программиста и синтаксис языка.


Минимум сущностей


Вот чем мне не нравится С# — в язык втащили кучу всего, это всё как-то странно сочетается и повышает сложность языка. (Я могу сильно ошибаться в деталях, так как на С# я писал очень давно и только под Unity) Например, там есть поля класса, проперти и методы. 3 сущности! Они друг с другом не очень сочетаются, можно объявить несколько методов с одним именем, но разной сигнатурой, но почему-то нельзя объявить проперти с тем же именем. Или если интерфейс требует чтобы было проперти, то нельзя в классе просто объявить поле — это должно быть именно проперти.


В kotlin/scala сделано лучше — все поля приватные, снаружи используются через сгенерированные геттеры и сеттеры. Технически они являются просто методами со специальными именами, и их в любой момент можно переопределить. Всё, никаких извращений.


Ещё пример — слово inline в C++/Kotlin. Не стоит его тащить в язык! И там и там слово inline меняет логику компиляции и исполнения кода, люди начинают писать inline не ради собственно инлайна, а ради возможностей писать функцию в хедере (С++) или делать хитрый return из вложенной функции как из вызывающей (kotlin). Потом в языке появляются forced_inline__, noinline, crossinline, влияющие на какие-нибудь нюансы и ещё более усложняющие язык. Мне кажется, язык должен быть максимально гибким и простым, а те же inline могут быть аннотациями, которые не влияют на логику исполнения кода и лишь помогают компилятору.


Макросы


В языке должны быть макросы, которые принимают на вход синтаксическое дерево и преобразуют его. В случае с унылым повторяющимся кодом макросы могут спасти от ошибок и сделать код в несколько раз короче. К сожалению, достаточно серьёзные языки типа С++ имеют ещё и сложный синтаксис с кучей нюансов, возможно, поэтому нормальных макросов там до сих пор так и не появилось. В языках типа lisp и scheme, где программа сама по себе подозрительно похожа на список, написание макросов не вызывает больших проблем.


Функции внутри функций


Плоская структура уныла. Если что-то используется только в одном-двух местах, то почему бы не сделать это максимально локально — например, разрешить объявлять внутри функций какие-то локальные функции или классы. Это же удобно: не засоряется пространство имён, при удалении кода функции заодно удаляются и её "внутренние" подробности.


Substructural type system


Можно реализовать систему типов, на использование которых накладываются ограничения. Например, переменную можно использовать только один раз или, например, не более одного.
Зачем это может пригодиться? Move-семантика и идея владения основана на том, что отдать владение объектом можно только один раз. Кроме того, всякие объекты с внутренним состоянием могут подразумевать определённый сценарий работы. Например, мы сначала открываем файл, что-то читаем/пишем, а потом закрываем обратно. Сейчас состояние файла лежит на совести программиста, хотя действия с ним (теоретически) можно запихнуть в систему типов и избавиться от части ошибок.


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


Зависимые типы


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


Сборка


Во-первых, язык должен уметь работать и без стандартной библиотеки. Во-вторых, библиотека должна состоять из отдельных кусочков (возможно, с зависимостями между ними), чтобы при желании можно было включить только часть из них. В третьих, в современном языке должна быть удобная система сборки (в С++ боль и печаль).
Функции, переменные и классы должны использоваться для описания хода вычисления, это не то, что надо запихивать в бинарник. Для экспорта наружу можно как-нибудь аннотировать необходимые кусочки, но всё остальное должно быть отдано компилятору и пусть он преобазует код как хочет.


Выводы


Итак, на мой взгляд, в идеальном языке программирования должны быть:


  • мощная система типов, с самого начала поддерживающая
    • типы-объединения и кортежи
    • ограничения на шаблонные параметры и их взаимоотношения друг с другом
    • возможно, экзотику типа линейных или даже зависимых типов.
  • удобный синтаксис
    • лаконичный
    • располагающий писать в декларативном стиле и использовать константы.
    • унифицированный для типов по значению и по указателю (ссылке)
    • максимально простой и гибкий
    • учитывающий возможности ide и рассчитаный на удобное взаимодействие с ней.
  • чуточку синтаксического сахара типа extension-методов и ленивых аргументов функции.
  • возможность перенести часть вычислений на этап компиляции
  • макросы, работающие прямо с AST
  • удобные сопутствующие инструменты типа системы сборки

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


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


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

Комментарии (216)


  1. kiloboot
    07.01.2019 18:08

    Новый, интересный zig. Построен на плечах LLVM/LLD. Что удивительно — не поделка, а очень и очень рабочий инструмент.


    1. artemisia_borealis
      08.01.2019 01:34

      Любпытно, по крайне мере. Вот здесь написано много интересного, что-то очень заманчиво, что-то неоднозначно.
      Но покурить стоит.


      1. TargetSan
        08.01.2019 14:35

        Самая большая неоднозначность — парадигма полностью ручного ресурс менеджмента. Т.е. при отсутствии GC они не предоставляют ничего кроме куцего defer.


  1. sshikov
    07.01.2019 18:50

    >имеет много общего со Scala. По крайней мере, я отдаю себе отчёт в этом сходстве)
    Раз уж сходство очевидно, то было бы неплохо четко сформулировать заодно и в чем различия. И какую нишу вы планируете занять. Без этого обычно все равно ничего не получается.


    1. lgorSL Автор
      07.01.2019 19:37

      Ну я больше описал "как я бы хотел", а не "как оно получится после столкновения с реальностью". Первичной целью является получение опыта и знаний. В идеале я бы хотел потеснить lua — сделать маленький гибкий язык, чтобы его можно было встраивать куда угодно. Ещё я хотел попробовать Graal VM, но у меня никак не дойдут руки(


      В скале 2 нет типов-сумм, есть только очень ограниченная поддержка в виде case классов. Есть мелочи типа не устаканившихся макросов и некоторых костылей для обхода ограничений jvm. Есть некоторые моменты, которые, как мне кажется, сделаны слишком сложно. Всякие интересные возможности добавляют в dotty, но я не знаю, как их поддержка повлияет на производительность кода. Не хватает возможности в качестве шаблонного параметра передать число (в scala native это нужно для описания типа массива фиксированной длины). В scala native после компиляции получаются подозрительно большие бинарники. Код (jar файлы) получаются тоже довольно большими, на порядок больше того что в java (именно мой код, без учёта стандартной библиотеки). Вдобавок, чувствуется, что скала создавалась под jvm, это выражается в некоторых ограничениях. Например, для интерфейса c методом print(t: T) и не могу сделать, чтобы один класс реализовал интерфейс одновременно и для T=int и для T=String


      1. sshikov
        07.01.2019 19:42

        >маленький гибкий язык, чтобы его можно было встраивать куда угодно.
        Знаете, у меня вот прямо сейчас скала выступает именно таким языком, причем наравне с груви (они применяются по очереди). Это называется Spark Shell, и как это ни странно, но то подмножество, которое нужно для создания прототипов — оно достаточно маленькое и простое, в том числе как выяснилось для освоения не совсем программистами.

        >В скале 2 нет типов-сумм
        Мне казалось, что это запланировано в будущей версии (dotty, правда, непонятно, когда она будет).


      1. OldFisher
        08.01.2019 15:17
        +1

        Lua был придуман и развивается именно как маленький гибкий язык для встраивания. В чём состоит необходимость его потеснить, чего ему настолько не хватает в этой роли? Что нового даст этот «язык мечты», что так нужно Lua и более-менее аналогам, типа Squirrel и прочих?


  1. ApeCoder
    07.01.2019 18:53
    +2

    В F# интересно с точки зрения множественности аргументов: есть каринг (т.е. функция от двух аргументов, это функция от первого аргумента, которая возвращает функцию с примененным первым аргументом:
    let f x y = x + y
    тогда
    f 2 3 вернет 5
    f 2 вернет функцию прибавляющую 2 к аргументу
    )
    это удобно для функционального кода. (map (f 2) — прибавить двойку ко всем элементам списка. Можно написать map ((+) 2) — операторы тоже можно вызывать как функции)


    Но можно как вы и предлагаете, сделать функцию от одного аргумента — кортежа. И именно так рабатет вся интеграция с дотнет — наверное потому, что ложно сделать каринг при перегрузке метода по типам параметров. Соответственно карринг по ним не работает.


    Их можно вызывать только так:


    System.Console.WriteLine(x, y), где скобки это не часть синтаксиса вызова, а констрирование кортежа.


    1. Sirikid
      09.01.2019 06:07

      Самое забавное что там и out параметры мапятся в кортеж.
      C#: bool TryParse(string, out T)
      F#: TryParse : string -> (bool, T)


    1. Deosis
      09.01.2019 14:12

      Интеграция работает по особому и ни кортеж, ни частичное применение не работает. Нужно делать обертки.
      Такой код не сработает:
      let tuple = (x, y)
      System.Console.WriteLine tuple


  1. MonkAlex
    07.01.2019 19:07
    +1

    Очень многое, из того что написано, есть в C#.
    Он разок упомянут в статье, но мелко и не конструктивно, поэтому комментировать не буду, ибо не понял проблемы.


    1. lgorSL Автор
      07.01.2019 20:00

      Вот пример.


      class A {
          public void a(double c){}
          public void a(int x, int y){}
          //public int a{get; set;}
      }

      Почему я могу сделать несколько методов с одинаковым именем и разный сигнатурой, и это нормально, а если я для проперти попробую использовать то же самое имя, то нельзя? И это не смотря на то, что "под капотом" геттер и сеттер это всё равно методы. Это ограничение мне кажется совершенно искусственным, как и отдельный синтаксис для определения геттеров и сеттеров.


      1. MonkAlex
        07.01.2019 20:19

        Не смог найти конкретного ответа на ваш вопрос. Допускаю, что в каких то кейсах будет неоднозначное поведение, когда надо например отличить свойство типа Action от метода.
        ПС: если свойство будет типа Action<\int> (как елочки экранировать на хабре?) его можно будет вызывать буквально как метод — obj.Property(123), и в этом случае вызов между таким методом и таким свойством дадут неоднозначность.


      1. lair
        07.01.2019 21:20

        а если я для проперти попробую использовать то же самое имя, то нельзя?

        Потому что невозможно будет определить, что такое var q = A.a.


        Это ограничение мне кажется совершенно искусственным

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


        1. lgorSL Автор
          07.01.2019 23:28

          Потому что невозможно будет определить, что такое var q = A.a.

          Не вижу проблемы. Если у класса несколько методов, то тоже невозможно определить, к чему относится A.a При этом всё нормально работает:


          class A {
              public void a(double c){}
              public int a(){return 1;}
          }
          
          public static void Main()
          {
              Func<int> a = new A().a;
              Action<double> a2 = new A().a;
              var a3 = new A().a();
          }

          Проблема в том, что усложнился весь язык.


          1. Проперти считается отдельной сущностью, хотя реализована через методы
          2. Геттер будет реализован как метод get_a, но при этом я не могу в классе определить ни одного метода с именем a.
          3. метод get_a() объявить тоже не получится. (Единственный логичный пункт)
          4. Метод геттера get_a() нельзя вызвать как метод. Но можно через рефлексию.

          Л-логика! В C# создали проблемы на пустом месте.


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


          class A() {
              val x = 1
              def x(s: String): Unit = println(s)
              def x(arg: Int): Unit = x(arg.toString)
          }
          
          val a = new A()
          a.x(a.x)
          val method: String => Unit = a.x(_)

          Eдинственное, что запрещает язык — сделать метод с именем 'x', который ничего не принимает, поскольку геттер является этим самым методом.


          1. lair
            07.01.2019 23:54

            При этом всё нормально работает

            Не, не работает. Вы не можете сделать var q = A.a. Вы, собственно, не можете этого сделать даже тогда, когда метод a один: "CS0815: Cannot assign method group to an implicitly-typed variable".


            Вы хотите потребовать, чтобы для свойства a указывали явно тип принимающей переменной? int q = A.a? Разработчики будут против, это неудобно.


            Проблема в том, что усложнился весь язык.

            Не, не усложнился.


            Проперти считается отдельной сущностью, хотя реализована через методы

            Много что считается отдельной сущностью, хотя реализуется через что-нибудь другое. И что?


            Геттер будет реализован как метод get_a, но при этом я не могу в классе определить ни одного метода с именем a.

            Вам уже объяснили, почему. Могу повторить.


            Метод геттера get_a() нельзя вызвать как метод.

            А зачем?


            Л-логика! В C# создали проблемы на пустом месте.

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


            1. Sirikid
              09.01.2019 06:15

              Метод геттера get_a() нельзя вызвать как метод.
              А зачем?
              Например чтобы писать Select(A.a), а не Select(x => x.a)


              1. lair
                09.01.2019 11:28

                Идея, конечно, привлекательная, но не взлетит.


                Во-первых, метод геттера вам для этого никак не поможет. Давайте проведем эксперимент на обычном методе:


                public class A {
                    public int get_a() {
                        return 0;
                    }
                
                    public void Q(IEnumerable<A> aa)
                    {
                        aa.Select(x => x.get_a()); //компилируется и работает
                        aa.Select(A.get_a); //error CS0411
                    }
                }

                Во-вторых, если сделать так, чтобы это работало (именно через методы, а не через свойства), то что станет с IQueryable.Select?


                1. Sirikid
                  09.01.2019 17:06
                  +1

                  В Котлине взлетело, правда там доступ к методу/свойству и "взятие ссылки" синтаксически отличаются: A.get_prop и A::get_prop.


                  1. lair
                    09.01.2019 20:00

                    А в C# такой синтаксис, и с ним уже ничего не поделаешь.


                    Ну так а что с expressions?


                    1. Sirikid
                      10.01.2019 04:47

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


                      1. lair
                        10.01.2019 12:54

                        Когда используется Expression<Func<A,B>> e = x => x.q, создается выражение, из которого явно понятно, что q — это свойство. Всякие там милые ORM используют это для создания запросов в БД, опираясь на маппинг этого самого свойства на БД. Если вы сделаете вызов метода, то он будет неотличим от вызова любого другого метода, отследить его до свойства будет нельзя, ORM сломается.


                        1. Sirikid
                          10.01.2019 15:15

                          А что если вешать на сгенерированный геттер атрибут, который сигнализировал бы о том, к какому свойству он относится?


                          1. lair
                            10.01.2019 15:29

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


                            Ну и да, ввиду того, что нет инстанса, это все равно все невозможно.


          1. lair
            07.01.2019 23:59

            Вот, кстати, прекрасный пример с SO:


            class A
            {
              public Action<int> Q {get;}
              public void Q (int a) {}
            }
            
            Action<int> q = new A().Q;


            1. lgorSL Автор
              08.01.2019 00:40

              Вы хотите потребовать, чтобы для свойства a указывали явно тип принимающей переменной? int q = A.a? Разработчики будут против, это неудобно.

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


              А зачем?

              Потому что он есть. Кому это мешает?


              прекрасный пример с SO:

              Это уже больше похоже на некорректную ситуацию. Ну так запрещать надо именно такое, а не все совпадения имён!


              Вдобавок, даже из этой ситуации можно было вывернуться, если бы проперти было доступно как метод get_Q(), а в неоднозначности из примера отдавался бы приоритет методу Q.


              В языках типа Groovy/Scala проперти играют роль синтаксического сахара, который использовать не обязательно — можно напрямую звать геттеры и сеттеры. С моей точки зрения происходящее в С# выглядит лишним усложнением.


              1. lair
                08.01.2019 00:53

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

                Угу. Было свойство Q, можно было писать var q = x.Q. Теперь кто-то добавил к классу новый метод Q, и внезапно весь старый код больше не компилируется. Правда, круто?


                Потому что он есть.

                Я спросил, зачем?


                Ну так запрещать надо именно такое, а не все совпадения имён!

                Логика для анализа слишком сложная окажется.


                если бы проперти было доступно как метод get_Q()

                А это был бы мусор в именовании.


                а в неоднозначности из примера отдавался бы приоритет методу Q.

                А почему методу, а не свойству? А если там Func<int>? А если у вас не просто свойство, а индексер?


                С моей точки зрения происходящее в С# выглядит лишним усложнением.

                Ровно наоборот, в C# все понятно, если не пытаться подходить к этому с точки зрения скалы.


              1. anaym
                08.01.2019 13:11
                +1

                Это уже больше похоже на некорректную ситуацию. Ну так запрещать надо именно такое, а не все совпадения имён!

                А нужно запретить такое объявление класса?
                class A<T>
                {
                  public T Q {get;}
                  public void Q (int a) {}
                }
                

                Или такое использование?
                Action<int> q = new A<Action<int>>().Q;
                

                А как быть с таким использованием?
                void Foo<T>(A<T> a)
                {
                  Action<int> q = a.Q;
                  ...
                }
                


          1. MonkAlex
            08.01.2019 00:44

            А зачем кстати одноименные свойства и методы? Обычно у них разные цели и разные названия, ни разу не сталкивался с такой необходимостью.


            1. a-tk
              08.01.2019 16:21

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


            1. gnefedev
              08.01.2019 22:02
              +1

              Недавно именно это понадобилось при написании DSL для тестов (factory тестовых данных).

              class ApprenticeCustomization {
                  var group: Group? = null
                  var groupCustom: GroupCustomization.() -> Unit = {}
                  fun group(custom: GroupCustomization.() -> Unit) {
                      groupCustom = custom
                  }
              }
              

              В итоге выходит синтаксис, где
              var customGroup = factory.group { }
              var apprentice = factory.apprentice {
                  group = customGroup
              }
              

              это присвоение уже гового, а вот такой вызов
              var apprentice = factory.apprentice {
                  group {
                      price = BigDecimal(3_000)
                  }
              }
              

              это уже дефротная группа, но с переопределенной стоимостью занятий


      1. fedorro
        07.01.2019 22:09

        Потому что у свойств получаются «геттеры» с одинаковой сигнатурой и именем.
        Методы int Get_A(){} string Get_A(){} тоже объявить не получится.


      1. playermet
        08.01.2019 19:42

        А теперь давайте представим, что у проперти тип не int, а делегат, имеющий сигнатуру совпадающую с одним из методов. И тут уже возникает неразрешимая проблема при попытке сделать obj.a().


  1. ice2heart
    07.01.2019 19:35

    void не зря не равен 0. А то массив void был бы равен 0. Что даст неопределенное поведение.


    1. NeoCode
      07.01.2019 22:56

      А кстати, каким образом?


    1. iig
      08.01.2019 11:24

      void это абстрактное значение, ничему не соответствующие. Отсутствие значения. Древние математики до изобретения понятия нуля тоже видели в этом проблему.
      Если очень хочется, не вижу никаких проблем сделать что-то вида


      define MAX_INT _OLD_MAXINT — 1
      define void _OLD_MAXINT

      Откуда тут неопределенность?


      1. ice2heart
        08.01.2019 12:01

        Я не очень чётко выразился, размер не 0 чтоб например масив из void не был равен 0.


        1. iig
          08.01.2019 12:44

          #include <stdio.h>
          
          int main()
          {
              void v[2];
              printf("Hello World %d\n",sizeof(v));
              return 0;
          }
          


          даже не компилируется

          main.c: In function ‘main’:
          main.c:5:10: error: declaration of ‘v’ as array of voids
               void v[2];
                    ^


          Откуда неопределенность?


          1. ice2heart
            08.01.2019 13:16

            Эт хорошо что не компилируется.

            #include <stdio.h>
            class A {
            public:
            A() = default;
            };
            
            int main(){
            A a;
            printf("Hello World %d\n",sizeof(a));
            return 0;
            }
            

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


            1. iig
              08.01.2019 13:34

              Класс может быть равен нулю


              Не могу себе представить операцию сравнения класса с константой. Разве что на javascript ;)

              Размер экземпляра класса в вашем примере получается 1. Логично, у класса есть конструктор, и его размер !=0.


              1. ice2heart
                08.01.2019 18:11

                Конструктор не виляет на размер класа. Он не хранится внутри класса. Внутри класа хранится только vtable. Функции не влияют на размер класса. Это сделано специально, чтоб если сложить экземпляры класса в массив, массив внезапно не стал 0 размера.


                1. iig
                  08.01.2019 19:10

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



                  Вряд ли массив 0 размера чем-то хуже переменной 0 размера. ;)
                  Просто обьектов переменных 0 размера в языке С(++) нет.


                  1. Sirikid
                    09.01.2019 06:44
                    +1

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


                    empty-struct.c
                    #include <stdio.h>
                    
                    #define print(FMT, X) printf("%s = " FMT "\n", #X, X)
                    
                    struct empty {};
                    
                    int main(void) {
                      struct empty a1;
                      struct empty a2;
                      print("%lu", sizeof(struct empty));
                      print("%p", &a1);
                      print("%p", &a2);
                    }


                    1. iig
                      09.01.2019 10:00

                      Забавно ;)
                      Есть, значит, способ сломать стандартный трюк с sizeof(array)/sizeof(array[0]).


          1. lgorSL Автор
            08.01.2019 13:28

            В С при увеличении указателя на void людя хотят именно перемещаться по байтам в памяти. Если размер void вместо 1 станет 0, то тогда сломается совместимость.


            1. iig
              08.01.2019 13:51

              при увеличении указателя на void людя хотят именно перемещаться по байтам в памяти.

              хотят ожидают
              Тогда логично использовать char*
              А то можно представить себе архитектуру, где указатели указывают на машинное слово, а байты — по прежнему 8-битные.


            1. NeoCode
              08.01.2019 21:39

              Ну если размер объекта 0 байт, то объекта не существует в памяти, не так ли? И совершенно логично, что операции перемещения по байтам в памяти для такого объекта бессмысленны. Фактически, объекты и массивы типа void, если их ввести в некий язык программирования, могут существовать только на этапе компиляции, для каких-то целей обобщенного метапрограммирования. В скомпилированной программе никаких следов таких объектов оставаться не должно.
              Компилятору известно что объект 0 байт, стало быть он может выдать ошибку при попытке получить адрес такого объекта (или возвратить NULL, не знаю как лучше), и т.д.


              1. a-tk
                09.01.2019 08:15

                > или возвратить NULL, не знаю как лучше

                Вот этого точно делать не стоит, потому что возвращённый указатель может куда-то каститься и дальше разыменоваться, и бум случится в рантайме.
                С другой стороны, даже пустой (в смысле без полей) объект может иметь методы, на которые может захотеться получить указатель, который потом захочется вызвать. И что делать?


                1. NeoCode
                  09.01.2019 09:03

                  Указатель на метод это указатель на функцию, там проблем нет.
                  А если нужен указатель на объект без полей для передачи его в качестве «this», то он все равно не должен использоваться (полей-то нет) — поэтому компилятор может или выкинуть неявный аргумент this вовсе, или передавать null. Если же есть виртуальные методы, по появляется поле vfptr, значит уже не 0 байт.


                  1. a-tk
                    09.01.2019 09:21

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


                    1. NeoCode
                      09.01.2019 16:31

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


  1. DaneSoul
    07.01.2019 19:42

    Если бы в питоне можно было объявлять лямбды более коротким способом, хотя бы it => it.y > 2, то генераторы списков оказались бы не очень нужными.
    Python пропагандирует что код должен быть по возможности простой и хорошо читаемый.
    И в плане читаемости генераторы списков намного понятней чем нагромождение лямбд, даже если придумать более короткий способ эти лямбды задавать.
    Генераторы списков простые и наглядные, по сути, более компактно записанный цикл.
    При этом сохраняется высокая гибкость конструкции:
    list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
    list_b = [x**3 if x < 0 else x**2 for x in list_a if x % 2 == 0]
    # вначале фильтр пропускает в выражение только четные значения
    # после этого ветвление в выражении для отрицательных возводит в куб, а для остальных в квадрат
    print(list_b)   # [-8, 0, 4, 16]


    1. etho0
      07.01.2019 20:59

      Заметьте что порядок действий в словах не совпадает с порядком действий в коде(при прямом чтении слева направо). Разве не логичней было б:

      list_b = [for x in list_a: if x % 2 == 0: if x < 0: yield x**3 else: yield x**2]
      


      1. DaneSoul
        07.01.2019 23:42

        В текущем виде то что справа от for x in list_a — это фильтры, что слева — условие обработки отфильтрованного. Может и не очень логично, но запомнив один раз легко потом вычленять в коде и понимать.
        В Вашем случае оба типа условий идут подряд, визуально воспринимать тяжелей.


    1. potan
      07.01.2019 20:59

      Вот как раз этот код я бы читабельным не назвал, из-за двойственного использования if — как тернарный оператор в теле и как фильтр в генераторе.
      Я много программировал на Scala и Haskell, где генераторы применяются довольно часто. Не всегда это делает код понятнее, иногда явное использование map, filter и flatMap(>>=) более выразительно.


      1. DaneSoul
        07.01.2019 23:56

        Так можно и в Python явно использовать filter, причем даже в генераторе списка:

        list_c = [x**3 if x < 0 else x**2 for x in filter(lambda x: x % 2 == 0, list_a)]
        

        Это аналог моего примера выше с тем же выводом.
        НО! Мой первый пример эффективней, так как там исходная последовательность проходится один раз, по ходу прохода идет и фильтрация и преобразование, а в случае фильтра — вначале проход для фильтрации, потом проход для преобразования, иногда это может быть важно!


        1. potan
          08.01.2019 00:09

          Если на список создается итератор, то filter/map/flatMap будут столь же эффективны, как и генератор.


          1. DaneSoul
            08.01.2019 00:23

            (Коментарий исправлен после проверки гипотезы в коде)
            У меня получается самый быстрый вариант на генераторе списка, решение на map/filter/lambda в более чем в 1,5 раза медленей…

            Код для проверки
            import time
            
            list_data = range(0, 10000000)
            
            start = time.time()
            list_huge = [x**3 if x < 0 else x**2 for x in list_data if x % 2 == 0]
            end = time.time()
            print(end - start)  # 1.896183967590332
            
            
            start = time.time()
            list_huge = [x**3 if x < 0 else x**2 for x in filter(lambda x: x % 2 == 0, list_data)]
            end = time.time()
            print(end - start)  # 3.0600476264953613
            
            start = time.time()
            list_huge = list(map(lambda x: x**3 if x < 0 else x**2, filter(lambda x: x % 2 == 0, list_data)))
            end = time.time()
            print(end - start)  # 3.264080762863159


        1. DaneSoul
          08.01.2019 01:22

          Я был не прав, прохода второй раз не будет, так как filter — это генератор сам по себе, но тем не менее, данный пример примерно в 1,5 раза медленней, в моем комменте выше есть тесты с примерами.


          1. Sirikid
            09.01.2019 06:47

            А у Haskell компилятор умеет в stream fusion, он и сам все операции оптимально соберет.


    1. rssdev10
      07.01.2019 21:34

      К вопросу о лёгкой читаемости кода на питоне....


      Это Ruby, если что...


      list_b = [-2, -1, 0, 1, 2, 3, 4, 5].select(&:even?).map { |x| x < 0 ? x**3 : x**2 }

      А это Julia:


      list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
      list_b = map(x -> x < 0 ? x^3 : x^2,  filter(iseven, list_a))

      list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
      list_b = filter(iseven, list_a) |> list -> map(x -> x < 0 ? x^3 : x^2, list)

      list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
      list_b = filter(iseven, list_a) |> 
               list -> map(list) do x
                  x < 0 ? x^3 : x^2
               end

      И во всех случаях всё прозрачно. Либо читаем последовательно, либо разворачиваем скобки в порядке вложенности.


      1. a-tk
        08.01.2019 16:30

        from x in new[]{-2, -1, 0, 1, 2, 3, 4, 5}
        where x % 2 != 0
        let pow = x < 0 ? 3 : 2
        select Pow(x, pow)


        Это C# к слову


        1. 0xd34df00d
          08.01.2019 17:17

          Как-то многословно.


          [ x ^ exp | x <- [-2 .. 5]
                    , x `mod` 2 /= 0
                    , let exp = if x < 0 then 3 else 2
                    ]


          1. a-tk
            08.01.2019 20:00

            Если убрать явный массив и сделать как у Вас, то с точностью до форматирующих пробельных символов код получается одинаковым по длине — 83...85 символов, в обеих реализациях.
            Знаки препинания в Вашем коде разменялись на буквы в моём. Но буквы может прочесть относительно неподготовленный разработчик, то в Вашем коде, имхо, в каждую закорючку вкладывается свой смысл. Я лично только из контекста того, что делает код, понял различие между x < — … и let x =…
            Но всё это вкусовщина, конечно.


            1. 0xd34df00d
              08.01.2019 20:35

              Вкусовщина, конечно, да.


              Математику, наверное, такая запись будет понятнее. И в обратную сторону: я вот программист, но from x in new ... я понял только тоже из контекста.


              Ну и в linq можно, например, добавить ещё один where-блок после вашего let pow?


              1. a-tk
                08.01.2019 20:39

                Ну, можно new[]{...} заменить на Range(...) (предполагается using static… Enumerable)
                Условия добавляются прозрачно: ещё одно where пишете и всё.


                1. rssdev10
                  08.01.2019 20:54

                  (-2..5).select(&:even?).map { |x| x < 0 ? x**3 : x**2 }

                  56 символов. И читаемость никуда не пропала.


                  1. nexmean
                    10.01.2019 12:37

                    map (\x -> x ^ if x < 0 then 3 else 2) $ filter odd [-2 .. 5]

                    Сиподобная тернарка это ну такое. Функции в обратную сторону тоже, хотя при желании можно написать конечно и так:


                    [-2 .. 5] & filter odd & map (\x -> x ^ if x < 0 then 3 else 2)


    1. KES777
      09.01.2019 15:29

      Попробовал переписать выражение на perl


      @list_b =  map{ x<0? x**3 : x**2 } grep { x%2 == 0 } @list_a


    1. PsyHaSTe
      09.01.2019 18:58

      Не знаю, в питоне лямбды очень не очень.


      Вот я например хотел агрегировать единственный элемент, а если какие-то элементы не совпадают, то бросить ошибку. В шарпе я бы написал


      int[] array = {1,1,1,1,2};
      int value = array.Aggregate((a,b) => a == b ? a : throw new InvalidOperationException("All values should be equal");

      в питоне мне в итоге пришлось делать как-то так:


      def raise_(ex):
          raise ex
      
      ...
      
      value = reduce(lambda a, b: (a if a
                                     == b else raise_(ValueError('All values should be equal'
                                                              ))), array)

      выглядит весьма фигово. Про необходимость в лямбде иногда больше 1 действия сделать я молчу.


  1. go-prolog
    07.01.2019 19:52

    У вас:


    удобный синтаксис
    располагающий писать в декларативном стиле и использовать константы.

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


    1. potan
      07.01.2019 20:48

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


  1. rozhik
    07.01.2019 19:55

    1. Ну вот, получился typescript ;) (почти) с#
    2. Препроцессоры это огромное зло (или по чему я не люблю С++)
    3. Вы в основном сосредоточились на синтаксисе но есть многое, что трудно эмулируется
    К примеру- замыкания, upvalues, try/catch/, yild, некоторое из функциональных или декларативных языков, прототипное наследование, миксины. Модель работы с памятью, с тредами, с параллельными вычислениями, с распределенными вычислениями, сигналинг, изоляции…


    1. potan
      07.01.2019 20:51

      В C/C++ подход к препроцессору был очень плохо продуман. Но применение макросов в Lisp, Nemerle, OCaml и Rust показывает, что такой подход может быть очень удобным и полезным.


      1. rozhik
        08.01.2019 08:52

        С OCaml и Nemerle не знаком. Но в остальных случаях, по моему мнению, препроцессор — недостаток выразительных средств языка. Даже в случае кондишн-компайлинга можно обойтись и без него.
        Забыл где лежит большое письмо создателя делфи сишарпа и тайпскрипта, там где он взвешивает +- препроцессора. Для меня все ++ малоубедительны.


        1. Druu
          08.01.2019 13:59

          Но в остальных случаях, по моему мнению, препроцессор — недостаток

          Макросы не имеют ничего общего с препроцессорами.


          1. 0xd34df00d
            08.01.2019 14:41

            TH, например, таки не очень жалуют.


          1. rozhik
            08.01.2019 16:30

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


    1. xPomaHx
      09.01.2019 14:26

      До идеального в ts нету перегрузки операторов имхо.


  1. potan
    07.01.2019 20:46
    +1

    Генераторы списков — это аналог for в Scala, только менее обобщенное. Основная его фишку не столько замена map и filter, сколько удобное использование flatMap.
    Union-типы, как они ожидаются в Dotty, это не алгебраические типы-суммы. A | A тождественно A, а Either[A,A] несет больше информации. На мой взгляд Union-типы — опасная штука, которая будет порождать неожиданные эффекты при обобщенном программировании (например, код обработки ошибок может начать путать код ошибки и корректные данные, если они окажутся одного типа). Но в теории подход интересен.
    Применение зависимых типов в императивных языках еще плохо проработано. Если меняется значение переменной, в которой хранился размер массива, что должно произойти с массивом? В ATS пытаются подобные проблемы решить, подружив зависимые типы с линейными, но пока далеко не продвинулись.


    1. sashagil
      08.01.2019 14:58

      Заинтересовался, что такое ATS — выглядит, как будто один автор, или кафедра в каком-то китайском университете пилит понемногу, диковато как-то.


      1. 0xd34df00d
        08.01.2019 15:04

        На Idris не смотрите тогда, а то вообще дичью покажется.


        1. sashagil
          08.01.2019 15:06

          Почему же, я на Idris смотрю, и дичью он мне не кажется. Вы можете сравнить ATS и Idris?


          1. 0xd34df00d
            08.01.2019 15:41

            Ну, его ещё меньше людей разрабатывает, в этом-то всё дело.


  1. xomachine
    07.01.2019 21:29

    Мне кажется, большая часть перечисленного в статье, уже есть в Nim. Вот несколько примеров:

    Макросы поддерживают работу с синтаксическим деревом.

    import macros
    macro hello(x: untyped): untyped =
      result = x
      # Сакральный смысл индексов ниже:
      # Первый индекс - утверждение (у нас оно одно)
      # Второй индекс - часть утверждения (для нашего случая под номером 1 будет первый аргумент
      #      0(1, 2...)
      # 0: echo("goodbye world")
      # 1: ...
      result[0][1] = newStrLitNode("hello world")
    hello:
      echo("goodbye world") # Превратится в echo "hello world"
    

    Кортежи без предварительного объявления
    proc x(): (int, int) = (4, 2)
    

    Лямбды:
    let f = (x:int)=>x*x
    echo(f(2)) # 4
    


    Удобная система сборки: nimble

    Не хватает разве что ленивых вычислений из коробки (но можно сделать за счёт макросов) и хорошей интеграции с IDE, но это дело наживное. Ах, да, ещё подсветки синтаксиса хабрадвижком, чтобы не пользоваться питоновской подсветкой.


  1. Akon32
    07.01.2019 22:21
    +1

    Добавлю к субъективному набору фич идеального языка (для меня) ещё несколько:
    1) Автоматическое управление памятью: gc или хотя бы RAII. Лучше gc — он нормально обрабатывает циклические ссылки.
    2) Перемещающий сборщик мусора. На архитектурах с малым (2-4ГБ) максимальным объёмом виртуальной памяти куча иногда фрагментируется, и хотелось бы, чтобы выделенные куски памяти могли перемещаться для дефрагментации свободного места. На 64-разрядных архитектурах такая проблема выражена в меньшей степени.
    (я понимаю, что gc удобен, но уместен не везде. указанные фичи, как мне кажется, критичны для достаточно "больших" систем)
    3) (наименее холиварный пункт) Именованные параметры при вызове. Очень удобны вместе со значениями по-умолчанию для конфигурации метода с большим набором параметров.


    case class A(x: T = a1, y: U = b2, z: V = c3)
    
    val a = A(y = b5)

    4) Близость к железу, хорошая оптимизация компилятором или JIT-компилятором. Когда вы возвращаете из метода пару (Int,Int), она всё-таки не должна аллоцироваться в куче, как в scala, где какое-нибудь val (x,y)=p.coords() в цикле может напрочь убить производительность (вы видите в строке 4 new? а они есть!). Может, этот конкретный пример уже компилируется более оптимизированно, сейчас не проверял. Также вложенные for-yield for(x<-xs if f(x); y<-ys(x) ...) yield ... когда-то неожиданно сильно жрали память.
    5) Community-фичи: поддержка, документация, обновления, кроссплатформенность, свободные компиляторы и IDE, библиотеки. Недостаточно создать язык, нужно ещё и 100500 доступных в нём библиотек.


  1. michael_vostrikov
    07.01.2019 22:30

    Технически они являются просто методами со специальными именами, и их в любой момент можно переопределить.

    Технически, значение должно где-то храниться, в этом и отличие от методов.


    1. Sirikid
      09.01.2019 06:56

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


      1. michael_vostrikov
        09.01.2019 07:17

        В контексте создания компилятора вы можете не разделять сущность для хранения данных на "Поле" и "Свойство", но не можете убрать ее совсем и использовать для замены только сущность "Метод". Поэтому говорить, что технически она является просто 2 методами, некорректно.


        1. lgorSL Автор
          09.01.2019 13:21

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


  1. 411
    07.01.2019 22:33
    +1

    Я бы сказал, что «идеальный» язык программирования должен быть минималистичен, но при этом иметь четкий и однозначный синтаксис. Большинство удобных синтаксических штук должны подключаться с помощью расширений. К примеру вам нужно много классных математических функций — подключили расширение и решаете математическую задачу.
    Нужна вариантность — подключили и её. Функциональные фишечки — пожалуйста. И т.д.

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

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


  1. ideological
    08.01.2019 00:01

    Околотемы вопрос, больно не пинайте.
    Добавление в Python списков с чётко заданным одним типом, вроде: listint, listfloat, listbool или liststr — помогло бы ускорить язык тогда когда это нужно?


    1. worldmind
      08.01.2019 13:30

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


      1. ideological
        08.01.2019 13:41

        Ну я и хотел спросить/предложить, почему в Python не добавят такие списки (в сам язык или в стандартную библиотеку), чтобы даже без numpy можно было, там где это нужно, указать тип всех элементов в списке.


        1. worldmind
          08.01.2019 13:46
          +1

          Не знаю, но вероятно это не решает никаких проблем за пределами тех задач где всё равно нужен numpy и товарищи, а может потребовать значительной переделки языка и реализации.
          Надо искать, может что-то такое уже спрашивали на SO или в python-ideas



  1. Scf
    08.01.2019 00:06
    +4

    Я всегда хотел несколько фишек из Ada:


    • constrained types: например, целое число от 1 до 10
    • by-name type equivalence: типы с разными именами но с одинаковым представлением данных (число или структура с одинаковым набором полей) несовместимы. т.е. type A=Int и type B=Int несовместимы без явного преобразования типов.


    1. QtRoS
      08.01.2019 10:25

      Хотели в какой язык?
      Вторая 'фишка' есть в Go, если я правильно понял описание.


      1. Scf
        08.01.2019 11:32
        +1

        В идеальный конечно :-)
        А так я скалист.


    1. iig
      08.01.2019 11:31

      constrained types

      Очень нишевая вещь КМК. Что делать, если в runtime попытались, к примеру, вычислить 8 день недели? Ловить exception?


      1. Scf
        08.01.2019 11:35
        +1

        Да. Лучше же словить исключение и откатить операцию, чем записать в базу или передать в другой сервис мусорные данные?


        Главное преимущество таких типов — если параметр функции объявлен с этим типом, то можно быть уверенным, что он находится в нужном диапазоне.


        1. NeoCode
          08.01.2019 12:13
          +1

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


    1. Hazactam
      10.01.2019 06:14

      constrained types: например, целое число от 1 до 10

      Pascal так умеет. Иногда удобно:

      Subrange Types
      Subrange types allow a variable to assume values that lie within a certain range. For example, if the age of voters should lie between 18 to 100 years, a variable named age could be declared as:
      var
      age: 18 ... 100;


      Subrange types can be created from a subset of an already defined enumerated type, For example:

      type
      Months = (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
      Summer = Apr ... Aug;
      Winter = Oct ... Dec;

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


  1. NeoCode
    08.01.2019 01:18

    Очень интересная тема! Огромное вам спасибо за нее, и комментарии очень интересные. У меня на хабре практически все статьи так или иначе не эту тему.
    Почти со всеми хотелками согласен. Немножко прокомментирую.
    Синтаксическая чистота. Глядя на незнакомый код, программист должен четко понимать — где тут операторы, где ключевые слова, где идентификаторы и т.д. То есть в языке должны быть простые правила разделения всех возможных синтаксических сущностей на категории, и по виду сущности сразу должно быть понятно к какой категории она относится. Это же огромное облегчение для syntax highlighter'ов.
    Кортежи. Я в свое время написал аж две статьи здесь, на тему кортежей и своих хотелок с ними связанных. Да, кортежи должны однозначно быть частью языка, они должны лежать в основе синтаксиса языка а не прикручиваться снаружи как это зачастую бывает. По сути в любом языке «последовательность чего-то через запятую» — базовый кортеж, и от этого нужно строить весь синтаксис.
    tagged unions Штука полезная, поддержка со стороны компилятора должна быть, но хотелось бы, чтобы чистые перечисления и чистые (низкоуровневые) unions остались.
    константы — все верно. Вы очень точно сформулировали про виды констант.
    Call-by-name семантика Интересны вопросы реализации. Это может быть рантайм — неявно генерируемая лямбда-функция, или compile-time — тогда это шаблонная функция, принимающая фрагмент кода шаблонным параметром.
    преобразования да, идея с явным разрешением (или явным запретом) преобразований очень красивая. Для разных программистов и для разных целей может требоваться разный уровень «неявности» преобразований.
    рефлексия — может тоже опцией? хотим сгенерировать для класса метаинформацию — добавляем какое-то ключевое слово перед описанием класса, или ставим глобальную прагму. А кому-то может наоборот нужно оставить в бинарнике как можно меньше следов:)
    Значения, ссылки, указатели примитивные типы не должны притворяться, они должны быть объектами — но при этом оставаться примитивными типами! Не понимаю почему так не сделать. Ну и по шаблонам в С++ — это концепты, то есть по сути введение нормальной статической типизации в систему шаблонов. Все те гигантские error'ы, которые вылазят, если в шаблон передать не то что ожидается — прелести динамической типизации:)
    Минимум сущностей вот здесь не согласен. Поля, методы и свойства — это разные сущности, пускай и будут разными. Делать все поля приватными насильно — не хочу.
    Макросы однозначно да. Я писал об этом статью со своим видением, впрочем с тех пор уже кое-что поменялось, да и в дискуссии выяснились некоторые дополнительные факты — в частности, людям нужен универсальный код, который можно выполнить и в runtime и в compile-time.
    Функции внутри функций Ну это вообще очевидная вещь. В расширениях GCC она реализована давно, но в стандарте до сих пор нет. Почему?
    Substructural type system пока не очень понятно
    Сборка однозначно не так как в С++. Во всех следующих языках все сделано гораздо лучше.


  1. 0xd34df00d
    08.01.2019 02:49
    +1

    Итак, вы захотели параметрический полиморфизм, ограничения полиморфизма и сабтайпинг (а почему не row polymorphism, кстати?). У меня к вам всего один вопрос из трёх пунктов.


    Будем обозначать тот факт, что T — подтип U, как T <: U. Кроме того, для дженерик-функций, параметризованных параметром X, являющимся сабтайпом U, будем обозначать этот факт как ?X <: U в начале их типа.


    Итак, пусть T? <: T? и U? <: U?.


    1. Состоят ли типы ?X <: T?. X > U? и ?X <: T?. X > U? в отношении сабтайпинга?
    2. Состоят ли типы ?X <: T?. X > U? и ?X <: T?. X > U? в этом отношении?
    3. Как насчёт ?X <: T?. U? и ?X <: T?. U??


    1. VaalKIA
      08.01.2019 06:37

      Вот за это я и не люблю математику! :-)


      1. xPomaHx
        09.01.2019 14:35

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


        1. 0xd34df00d
          09.01.2019 18:03

          Я не уверен, что правильно распарсил.


          В математике много неудобных нелогичных решений или в любом произвольном ЯП?


          1. xPomaHx
            10.01.2019 17:28
            -1

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


    1. Druu
      08.01.2019 14:11

      Состоят ли типы ?X <: T?. X > U? и ?X <: T?. X > U? в этом отношении?

      Может, ?X <: T?. X > U? и ?X <: T?. X > U?? Иначе-то очевидно не состоят, т.к. для стрелки вариантность аргументов должна быть разной.


      Как насчёт ?X <: T?. U? и ?X <: T?. U??

      Можно же просто запретить термы, в которых связывается неиспользуемая типовая переменная :)


      1. 0xd34df00d
        08.01.2019 14:42

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

        Да, не состоит. Это, так сказать, подготовка перед третьим вопросом.


        Можно же просто запретить термы, в которых связывается неиспользуемая типовая переменная :)

        А кто сказал, что она в U'шках не используется? ;) Я, конечно, немного неаккуратно поставил вопрос, ибо нельзя сказать, что U? <: U?, не описывая, что в контексте при этом X, но мне было лень описывать понятие контекста и идти в википедию за уникодовым значком для turnstile.


        Но вопрос-то таки с подвохом.


    1. lgorSL Автор
      08.01.2019 14:18

      1. да, левый — подтип правого.
      2. нет. (если поменять местами T? и T?, то да)
      3. Хм. Тут подвох? Я вижу два варианта ответа, но не могу сказать, какой из них лучше:
        можно сказать, что выбор типа X не влияет на U, и потому левый будет подтипом. С другой стороны, если предположить, что T? это bottom type, а T? и U? — нет, то левый не сможет быть подтипом правого.


      1. 0xd34df00d
        08.01.2019 15:03

        Тут подвох?

        Тут всё сложно, на самом деле.


        можно сказать, что выбор типа X не влияет на U

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


        С другой стороны, если предположить, что T? это bottom type, а T? и U? — нет, то левый не сможет быть подтипом правого.

        Почему?


        Хорошая интуиция для дженериков (или, как сказал бы типотеоретик, для System F) — это просто функции из типов в термы?. То есть, функции, которые принимают тип и возвращают терм, использующий этот тип, прям как обычные функции принимают терм и возвращают терм. Тогда возвращаемое значение правой функции — подтип возвращаемого значения левой функции, а принимаемое значение правой функции — надтип принимаемого левой. То есть, вполне логично было бы сказать, что они в этом отношении состоят. Ну или, апеллируя к интуиции, функцию справа можно использовать в любом контексте, где используется функция слева.


        Но проблема глубже. Сказать, что они в этом отношении состоят, действительно можно, но если вы разрешаете варьировать ограничения под ?, то можно показать, что тайпчекинг становится неразрешимым по Тьюрингу для любых полезных систем типов. К неразрешимым системам типов хаскелистам (и уж тем более плюсистам, у них и парсинг-то неразрешим) не привыкать, конечно, но всё равно неприятно.


        ?

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


        1. Druu
          09.01.2019 07:43

          Но проблема глубже. Сказать, что они в этом отношении состоят, действительно можно, но если вы разрешаете варьировать ограничения под ?, то можно показать, что тайпчекинг становится неразрешимым по Тьюрингу для любых полезных систем типов. К неразрешимым системам типов хаскелистам (и уж тем более плюсистам, у них и парсинг-то неразрешим) не привыкать, конечно, но всё равно неприятно.

          Вобщем-то, если чек подтипирования ограничить по глубине рекурсии, как те же темплейты из плюсов — то найти терм, который эту глубину потом таки пробьет, еще постараться надо :)


          1. a-tk
            09.01.2019 08:17

            > как те же темплейты из плюсов

            а можно с этого момента чуть поподробнее?


            1. 0xd34df00d
              09.01.2019 18:05

              Вы можете написать


              template<unsigned int N>
              struct Foo : Foo<N - 1> {};
              
              Foo<10000> foo;

              и компилятор имеет право сделать сильно меньше 2^32 инстанциаций.


          1. 0xd34df00d
            09.01.2019 18:04

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


            Но это так, эмпирическое наблюдение.


            1. Druu
              10.01.2019 09:54

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


  1. Yermack
    08.01.2019 11:36

    Присмотритесь к языку Julia — его создавали как раз переосмысливая хотелки программистов


  1. AN3333
    08.01.2019 13:21
    +2

    Вот пишу я 30 лет на С++. В слезах. Периодически поглядываю что нового. Вижу смену модных языков которые меняют как перчатки. Отличаются они набором штучек — чего хочется. Иногда попадаются наборы которые мне лично нравятся. Ну и что? Есть ли причина переходить на какой-то конкретный новый набор?

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

    Я бы перешел на аналог С++ с приличным синтаксисом, если бы он имел сравнимую с С++ поддержку. Чего нет. Есть ли еще какая-то причина?


    1. lega
      08.01.2019 13:45

      Dlang был как претендент, но не взлетел.


      1. AN3333
        08.01.2019 13:50

        Еще был Eiffel и тоже не взлетел. Вот его я хотел попробовать.


      1. OldFisher
        08.01.2019 15:43

        Точнее говоря, до сих пор пытается взлететь «с толкача», неся тяжёлые потери в борьбе со сборщиком мусора.


    1. NeoCode
      08.01.2019 14:21
      +1

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


    1. vlad9486
      08.01.2019 21:12

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

      Что бы потеснить C++ мало быть лучшим, нужно быть намного-много-много лучшим. А пока что-то такое изобретут, опытные и молодежь будут страдать.


    1. PsyHaSTe
      09.01.2019 19:04

      Вот пишу я 30 лет на С++

      Я бы перешел на аналог С++ с приличным синтаксисом, если бы он имел сравнимую с С++ поддержку. Чего нет. Есть ли еще какая-то причина?

      Ну вот через 30 лет современные молодые языки получат сравнимую поддержку (особенно по количеству легаси)


      А вообще тут вопрос простой: кто раньше влез в удачный вагон, тот потом и гребет лопатой интересные проекты. Всё как в жизни.


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


      1. 0xd34df00d
        10.01.2019 01:13

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

        А самое главное — и выкидывать-то особо ничего не нужно, на самом деле.
        Ну вот люблю я темплейтами упарываться — так это ж просто определённый вид DSL с определёнными ограничениями. Оно переносится на раст (судя по тому, что я знаю о расте). Оно, блин, даже на хаскель переносится!
        Ну вот люблю я в байтики и SIMD пердолиться — ну так навыки в оптимизации программ будут полезны и в таком же императивном близком к железу языке, как Rust. Особенно если учесть, что у него тот же llvm'ный бекенд.
        Ну вот есть у меня полтора десятка лет отладки за плечами, так я лучше и глубже знаю машину, многопоточность, работу с памятью, работу с внешними библиотеками на сях, которые неизбежно будут всегда.
        И так можно, на самом деле, долго продолжать.


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


    1. Hazactam
      10.01.2019 06:26

      Вот пишу я 30 лет на С++. В слезах. Периодически поглядываю что нового. Вижу смену модных языков которые меняют как перчатки. Отличаются они набором штучек — чего хочется. Иногда попадаются наборы которые мне лично нравятся. Ну и что? Есть ли причина переходить на какой-то конкретный новый набор?
      Тоже самое, только Делфи, не так, правда, долго (17-й год). Язык очень нравится. Поглядываю еще что-то. Но так ничего толком и не взлетело. Что бы всё в одном и нативное, без виртуалок.


  1. PhoenixUA
    08.01.2019 13:22

    Powershell:
    $filteredLst = $lst | where y -gt 2
    Есть скриптблоки — анонимные функции — аналог лямбда.
    Их можно выполнить на месте, а можно передать как параметр функции.
    Каринг тоже можно реализовать методом GetNewClosure():
    function f ( $x ) {
    {
    param( $y )
    $x + $y
    }.GetNewClosure()
    }
    Set-Item function:GetPlus2 -Value (f 2)
    GetPlus2 3


    Читаемость получше чем в Python будет:
    $list_b = $list_a | Where {$_ % 2 -eq 0 } | foreach { If ($_ -gt 0) {$_*$_} else {$_*$_*$_} }
    Хотя с математикой родными средствами не очень, можно использовать .Net-класс Math.


    1. xomachine
      08.01.2019 15:42
      +2

      Читаемость получше чем в Python будет

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


      1. 0xd34df00d
        08.01.2019 15:44

        Как только вы изучили эти символы пунктуации — да. Потому что ресурсов на то, чтобы распарсить и отличить <$> от <*> и от прочих идентификаторов в тексте, надо меньше, чем fmap от liftA2 id, скажем.


        Математики тоже всё время значки выдумывают, нет бы естественным языком писать!


        1. xomachine
          08.01.2019 15:49

          В конце концов, языки программирования пишутся для программистов, а не для парсера.

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


          1. 0xd34df00d
            08.01.2019 16:21

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

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


        1. Druu
          09.01.2019 07:49

          Как только вы изучили эти символы пунктуации — да. Потому что ресурсов на то, чтобы распарсить и отличить <$> от <*> и от прочих идентификаторов в тексте, надо меньше, чем fmap от liftA2 id, скажем.

          Ну математики сокращают запись за тем, что обычно с выражениями работают. А так, утверждение о том, что ресурсов требуется меньше — не совсем очевидно. Человек слова обычно воспринимает целиком, как единый объект (если это не ребенок, который учится читать), и в этом случае кажется логичным, что проще различить два объекта которые отличаются, например, 3 буквами из 5, чем 1 из 3.


          1. Zenitchik
            09.01.2019 11:19

            3 буквами из 5, чем 1 из 3.

            Это зависит от того, насколько различны графически те буквы, которые различаются.
            В этом смысле спецсимволы предпочтительнее — они изначально более разные, чем буквы.


            1. Druu
              09.01.2019 13:00

              Но факт в том, что плотную абракадабру человек воспринимает медленнее, чем раздельный текст.


              1. Zenitchik
                09.01.2019 13:15

                О плотной абракадабре, вроде как речи не шло.


          1. 0xd34df00d
            09.01.2019 18:07

            Ну, прочитать f <$> c мне существенно быстрее, чем fmap f c, и сразу ясно, что там как.


      1. lair
        08.01.2019 16:43

        Разве нагромождение символов пунктуации читабельнее обычных английских слов с редкими вкраплениями пунктуации?

        Это зависит от того, к чему вы привыкли. Кому-то удобнее "X, следовательно, Y", кому-то — X => Y.


  1. worldmind
    08.01.2019 13:33

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


    1. lair
      08.01.2019 13:59
      +5

      мне вот кажется что он наиболее близо к идеалу, хотя ничего реального на нём не делал

      Мне кажется, эта фраза лучше всего описывает идеальные языки программирования.


      1. AN3333
        08.01.2019 14:21

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


        1. lair
          08.01.2019 14:39
          +2

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


          1. worldmind
            08.01.2019 14:56
            -1

            Мне думается это проблема с образованием — учат непойми чему аля паскаль, а потом уже трудно перестраиваться, да и времени учить что-то сильно отличающеся мало, вот в итоге и получается относительно малое сообщество.
            По идее именно в студенческие годы и время есть и мозги посвежее, чтобы пихать туда разные концепции, а не учить несколько ничем принципиально не отличающихся языков.
            Предполагаю, что Python, Haskell, Lisp, Prolog, SQL видимо должны изучаться на хорошем уровне как примеры достаточно разных подходов. Что-то ещё?


            1. AN3333
              08.01.2019 15:28
              +1

              Не очень понятно. Почему Паскаль аля чего, а скажем Python должен изучаться?


              1. worldmind
                08.01.2019 15:33

                Не очень удачно сформулировал: s/учат непойми чему аля паскаль/учат только чему-то аля паскаль/


            1. lair
              08.01.2019 16:39

              а потом уже трудно перестраиваться

              Если трудно перестраиваться — значит, учили плохо и не тому. Я вот после десяти с гаком лет C# взял в руки Python — и ничего, жив пока.


              Предполагаю, что Python, Haskell, Lisp, Prolog, SQL видимо должны изучаться на хорошем уровне как примеры достаточно разных подходов

              Я предполагаю, что это зависит от того, кого и для чего учат. Мне ваша подборка кажется произвольной.


              1. worldmind
                08.01.2019 16:44

                > после… C# взял в руки Python

                для меня это языки одного класса, принципиальных отличий нет

                > Мне ваша подборка кажется произвольной.

                Python — традиционной императивное программирование
                Haskell — чистое функциональное программирование
                Lisp — не знаю как его категоризировать, но это мутант ещё тот, от остального сильно отличается
                Prolog — логическое/предикатное программирование
                SQL — декларативное программирование в терминах предметной области, по сути пример декларативного DSL


                1. lair
                  08.01.2019 16:48

                  для меня это языки одного класса, принципиальных отличий нет

                  Ну так это для вас. А для меня они очень далеки.


                  Если что, F# я взял в руки намного раньше, и там переход был меньше.


                  Мне ваша подборка кажется произвольной.

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


                  BTW, Лисп — это как раз чистое функциональное программирование.


                  1. worldmind
                    08.01.2019 17:08

                    > BTW, Лисп — это как раз чистое функциональное программирование.

                    изначально да, но как я понял сейчас там каша из всех концепций


                    1. lair
                      08.01.2019 23:00

                      Ну так и в Питоне каша.


                      1. worldmind
                        09.01.2019 10:26

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


                        1. lair
                          09.01.2019 11:30

                          Вот потому и каша, а не Истинное Функциональное Программирование.


                  1. 0xd34df00d
                    08.01.2019 17:19

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


            1. Hazactam
              10.01.2019 06:46

              учат непойми чему аля паскаль, а потом уже трудно перестраиваться
              Если даже Паскаль плохо даётся, я не знаю что вообще в программировании можно делать? Мне кажется, что с Бейсика/Паскаля можно перейти на любой язык почти сразу.


            1. babylon
              10.01.2019 09:04
              -1

              f..mind перестраиваться как раз и не учат. Учат соответствовать. И все оценки вытекают из того насколько вы близки или далеки от предмета обучения. Если потом вдруг выясняется, что вас учили не тому, то система оценок, зафиксированная в вашем красном дипломе, девальвируется. И хорошо если вы сами это понимаете и не страдаете от комплексов. Отличники этому подвержены реже. Так как им все равно, что изучать и чему максимально соответствовать. И чем циничнее такой спец, тем лучше.
              Перестраивание и возврат при неудаче между тем основа поиска решений даже в тупиковых ситуациях. Поддержка и сообщество это сомнительные критерии идеальности. В основе идеальности лежат идеи. Насколько хороши идеи настолько язык идеален и удобен. Удобство это еще одно требование человека к языку.
              Lisp хорош, но вы про JSONNET и SmallTalk забыли. Я бы предложил начать изучение языков со структур данных, AST, КС -грамматик. Допустим, не все понимают, что объект это уже упакованная структура данных и поэтому предлагают нисходящие алгоритмы там где должны быть самосборки и т.д.
              Возможность воспроизведения объекта ценнее его уникальности.
              От того как ты думаешь, вытекает то как ты решаешь задачи. Подход к решению важнее текущих знаний…
              Поворчал немного. Возраст, однако


              1. Zenitchik
                10.01.2019 15:11

                f..mind перестраиваться как раз и не учат.

                А этому можно научить? Мне казалось, что человек либо может, либо не может. Гены…


      1. 0xd34df00d
        08.01.2019 15:06

        Не, это фраза, которая описывает впечатления.


        Мне когда-то тоже так казалось, а потом я начал на нём делать что-то реальное, и понял, что идеальных языков не бывает :(


        1. worldmind
          08.01.2019 15:34

          Но бывают же ближе к идеалу? Чем в итоге хаскел не понравился?


          1. 0xd34df00d
            08.01.2019 15:42

            Ну, ближе, да, бывают.


            Не понравилась неконсистентность языка и слабая выразительность системы типов. Но можно сказать, что это я зажрался.


            1. worldmind
              08.01.2019 15:47

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


              1. 0xd34df00d
                08.01.2019 16:22

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

                А неконсистентность — тоталити чекера нет и, наверное, не будет никогда, undefined есть, seq ведёт себя так себе, в итоге как логика язык неконсистентен (что сильно уменьшает полезность завтипов), и вообще Hask is not a category.


                1. worldmind
                  08.01.2019 16:27

                  ) конечно же я ничего не понял, напишите при случае пост об этом.


                  1. 0xd34df00d
                    08.01.2019 17:20

                    А там конструктива нет, не знаю, о чём писать. А статьи вроде той о фрактале плохого дизайна, или какой-нибудь крик души про Go/Python/C++ — это всё у меня очень плохо получается.


                    1. worldmind
                      08.01.2019 17:44

                      Не конструктивом единым, критика тоже нужна: «Почему хаскел не идеальный язык?» — вот мол видится мне в этих местах плохой дизайн, может кто знает почему так? Или знает решения?
                      Обгалдить конечно могут, этот тут мастера, но шанс на фидбэк зачем упускать?


                    1. worldmind
                      09.01.2019 10:27

                      Хотя может эту тему надо поднимать на буржуйский сайтах — SO, reddit и т.п., всегда есть шанс, что у описанных проблем есть какие-то важные причины и кто-то их знает.


                      1. 0xd34df00d
                        09.01.2019 18:07

                        Причины у всего этого исторические: когда хаскель запиливали 30 лет назад, никто не думал о том, что об вот этом всём надо задумываться. Design goals у него другие были.


        1. lair
          08.01.2019 16:43

          понял, что идеальных языков не бывает

          Ну вот я об этом.


  1. andrey_ssh
    08.01.2019 15:10
    +1

    Процедуры и функции — это два сильно разных способа организации кода. И не следует сливать их в один.
    То, что в Pascal/Delphi они мало чем отличаются это недоработка Вирта/Хеилсберга (ну или особенность эпохи).
    В Аде различие между procedure и function сильнее, но и там достаточно слабое.
    В идеальном языке подпрограммы, объявленные как функции, должны контролироваться на чистоту.


    1. iig
      08.01.2019 15:33

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


      Как и почему? Можно с маленьким примером.


      1. AN3333
        08.01.2019 15:39

        Мне тоже интересно.


      1. 0xd34df00d
        08.01.2019 15:43
        +2

        Скорее интересно, почему только функции должны контролироваться на чистоту. Как их на чистоту контролировать, как раз ясно, чистое ФП давно это показывает.


      1. vintage
        08.01.2019 22:24

        1. iig
          08.01.2019 23:28

          Ага, спс.
          То есть чистая функция может вызывать внутри только чистые функции. никакого ввода-вывода, выделения памяти…


          1. 0xd34df00d
            09.01.2019 00:54

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


        1. AN3333
          09.01.2019 15:14

          А… речь о функциональном программировании.


    1. VaalKIA
      08.01.2019 16:12

      В чём проявляется более сильное различие в Аде? И почему в идеальном языке процедуры не должны быть чистыми, ведь побочные эффекты, это — зло?


      1. Zenitchik
        08.01.2019 16:37
        +3

        Потому что процедура — не возвращает значения. Если у неё ещё и побочных действий не будет — она станет совсем не нужна.


        1. VaalKIA
          08.01.2019 16:55

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


        1. 0xd34df00d
          08.01.2019 17:21

          То есть, раз она не возвращает значение, то она и управление не возвращает?


          1. a-tk
            08.01.2019 20:03

            Нет. Если она не возвращает значения и не имеет побочных эффектов, то с точки зрения вызывающего кода нет разницы, вызывать её или нет


            1. 0xd34df00d
              08.01.2019 20:35

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


        1. lgorSL Автор
          08.01.2019 23:07

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

          Потому что процедура — не возвращает значения. Если у неё ещё и побочных действий не будет — она станет совсем не нужна.

          Хм, а если на это взглянуть иначе? Допустим, функции должны быть чистыми — поэтому они обязаны что-то возвращать.


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


          И ещё — получается, из функций нельзя вызывать процедуры?


          1. Zenitchik
            09.01.2019 11:22

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


            1. 0xd34df00d
              09.01.2019 18:08

              Осталось придумать, как это гарантировать на уровне системы типов, и получится что-то вроде монады ST.


              1. vlad9486
                10.01.2019 18:25

                Подумал о лайфтаймах раста, они немного не об этом, но если из того что у тебя есть не торчит аннотация, значит оно ничего не заимствует.


          1. andrey_ssh
            09.01.2019 13:01

            Вот именно.
            function в Паскале — это процедура с возвращаемым значением, а не функция.


        1. andrey_ssh
          09.01.2019 12:57

          Я бы это немного по другому сформулировал:
          Существуют императивные алгоритмы, которые работают за счёт изменения состояния. Их нельзя реализовать «чистыми» процедурами (как и чистыми функциями).


          1. Druu
            09.01.2019 13:05

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


            1. andrey_ssh
              09.01.2019 13:08

              Да, будет. Но это уже другой алгоритм. А идеальный язык должен позволить реализовать оба.


              1. Zenitchik
                09.01.2019 13:17

                Мудро ли это? Может, это должно делаться вставками на другом языке?


              1. Druu
                10.01.2019 09:59

                А идеальный язык должен позволить реализовать оба.

                Если язык чистый, то реализовать сам алгоритм он вам не позволит by design.


      1. andrey_ssh
        09.01.2019 12:52

        В Аде образца 1983 года. Функциям запрещено модифицировать входные параметры, а процедурам можно.
        Позднее выяснилось, что с таким радикальным подходом не выжить в мире написанном на Си (невозможно, например, вызвать WinAPI). Ограничение ослабили и стало почти как в Delphi.


  1. vintage
    08.01.2019 15:56
    +2

    Похоже вы описали чуть улучшенный язык 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.


    1. lair
      08.01.2019 16:46

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

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


      1. vintage
        08.01.2019 22:51
        -1

        Слово Hash в названии вас не смутило?


        1. lair
          08.01.2019 23:02

          Нет, а должно было?


        1. Sirikid
          09.01.2019 07:17

          Вы упускаете то, что в сете нас волнует только присутствие или отсутствие ключа, от которого и вычисляется хэш, а на значение нам плевать. Впрочем, HashMap[Unit, Whatever] вполне себе валидный тип, хотя по смыслу и эквивалентен обычному Maybe[Whatever].


          1. vintage
            09.01.2019 22:20

            Попробуйте реализовать и поймёте, что значение в любом случае будет.


            1. lair
              09.01.2019 22:57

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


  1. devalone
    08.01.2019 17:18

    Антипримером можно назвать С++, где по историческим причинам определение класса раскидывается по паре файлов

    Ну, раскидывание по файлам и вправду неудобно, но разделение определения и объявления — очень удобно, особенно когда читаешь код вне IDE


  1. devalone
    08.01.2019 17:48

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

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

    Ещё ты не рассмотрел очень важный момент — обработка ошибок, тут как минимум несколько вариантов:
    — Как в C++ — хаос
    — Как в Go — функции возвращают ошибки, которые нужно долго и нудно обрабатывать
    — Как в C#, Python — исключения, которые можно забыть обработать(не знать, что библиотека кидала исключения) и оно уронит программу
    — Как в Java — ислючения, которые нужно или обработать или кинуть дальше по иерархии, имхо, самый лучший метод

    И ещё парадигмы программирования: ООП, функциональное, etc, что из этого должен язык поддерживать, а что нет


    1. a-tk
      08.01.2019 20:04

      > — Как в Java — ислючения, которые нужно или обработать или кинуть дальше по иерархии, имхо, самый лучший метод

      … но при этом задолбаться управлением всем этим делом.


      1. devalone
        08.01.2019 22:21

        ну, в Go, имхо, задолбаешься ещё больше, т.к. нужно каждую ошибку обработать, ну или также кинуть дальше, но не просто дописать throws Exception, а if err != nil {
        return err
        }

        Вариант, исключений, как в Python, C++ и C# плох тем, что не знаешь, какое исключение может бросить библиотека(или начать бросать с обновлением версии) где-нибудь через 10 стек фреймов.


        1. a-tk
          09.01.2019 08:27

          Недаром обработка ошибок является одной из самых сложных задач в разработке ПО.


    1. vintage
      08.01.2019 22:55

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

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


  1. taujavarob
    08.01.2019 19:50

    В некоторых случаях нам нужен именно третий тип неизменяемости — например, при чтении объекта из нескольких потоков или при вычислении чего-то, основанного на свойствах полученного объекта. Именно третий тип неизменяемости позволит компилятору проводить какие-то хитрые оптимизации. Пример использования — final поле в java.
    Не нужен final компилятору. Современный компилятор в силах сам определить что значение переменной нигде не меняется.

    Аналогично и const в JavaScript — он не нужен вовсе. От слова — абсолютно не нужен.


    1. 0xd34df00d
      08.01.2019 20:35

      Это нужно не компилятору, это нужно мне, чтобы случайно не менять то, что не нужно.


      1. taujavarob
        09.01.2019 20:54

        Это нужно не компилятору, это нужно мне, чтобы случайно не менять то, что не нужно.
        Автор статьи писал про «тип неизменяемости позволит компилятору проводить какие-то хитрые оптимизации.» — Про вас он ничего не писал.

        Теперь с вами:
        Вот пишите вы const в JavaScript (если вы Java-программист, то вам и в голову не придёт никогда писать final для того, чтобы «случайно не менять то, что не нужно» — автор статьи об этом сожалеет — «вариант с константой [final] более строгий, но он занимает больше места, хуже читается и используется далеко не везде, где мог бы.», — но это так и есть — Java-программист на практике НЕ пишет никогда final для того, чтобы «случайно не менять то, что не нужно»).

        А вот JavaScript-программист пробует писать const в JavaScript — ибо это модно и хайпово.

        Вот пишет JavaScript-программист const, пишет, потом рефакторит, изменяя на let или наоборот let на const — а потом понимает, что он страдает… и всюду пишет let.

        Но есть же и упрямые JavaScript-программисты (они сразу стали JavaScript-программистами, не из мира Java пришли, как я, к примеру) — и они продолжают, стиснув зубы писать const.

        Через месяц другой они возвращаются к своему коду (или коду другого упрямого программиста) и видят:

        const PI = 3.14;

        Хм, что это значит? — думает упрямый JavaScript-программист:

        1) PI может иметь значение только и только равное 3.14 и никак не 3.141 или 3.1415?
        2) PI может иметь любое разумное значение (как 3.14 так и 3.141), но дальше в программе его ни в коем случае нельзя менять?
        3) PI просто нигде дальше не меняется в коде программы, а если хочется его менять, уточняя в процессе расчёта (выполнения программы), то можно и let поставить тогда в этом месте вместо const.

        Само собой никаких коментов нет — какой путь на этой развилке выбрать и ЧТО ДУМАЛ упрямый JavaScript-программист употребляя const?

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

        А если это чужой код?

        «const не нужен». (С)


        1. 0xd34df00d
          10.01.2019 01:21
          +1

          Автор статьи писал про «тип неизменяемости позволит компилятору проводить какие-то хитрые оптимизации.»

          В случае ряда плюсовых либ (например, с CoW или с read/write-локами) const и не-const перегрузки функций могут иметь принципиально разную сложность. И, кстати, принципиально разные контракты.


          А ещё есть, кстати, &&-квалификация функций-членов, там не то что const, там явно move писать надо.


          Вот пишите вы const в JavaScript (если вы Java-программист, то вам и в голову не придёт никогда писать final для того, чтобы «случайно не менять то, что не нужно»

          Ну, я не пишу const в JS и final в Java, потому что я не пишу на JS и Java. Зато в плюсах я очень сильно люблю const и очень щедро его везде расставляю, вплоть до


          const auto pos = std::find_if(vec.begin(), vec.end(), ...);

          хотя казалось бы.


          Это не то же самое, что final в Java? Или я зря это делаю? Или я вообще делаю плохо, и мне нужно резко менять свои привычки?


          Само собой никаких коментов нет — какой путь на этой развилке выбрать и ЧТО ДУМАЛ упрямый JavaScript-программист употребляя const?

          А если там написано просто PI = 3.14, то программист вообще не думает? У него нет никаких вопросов? Нет альтернатив? Вопросы, перечисленные вами, резко теряют смысл?


          Я не понимаю, как большее количество доступной информации может означать больше вопросов.


          Ну и ещё я не знаю, чем отличается const от let в JS, ибо, опять же, не пишу на JS. Надеюсь, дело не в этом.


          А если это чужой код?

          То const auto foo = ... мне говорит сильно больше, чем просто auto foo = ....
          Так же, как find_if или none_of говорит больше, чем for.
          Так же, как говорит const рядом с методом класса.
          Так же, как говорит явно удалённый конструктор копирования, хотя один из членов класса и так бы уже привёл к его удалению.
          Так же, как отсутствие деструктора говорит мне больше, чем ~Foo(); в хедере и Foo::~Foo() {} где-то в .cpp.


          Коммуницируйте ваш интент, как говорится.


  1. vasiliysenin
    08.01.2019 20:13

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

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


  1. ShadowTheAge
    08.01.2019 22:34
    +1

    Где хранить типы объектов?

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


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

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

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

    (Или нужна система как упаковать 2 указателя в одно машинное слово, это в принципе можно сделать в 64 битной системе, но тогда это означает что указатель придется «распаковывать» при каждом доступе)


    1. vintage
      08.01.2019 22:59

      По сравнению с временем доступа к памяти, такт на распаковку — это мелочи.


    1. iig
      08.01.2019 23:34

      Реализовать на уровне процессора операции по работе со структурами указателей. Что-то вроде SIMD.


  1. PsyHaSTe
    09.01.2019 19:43

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


    Удобные лямбды

    раст — чек
    шарп — чек


    Статическая типизация

    раст — чек
    шарп — чек


    Так вот, в идеальном языке должен быть тип Unit, который занимает 0 байт и принимает только одно значение (не важно какое, оно одно, и если у вас есть Unit, то это оно). Этот тип должен быть полноценным типом, и тогда компилятор станет проще, а дизайн языка красивее и логичнее. В идеальном языке можно будет реализовать HashSet<T> как HashMap<T, Unit> и не иметь никакого оверхеда на хранение ненужных объектов.

    Реализовано в rust. Например, сколько места займет массив из тысячи unit'ов.
    Никаких сегфолтов как выше люди описывали нет. Если объект не занимает места, то и массив таких объектов места тоже не занимает.


    Что касается HashSet<T> — то в расте он реалиован как HashMap<T, ()>. И учитывая вышесказанное, компилятор достаточно умен, чтобы не создавать массив под значения, выпилить весь код, который что-то сохраняет/читает из этого массива, и так далее. В итоге, получается реализация не хуже, чем написанная руками.


    раст — чек
    шарп — можно эмулировать через () кортежи, но во-первых экосистема к этому не очень располагает, а во-вторых никаких оптимизаций не будет. +-


    Кортежи

    раст — чек
    шарп — чек


    Enums, Union и Tagged Union

    раст — чек
    шарп — минус


    Константы

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


    раст — чек
    шарп — минус


    Фишечки котлина

    DSL на макросах — раст чек
    Extension методы — растовые тайпклассы, чек


    Call-by-name

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


    call-by-need: map.get_or_else(key, Smth::new())
    call-by-name: map.get_or_else(key, || Smth::new())

    просто, понятно, никакой странной магии. Четко видно, где значение, где лямбда.


    раст — чек
    шарп — map.getOrElse(key, () => new Smth()) чек


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

    если нет наследования — то и не нужно вариантность :)


    раст — минус
    шарп — чек


    Явные неявные преобразования

    подход раста мне нравится больше шарпового: все касты явные. Причем есть truncate-приведение as чисто для примитивных типов, и есть generic-приведение into(), которое может иметь более сложную логику (например, запаниковать вместо обрезания).


    раст — чек
    шарп — чек


    Где хранить типы объектов?

    раст хранит в указателе, с этим можно много удобных штук делать. Например, чуваки таким образом интеропились с С++-кодом, брали указатель на плюсовые данные, а vtable подсовывали свою.


    раст — чек
    шарп — обычный vtable, +-


    Язык должен скрывать от программиста подробности реализации. В С++ при написании шаблонов возникают проблемы, так как T в шаблоне может оказаться каким-нибудь неожиданным типом.

    тут вопрос не в ссылках, а в templates != generics. Если мы компилируем генерик в условиях ограничений на него, мы в виде Т можем использовать что угодно, что удовлетворяет им.


    Вот чем мне не нравится С# — в язык втащили кучу всего, это всё как-то странно сочетается и повышает сложность языка. (Я могу сильно ошибаться в деталях, так как на С# я писал очень давно и только под Unity) Например, там есть поля класса, проперти и методы. 3 сущности!

    Автосвойства сделали сильно позже. По хорошему я давно думал, что в языке оставить можно только их. Но это уже наследие времен, не все решения идеально правильны по прошествии двух десятков лет с момента релиза языка.


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


    раст — чек
    шарп — чек (хотя можете не верить, но я могу обосновать)


    Макросы

    раст — чек
    шарп — я достаточно давно развлекаюсь написанием анализаторов/плагинов для roslyn. Менее удобно, чем языковая поддержка, но достаточно реально. Одна из моих статей как раз про это. +-


    Функции внутри функций

    раст — чек
    шарп — чек


    Substructural type system

    Это тот же пункт, что и константы по сути. Аргументы аналогичны


    Зависимые типы

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


    Сборка

    раст — чек, cargo просто лучший менеджер зависимостей. Позволяет всё, от указания версий (привет, некоторые языки), до патча конкретных зависимостей в глубине дерева.
    шарп — чек, nuget и csproj очень хорошая система. Новый формат проектов наконец-то выглядит по-человечески. Второй по удобству менеджер для меня.




    Чувствуется очень сильное влияние Java на мышление, прямо как паскаль в статье по вашей ссылке :) Так что могу рекомендовать только расширять сознание дальше.


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


    Второй вариант 0 шарп. Там есть не всё, но многое. Учитывая то, что некоторых вещей в нем вы не оцениваете, было бы полезно понять, что же люди в этом находят.


    Ну и третий вариант — haskell, конечно же. Почему? Я думаю, что его изучение добавило бы вам пунктов в статью, потому что вы просто про многие вещи не знаете, а так бы обязательно указали бы как свойства идеального языка. Почти половина пунктов про ЯП так или иначе связана конкретно с ООП, но ведь есть и другие парадигмы. Неправильно считать, что ООП это текущий венец эволюции, и лучшее, до чего смогли додуматься инженеры в 2019 году. Альтернативные парадигмы не просто тупиковые ветви и гиковское развлечение странных ребят, а вполне рабочие лошадки.


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


    1. Akon32
      10.01.2019 16:52

      > Call-by-name
      С этим я просто не согласен. Значение должно быть значением, а не автомагической функцией имени самого себя.

      Call-by-name позволяет создавать новые "конструкции языка", стилистика синтаксиса которых неотличима от "нативных" для языка.
      Например, можно зачем-то ввести until в дополнение к while:


      def until(cond: =>Boolean)(body: =>Any):Unit = while(!cond) body
      var x=5
      until(x<0) {
        println(x)
        x-=1
      }

      Может быть полезно при создании DSL, особенно если нормальных макросов нет.


      Реальный пример — scala.concurrent.Future#apply():


      val f = Future{
         runLongTask()
      }(customExecutionContext)
      f.foreach(processResult)(baseContext)