Дальнейший текст — моя точка зрения. Возможно, она позволит кому-то по-новому взглянуть на дизайн языков программирования или увидеть какие-то преимущества и недостатки конкретных фич. Я не буду лезть в частные подробности типа "в языке должна быть конструкция 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)
Константы
Возможно, это звучит неожиданно, но неизменяемость можно понимать по разному. Я вижу аж четыре степени изменяемости.
- изменяемая переменная
- переменная, которую "мы" не можем менять, но вообще-то она изменяемая (например, в функцию передают контейнер по константной ссылке)
- переменная, которую инициализировали и она больше не изменится.
- константа, которую можно найти прямо во время компиляции.
Разница между пунктами 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)
sshikov
07.01.2019 18:50>имеет много общего со Scala. По крайней мере, я отдаю себе отчёт в этом сходстве)
Раз уж сходство очевидно, то было бы неплохо четко сформулировать заодно и в чем различия. И какую нишу вы планируете занять. Без этого обычно все равно ничего не получается.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
sshikov
07.01.2019 19:42>маленький гибкий язык, чтобы его можно было встраивать куда угодно.
Знаете, у меня вот прямо сейчас скала выступает именно таким языком, причем наравне с груви (они применяются по очереди). Это называется Spark Shell, и как это ни странно, но то подмножество, которое нужно для создания прототипов — оно достаточно маленькое и простое, в том числе как выяснилось для освоения не совсем программистами.
>В скале 2 нет типов-сумм
Мне казалось, что это запланировано в будущей версии (dotty, правда, непонятно, когда она будет).
OldFisher
08.01.2019 15:17+1Lua был придуман и развивается именно как маленький гибкий язык для встраивания. В чём состоит необходимость его потеснить, чего ему настолько не хватает в этой роли? Что нового даст этот «язык мечты», что так нужно Lua и более-менее аналогам, типа Squirrel и прочих?
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), где скобки это не часть синтаксиса вызова, а констрирование кортежа.
Sirikid
09.01.2019 06:07Самое забавное что там и
out
параметры мапятся в кортеж.
C#:bool TryParse(string, out T)
F#:TryParse : string -> (bool, T)
Deosis
09.01.2019 14:12Интеграция работает по особому и ни кортеж, ни частичное применение не работает. Нужно делать обертки.
Такой код не сработает:
let tuple = (x, y)
System.Console.WriteLine tuple
MonkAlex
07.01.2019 19:07+1Очень многое, из того что написано, есть в C#.
Он разок упомянут в статье, но мелко и не конструктивно, поэтому комментировать не буду, ибо не понял проблемы.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;} }
Почему я могу сделать несколько методов с одинаковым именем и разный сигнатурой, и это нормально, а если я для проперти попробую использовать то же самое имя, то нельзя? И это не смотря на то, что "под капотом" геттер и сеттер это всё равно методы. Это ограничение мне кажется совершенно искусственным, как и отдельный синтаксис для определения геттеров и сеттеров.
MonkAlex
07.01.2019 20:19Не смог найти конкретного ответа на ваш вопрос. Допускаю, что в каких то кейсах будет неоднозначное поведение, когда надо например отличить свойство типа Action от метода.
ПС: если свойство будет типаAction<\int>
(как елочки экранировать на хабре?) его можно будет вызывать буквально как метод —obj.Property(123)
, и в этом случае вызов между таким методом и таким свойством дадут неоднозначность.
lair
07.01.2019 21:20а если я для проперти попробую использовать то же самое имя, то нельзя?
Потому что невозможно будет определить, что такое
var q = A.a
.
Это ограничение мне кажется совершенно искусственным
Это пока вы себя не поставите на место разработчика компилятора. Вообще, если читать объяснения, скажем, Липперта, много таких "почему" пропадает.
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(); }
Проблема в том, что усложнился весь язык.
- Проперти считается отдельной сущностью, хотя реализована через методы
- Геттер будет реализован как метод
get_a
, но при этом я не могу в классе определить ни одного метода с именемa
. - метод
get_a()
объявить тоже не получится. (Единственный логичный пункт) - Метод геттера 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'
, который ничего не принимает, поскольку геттер является этим самым методом.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# создали проблемы на пустом месте.
Я не вижу ни одной проблемы, если честно. Свойства, по большому счету, все равно (семантически) не могут называться так же, как методы.
Sirikid
09.01.2019 06:15
Например чтобы писатьМетод геттера get_a() нельзя вызвать как метод.
А зачем?Select(A.a)
, а неSelect(x => x.a)
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
?Sirikid
09.01.2019 17:06+1В Котлине взлетело, правда там доступ к методу/свойству и "взятие ссылки" синтаксически отличаются:
A.get_prop
иA::get_prop
.lair
09.01.2019 20:00А в C# такой синтаксис, и с ним уже ничего не поделаешь.
Ну так а что с expressions?
Sirikid
10.01.2019 04:47Честно говоря, я не разбираюсь в этой теме, но будет интеесно разобраться. Какие проблемы могут возникнуть?
lair
10.01.2019 12:54Когда используется
Expression<Func<A,B>> e = x => x.q
, создается выражение, из которого явно понятно, чтоq
— это свойство. Всякие там милые ORM используют это для создания запросов в БД, опираясь на маппинг этого самого свойства на БД. Если вы сделаете вызов метода, то он будет неотличим от вызова любого другого метода, отследить его до свойства будет нельзя, ORM сломается.Sirikid
10.01.2019 15:15А что если вешать на сгенерированный геттер атрибут, который сигнализировал бы о том, к какому свойству он относится?
lair
10.01.2019 15:29Во-первых, люди немедленно начнут эти атрибуты вешать на другие методы. Во-вторых, это просто усложнение кода, который всем этим анализом ведует.
Ну и да, ввиду того, что нет инстанса, это все равно все невозможно.
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;
lgorSL Автор
08.01.2019 00:40Вы хотите потребовать, чтобы для свойства a указывали явно тип принимающей переменной? int q = A.a? Разработчики будут против, это неудобно.
Указание типа будет требоваться только для разрешения неоднозначности, когда имена проперти и метода совпадают. В обычных случаях ничего не изменится.
А зачем?
Потому что он есть. Кому это мешает?
прекрасный пример с SO:
Это уже больше похоже на некорректную ситуацию. Ну так запрещать надо именно такое, а не все совпадения имён!
Вдобавок, даже из этой ситуации можно было вывернуться, если бы проперти было доступно как метод get_Q(), а в неоднозначности из примера отдавался бы приоритет методу Q.
В языках типа Groovy/Scala проперти играют роль синтаксического сахара, который использовать не обязательно — можно напрямую звать геттеры и сеттеры. С моей точки зрения происходящее в С# выглядит лишним усложнением.
lair
08.01.2019 00:53Указание типа будет требоваться только для разрешения неоднозначности, когда имена проперти и метода совпадают. В обычных случаях ничего не изменится.
Угу. Было свойство
Q
, можно было писатьvar q = x.Q
. Теперь кто-то добавил к классу новый методQ
, и внезапно весь старый код больше не компилируется. Правда, круто?
Потому что он есть.
Я спросил, зачем?
Ну так запрещать надо именно такое, а не все совпадения имён!
Логика для анализа слишком сложная окажется.
если бы проперти было доступно как метод get_Q()
А это был бы мусор в именовании.
а в неоднозначности из примера отдавался бы приоритет методу Q.
А почему методу, а не свойству? А если там
Func<int>
? А если у вас не просто свойство, а индексер?
С моей точки зрения происходящее в С# выглядит лишним усложнением.
Ровно наоборот, в C# все понятно, если не пытаться подходить к этому с точки зрения скалы.
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; ... }
MonkAlex
08.01.2019 00:44А зачем кстати одноименные свойства и методы? Обычно у них разные цели и разные названия, ни разу не сталкивался с такой необходимостью.
a-tk
08.01.2019 16:21Логически связанные сущности могут иметь одинаковые имена, но то, что приведено выше, совсем-совсем непохожее одно на другое, должны иметь разные имена. Ну, если мы не на конкурсе быдлокода, конечно.
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) } }
это уже дефротная группа, но с переопределенной стоимостью занятий
fedorro
07.01.2019 22:09Потому что у свойств получаются «геттеры» с одинаковой сигнатурой и именем.
Методы int Get_A(){} string Get_A(){} тоже объявить не получится.
playermet
08.01.2019 19:42А теперь давайте представим, что у проперти тип не int, а делегат, имеющий сигнатуру совпадающую с одним из методов. И тут уже возникает неразрешимая проблема при попытке сделать obj.a().
ice2heart
07.01.2019 19:35void не зря не равен 0. А то массив void был бы равен 0. Что даст неопределенное поведение.
iig
08.01.2019 11:24void это абстрактное значение, ничему не соответствующие. Отсутствие значения. Древние математики до изобретения понятия нуля тоже видели в этом проблему.
Если очень хочется, не вижу никаких проблем сделать что-то вида
define MAX_INT _OLD_MAXINT — 1
define void _OLD_MAXINT
Откуда тут неопределенность?
ice2heart
08.01.2019 12:01Я не очень чётко выразился, размер не 0 чтоб например масив из void не был равен 0.
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]; ^
Откуда неопределенность?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. но не равен, по той же причине.iig
08.01.2019 13:34Класс может быть равен нулю
Не могу себе представить операцию сравнения класса с константой. Разве что на javascript ;)
Размер экземпляра класса в вашем примере получается 1. Логично, у класса есть конструктор, и его размер !=0.ice2heart
08.01.2019 18:11Конструктор не виляет на размер класа. Он не хранится внутри класса. Внутри класа хранится только vtable. Функции не влияют на размер класса. Это сделано специально, чтоб если сложить экземпляры класса в массив, массив внезапно не стал 0 размера.
iig
08.01.2019 19:10Это сделано специально, чтоб если сложить экземпляры класса в массив, массив внезапно не стал 0 размера.
Вряд ли массив 0 размера чем-то хуже переменной 0 размера. ;)
Простообьектовпеременных 0 размера в языке С(++) нет.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); }
iig
09.01.2019 10:00Забавно ;)
Есть, значит, способ сломать стандартный трюк с sizeof(array)/sizeof(array[0]).
lgorSL Автор
08.01.2019 13:28В С при увеличении указателя на void людя хотят именно перемещаться по байтам в памяти. Если размер void вместо 1 станет 0, то тогда сломается совместимость.
iig
08.01.2019 13:51при увеличении указателя на void людя хотят именно перемещаться по байтам в памяти.
хотятожидают
Тогда логично использовать char*
А то можно представить себе архитектуру, где указатели указывают на машинное слово, а байты — по прежнему 8-битные.
NeoCode
08.01.2019 21:39Ну если размер объекта 0 байт, то объекта не существует в памяти, не так ли? И совершенно логично, что операции перемещения по байтам в памяти для такого объекта бессмысленны. Фактически, объекты и массивы типа void, если их ввести в некий язык программирования, могут существовать только на этапе компиляции, для каких-то целей обобщенного метапрограммирования. В скомпилированной программе никаких следов таких объектов оставаться не должно.
Компилятору известно что объект 0 байт, стало быть он может выдать ошибку при попытке получить адрес такого объекта (или возвратить NULL, не знаю как лучше), и т.д.a-tk
09.01.2019 08:15> или возвратить NULL, не знаю как лучше
Вот этого точно делать не стоит, потому что возвращённый указатель может куда-то каститься и дальше разыменоваться, и бум случится в рантайме.
С другой стороны, даже пустой (в смысле без полей) объект может иметь методы, на которые может захотеться получить указатель, который потом захочется вызвать. И что делать?NeoCode
09.01.2019 09:03Указатель на метод это указатель на функцию, там проблем нет.
А если нужен указатель на объект без полей для передачи его в качестве «this», то он все равно не должен использоваться (полей-то нет) — поэтому компилятор может или выкинуть неявный аргумент this вовсе, или передавать null. Если же есть виртуальные методы, по появляется поле vfptr, значит уже не 0 байт.a-tk
09.01.2019 09:21Но тогда появляются различия между реализациями пустых и не пустых структур, которые надо везде проводить. Проще сделать фиктивное безымянное поле-байт, которое никому не нужно и всё равно будет исключено при оптимизации, и не делать частных случаев.
NeoCode
09.01.2019 16:31Не факт. Например, если я объявляю массив на 1000 таких структур в драгой структуре, у меня внезапно тратится килобайт памяти, хотя структуры пустые. Можно конечно спросить — зачем делать массив из пустых структур, но такое может получиться случайно при метапрограммировании.
Вообще ситуация интересная, нужно думать.
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]
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]
DaneSoul
07.01.2019 23:42В текущем виде то что справа от for x in list_a — это фильтры, что слева — условие обработки отфильтрованного. Может и не очень логично, но запомнив один раз легко потом вычленять в коде и понимать.
В Вашем случае оба типа условий идут подряд, визуально воспринимать тяжелей.
potan
07.01.2019 20:59Вот как раз этот код я бы читабельным не назвал, из-за двойственного использования if — как тернарный оператор в теле и как фильтр в генераторе.
Я много программировал на Scala и Haskell, где генераторы применяются довольно часто. Не всегда это делает код понятнее, иногда явное использование map, filter и flatMap(>>=) более выразительно.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)]
Это аналог моего примера выше с тем же выводом.
НО! Мой первый пример эффективней, так как там исходная последовательность проходится один раз, по ходу прохода идет и фильтрация и преобразование, а в случае фильтра — вначале проход для фильтрации, потом проход для преобразования, иногда это может быть важно!potan
08.01.2019 00:09Если на список создается итератор, то filter/map/flatMap будут столь же эффективны, как и генератор.
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
DaneSoul
08.01.2019 01:22Я был не прав, прохода второй раз не будет, так как filter — это генератор сам по себе, но тем не менее, данный пример примерно в 1,5 раза медленней, в моем комменте выше есть тесты с примерами.
Sirikid
09.01.2019 06:47А у Haskell компилятор умеет в stream fusion, он и сам все операции оптимально соберет.
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
И во всех случаях всё прозрачно. Либо читаем последовательно, либо разворачиваем скобки в порядке вложенности.
a-tk
08.01.2019 16:30from 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# к слову0xd34df00d
08.01.2019 17:17Как-то многословно.
[ x ^ exp | x <- [-2 .. 5] , x `mod` 2 /= 0 , let exp = if x < 0 then 3 else 2 ]
a-tk
08.01.2019 20:00Если убрать явный массив и сделать как у Вас, то с точностью до форматирующих пробельных символов код получается одинаковым по длине — 83...85 символов, в обеих реализациях.
Знаки препинания в Вашем коде разменялись на буквы в моём. Но буквы может прочесть относительно неподготовленный разработчик, то в Вашем коде, имхо, в каждую закорючку вкладывается свой смысл. Я лично только из контекста того, что делает код, понял различие между x < — … и let x =…
Но всё это вкусовщина, конечно.0xd34df00d
08.01.2019 20:35Вкусовщина, конечно, да.
Математику, наверное, такая запись будет понятнее. И в обратную сторону: я вот программист, но
from x in new ...
я понял только тоже из контекста.
Ну и в linq можно, например, добавить ещё один where-блок после вашего
let pow
?a-tk
08.01.2019 20:39Ну, можно new[]{...} заменить на Range(...) (предполагается using static… Enumerable)
Условия добавляются прозрачно: ещё одно where пишете и всё.rssdev10
08.01.2019 20:54(-2..5).select(&:even?).map { |x| x < 0 ? x**3 : x**2 }
56 символов. И читаемость никуда не пропала.
nexmean
10.01.2019 12:37map (\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)
KES777
09.01.2019 15:29Попробовал переписать выражение на perl
@list_b = map{ x<0? x**3 : x**2 } grep { x%2 == 0 } @list_a
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 действия сделать я молчу.
go-prolog
07.01.2019 19:52У вас:
удобный синтаксис
располагающий писать в декларативном стиле и использовать константы.А в чем же удобство, какие преимущества дает декларативная запись? Не императивны ли сами алгоритмы?
potan
07.01.2019 20:48Программы предназначены не для реализации алгоритмов, а для решения бизнес-задач, которые как раз ставятся достаточно декларативно. Кроме того, декларативные программы понятнее и легче формально обосновываются.
rozhik
07.01.2019 19:551. Ну вот, получился typescript ;) (почти) с#
2. Препроцессоры это огромное зло (или по чему я не люблю С++)
3. Вы в основном сосредоточились на синтаксисе но есть многое, что трудно эмулируется
К примеру- замыкания, upvalues, try/catch/, yild, некоторое из функциональных или декларативных языков, прототипное наследование, миксины. Модель работы с памятью, с тредами, с параллельными вычислениями, с распределенными вычислениями, сигналинг, изоляции…
potan
07.01.2019 20:51В C/C++ подход к препроцессору был очень плохо продуман. Но применение макросов в Lisp, Nemerle, OCaml и Rust показывает, что такой подход может быть очень удобным и полезным.
rozhik
08.01.2019 08:52С OCaml и Nemerle не знаком. Но в остальных случаях, по моему мнению, препроцессор — недостаток выразительных средств языка. Даже в случае кондишн-компайлинга можно обойтись и без него.
Забыл где лежит большое письмо создателя делфи сишарпа и тайпскрипта, там где он взвешивает +- препроцессора. Для меня все ++ малоубедительны.Druu
08.01.2019 13:59Но в остальных случаях, по моему мнению, препроцессор — недостаток
Макросы не имеют ничего общего с препроцессорами.
rozhik
08.01.2019 16:30Вы правы! Я «забыл»уточнить, что воюю против текстовых макросов. Против макросов, которые разворачиваются на уровне синтаксического дерева я ничего не имею против.
potan
07.01.2019 20:46+1Генераторы списков — это аналог for в Scala, только менее обобщенное. Основная его фишку не столько замена map и filter, сколько удобное использование flatMap.
Union-типы, как они ожидаются в Dotty, это не алгебраические типы-суммы. A | A тождественно A, а Either[A,A] несет больше информации. На мой взгляд Union-типы — опасная штука, которая будет порождать неожиданные эффекты при обобщенном программировании (например, код обработки ошибок может начать путать код ошибки и корректные данные, если они окажутся одного типа). Но в теории подход интересен.
Применение зависимых типов в императивных языках еще плохо проработано. Если меняется значение переменной, в которой хранился размер массива, что должно произойти с массивом? В ATS пытаются подобные проблемы решить, подружив зависимые типы с линейными, но пока далеко не продвинулись.sashagil
08.01.2019 14:58Заинтересовался, что такое ATS — выглядит, как будто один автор, или кафедра в каком-то китайском университете пилит понемногу, диковато как-то.
0xd34df00d
08.01.2019 15:04На Idris не смотрите тогда, а то вообще дичью покажется.
sashagil
08.01.2019 15:06Почему же, я на Idris смотрю, и дичью он мне не кажется. Вы можете сравнить ATS и Idris?
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, но это дело наживное. Ах, да, ещё подсветки синтаксиса хабрадвижком, чтобы не пользоваться питоновской подсветкой.
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-yieldfor(x<-xs if f(x); y<-ys(x) ...) yield ...
когда-то неожиданно сильно жрали память.
5) Community-фичи: поддержка, документация, обновления, кроссплатформенность, свободные компиляторы и IDE, библиотеки. Недостаточно создать язык, нужно ещё и 100500 доступных в нём библиотек.
michael_vostrikov
07.01.2019 22:30Технически они являются просто методами со специальными именами, и их в любой момент можно переопределить.
Технически, значение должно где-то храниться, в этом и отличие от методов.
Sirikid
09.01.2019 06:56Технически оно может хранится в приватном поле с именем, которое по правилам языка даже нельзя объявлять в коде. Пожалуйста, осмыслите то, о чем писал автор.
michael_vostrikov
09.01.2019 07:17В контексте создания компилятора вы можете не разделять сущность для хранения данных на "Поле" и "Свойство", но не можете убрать ее совсем и использовать для замены только сущность "Метод". Поэтому говорить, что технически она является просто 2 методами, некорректно.
lgorSL Автор
09.01.2019 13:21Можно сделать "приватное поле" и "методы с произвольными модификаторами доступа", потому что в некоторых языках публичные поля практически не используются. "Приватность" поля даёт компилятору простор по порядку расположения полей, а так же можно будет безболезненно переместить поле из объекта, например, во вложенный объект и поменять геттеры/сеттеры на возвращение значения оттуда.
411
07.01.2019 22:33+1Я бы сказал, что «идеальный» язык программирования должен быть минималистичен, но при этом иметь четкий и однозначный синтаксис. Большинство удобных синтаксических штук должны подключаться с помощью расширений. К примеру вам нужно много классных математических функций — подключили расширение и решаете математическую задачу.
Нужна вариантность — подключили и её. Функциональные фишечки — пожалуйста. И т.д.
Возможно что-то нужно будет встроить в язык, но в большинстве задач без этих вещей можно жить и не совсем понятно зачем перегружать ими разработчиков без нужды.
Более того такой подход позволит решать задачи из разных областей, а явное декларирование позволит разработчику, не читая код, понять, какие знания ему нужны для понимания данной программы.
ideological
08.01.2019 00:01Околотемы вопрос, больно не пинайте.
Добавление в Python списков с чётко заданным одним типом, вроде: listint, listfloat, listbool или liststr — помогло бы ускорить язык тогда когда это нужно?worldmind
08.01.2019 13:30В актуальном питоне можно указывать типы, но сам питон их указание просто игнорирует, поэтому указанием ускорить ничего не получиться.
Но для вычислений есть специальные типы списков, например numpy добавляет свой тип Array заточенный под числовые данные.ideological
08.01.2019 13:41Ну я и хотел спросить/предложить, почему в Python не добавят такие списки (в сам язык или в стандартную библиотеку), чтобы даже без numpy можно было, там где это нужно, указать тип всех элементов в списке.
worldmind
08.01.2019 13:46+1Не знаю, но вероятно это не решает никаких проблем за пределами тех задач где всё равно нужен numpy и товарищи, а может потребовать значительной переделки языка и реализации.
Надо искать, может что-то такое уже спрашивали на SO или в python-ideas
Scf
08.01.2019 00:06+4Я всегда хотел несколько фишек из Ada:
- constrained types: например, целое число от 1 до 10
- by-name type equivalence: типы с разными именами но с одинаковым представлением данных (число или структура с одинаковым набором полей) несовместимы. т.е.
type A=Int
иtype B=Int
несовместимы без явного преобразования типов.
iig
08.01.2019 11:31constrained types
Очень нишевая вещь КМК. Что делать, если в runtime попытались, к примеру, вычислить 8 день недели? Ловить exception?
Scf
08.01.2019 11:35+1Да. Лучше же словить исключение и откатить операцию, чем записать в базу или передать в другой сервис мусорные данные?
Главное преимущество таких типов — если параметр функции объявлен с этим типом, то можно быть уверенным, что он находится в нужном диапазоне.
NeoCode
08.01.2019 12:13+1Такие типы легко реализуются с помощью обычных классов в любом языке программирования.
Hazactam
10.01.2019 06:14constrained 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;
Такие типы легко реализуются с помощью обычных классов в любом языке программирования.
Может и так, однако удобнее, когда фишки есть в языке.
NeoCode
08.01.2019 01:18Очень интересная тема! Огромное вам спасибо за нее, и комментарии очень интересные. У меня на хабре практически все статьи так или иначе не эту тему.
Почти со всеми хотелками согласен. Немножко прокомментирую.
Синтаксическая чистота. Глядя на незнакомый код, программист должен четко понимать — где тут операторы, где ключевые слова, где идентификаторы и т.д. То есть в языке должны быть простые правила разделения всех возможных синтаксических сущностей на категории, и по виду сущности сразу должно быть понятно к какой категории она относится. Это же огромное облегчение для syntax highlighter'ов.
Кортежи. Я в свое время написал аж две статьи здесь, на тему кортежей и своих хотелок с ними связанных. Да, кортежи должны однозначно быть частью языка, они должны лежать в основе синтаксиса языка а не прикручиваться снаружи как это зачастую бывает. По сути в любом языке «последовательность чего-то через запятую» — базовый кортеж, и от этого нужно строить весь синтаксис.
tagged unions Штука полезная, поддержка со стороны компилятора должна быть, но хотелось бы, чтобы чистые перечисления и чистые (низкоуровневые) unions остались.
константы — все верно. Вы очень точно сформулировали про виды констант.
Call-by-name семантика Интересны вопросы реализации. Это может быть рантайм — неявно генерируемая лямбда-функция, или compile-time — тогда это шаблонная функция, принимающая фрагмент кода шаблонным параметром.
преобразования да, идея с явным разрешением (или явным запретом) преобразований очень красивая. Для разных программистов и для разных целей может требоваться разный уровень «неявности» преобразований.
рефлексия — может тоже опцией? хотим сгенерировать для класса метаинформацию — добавляем какое-то ключевое слово перед описанием класса, или ставим глобальную прагму. А кому-то может наоборот нужно оставить в бинарнике как можно меньше следов:)
Значения, ссылки, указатели примитивные типы не должны притворяться, они должны быть объектами — но при этом оставаться примитивными типами! Не понимаю почему так не сделать. Ну и по шаблонам в С++ — это концепты, то есть по сути введение нормальной статической типизации в систему шаблонов. Все те гигантские error'ы, которые вылазят, если в шаблон передать не то что ожидается — прелести динамической типизации:)
Минимум сущностей вот здесь не согласен. Поля, методы и свойства — это разные сущности, пускай и будут разными. Делать все поля приватными насильно — не хочу.
Макросы однозначно да. Я писал об этом статью со своим видением, впрочем с тех пор уже кое-что поменялось, да и в дискуссии выяснились некоторые дополнительные факты — в частности, людям нужен универсальный код, который можно выполнить и в runtime и в compile-time.
Функции внутри функций Ну это вообще очевидная вещь. В расширениях GCC она реализована давно, но в стандарте до сих пор нет. Почему?
Substructural type system пока не очень понятно
Сборка однозначно не так как в С++. Во всех следующих языках все сделано гораздо лучше.
0xd34df00d
08.01.2019 02:49+1Итак, вы захотели параметрический полиморфизм, ограничения полиморфизма и сабтайпинг (а почему не row polymorphism, кстати?). У меня к вам всего один вопрос из трёх пунктов.
Будем обозначать тот факт, что T — подтип U, как T <: U. Кроме того, для дженерик-функций, параметризованных параметром X, являющимся сабтайпом U, будем обозначать этот факт как ?X <: U в начале их типа.
Итак, пусть T? <: T? и U? <: U?.
- Состоят ли типы ?X <: T?. X > U? и ?X <: T?. X > U? в отношении сабтайпинга?
- Состоят ли типы ?X <: T?. X > U? и ?X <: T?. X > U? в этом отношении?
- Как насчёт ?X <: T?. U? и ?X <: T?. U??
VaalKIA
08.01.2019 06:37Вот за это я и не люблю математику! :-)
xPomaHx
09.01.2019 14:35Если говорить про идеальный язык то математический далеко не он. Если научился писать код до высшей математики это особенно заметно что там много неудобных нелогичных решений.
0xd34df00d
09.01.2019 18:03Я не уверен, что правильно распарсил.
В математике много неудобных нелогичных решений или в любом произвольном ЯП?
xPomaHx
10.01.2019 17:28-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??
Можно же просто запретить термы, в которых связывается неиспользуемая типовая переменная :)
0xd34df00d
08.01.2019 14:42Иначе-то очевидно не состоят, т.к. для стрелки вариантность аргументов должна быть разной.
Да, не состоит. Это, так сказать, подготовка перед третьим вопросом.
Можно же просто запретить термы, в которых связывается неиспользуемая типовая переменная :)
А кто сказал, что она в U'шках не используется? ;) Я, конечно, немного неаккуратно поставил вопрос, ибо нельзя сказать, что U? <: U?, не описывая, что в контексте при этом X, но мне было лень описывать понятие контекста
и идти в википедию за уникодовым значком для turnstile.
Но вопрос-то таки с подвохом.
lgorSL Автор
08.01.2019 14:18- да, левый — подтип правого.
- нет. (если поменять местами T? и T?, то да)
- Хм. Тут подвох? Я вижу два варианта ответа, но не могу сказать, какой из них лучше:
можно сказать, что выбор типа X не влияет на U, и потому левый будет подтипом. С другой стороны, если предположить, что T? это bottom type, а T? и U? — нет, то левый не сможет быть подтипом правого.
0xd34df00d
08.01.2019 15:03Тут подвох?
Тут всё сложно, на самом деле.
можно сказать, что выбор типа X не влияет на U
Я не говорил, что U не зависит от X, да и для тайпчекера это неважно: сабтайпинг типов справа от точки надо проверять, имея X в контексте (как показывает хотя бы даже первый пример).
С другой стороны, если предположить, что T? это bottom type, а T? и U? — нет, то левый не сможет быть подтипом правого.
Почему?
Хорошая интуиция для дженериков (или, как сказал бы типотеоретик, для System F) — это просто функции из типов в термы?. То есть, функции, которые принимают тип и возвращают терм, использующий этот тип, прям как обычные функции принимают терм и возвращают терм. Тогда возвращаемое значение правой функции — подтип возвращаемого значения левой функции, а принимаемое значение правой функции — надтип принимаемого левой. То есть, вполне логично было бы сказать, что они в этом отношении состоят. Ну или, апеллируя к интуиции, функцию справа можно использовать в любом контексте, где используется функция слева.
Но проблема глубже. Сказать, что они в этом отношении состоят, действительно можно, но если вы разрешаете варьировать ограничения под ?, то можно показать, что тайпчекинг становится неразрешимым по Тьюрингу для любых полезных систем типов. К неразрешимым системам типов хаскелистам (и уж тем более плюсистам, у них и парсинг-то неразрешим) не привыкать, конечно, но всё равно неприятно.
?И в более продвинутых системах типов они даже не отличаются синтаксисом и правилами вывода, вопрос только в сортах в formation rule.
Druu
09.01.2019 07:43Но проблема глубже. Сказать, что они в этом отношении состоят, действительно можно, но если вы разрешаете варьировать ограничения под ?, то можно показать, что тайпчекинг становится неразрешимым по Тьюрингу для любых полезных систем типов. К неразрешимым системам типов хаскелистам (и уж тем более плюсистам, у них и парсинг-то неразрешим) не привыкать, конечно, но всё равно неприятно.
Вобщем-то, если чек подтипирования ограничить по глубине рекурсии, как те же темплейты из плюсов — то найти терм, который эту глубину потом таки пробьет, еще постараться надо :)
a-tk
09.01.2019 08:17> как те же темплейты из плюсов
а можно с этого момента чуть поподробнее?0xd34df00d
09.01.2019 18:05Вы можете написать
template<unsigned int N> struct Foo : Foo<N - 1> {}; Foo<10000> foo;
и компилятор имеет право сделать сильно меньше 2^32 инстанциаций.
0xd34df00d
09.01.2019 18:04Тьюринг-полнота обычно связана с тем, что по каким-то причинам всё начинает тормозить. Поэтому парсинг плюсов принципиально медленный, например.
Но это так, эмпирическое наблюдение.
Druu
10.01.2019 09:54Ну с точки зрения алгоритма тайпчека, полный сабтайпинг от ядерного практически не отличается, проблема как раз именно в том, что он может на специфическом терме повиснуть. Ну и еще у полного вроде с объединениями какие-то проблемы были.
Yermack
08.01.2019 11:36Присмотритесь к языку Julia — его создавали как раз переосмысливая хотелки программистов
AN3333
08.01.2019 13:21+2Вот пишу я 30 лет на С++. В слезах. Периодически поглядываю что нового. Вижу смену модных языков которые меняют как перчатки. Отличаются они набором штучек — чего хочется. Иногда попадаются наборы которые мне лично нравятся. Ну и что? Есть ли причина переходить на какой-то конкретный новый набор?
В свое время причиной смены языка был OOP. Он получил распространение в жутко неудобной реализации — С++. С чем пришлось смириться. Причины для следующей смены языка, я за так и не увидел. (Игры с новенькими наборами не интересны, это молодежные развлечения).
Я бы перешел на аналог С++ с приличным синтаксисом, если бы он имел сравнимую с С++ поддержку. Чего нет. Есть ли еще какая-то причина?NeoCode
08.01.2019 14:21+1Причина — инерционность человеческого мышления.
Ну и порог вхождения самого языка. Это же еще и библиотеки/фреймворки, и среды разработки, и отладчики.
vlad9486
08.01.2019 21:12Игры с новенькими наборами не интересны, это молодежные развлечения
Ну так «курица, или яйцо». Опытные программисты не переходят потому что «молодежные развлечения», а «молодежные развлечения» потому что не переходят опытные.
Что бы потеснить C++ мало быть лучшим, нужно быть намного-много-много лучшим. А пока что-то такое изобретут, опытные и молодежь будут страдать.
PsyHaSTe
09.01.2019 19:04Вот пишу я 30 лет на С++
Я бы перешел на аналог С++ с приличным синтаксисом, если бы он имел сравнимую с С++ поддержку. Чего нет. Есть ли еще какая-то причина?Ну вот через 30 лет современные молодые языки получат сравнимую поддержку (особенно по количеству легаси)
А вообще тут вопрос простой: кто раньше влез в удачный вагон, тот потом и гребет лопатой интересные проекты. Всё как в жизни.
Ну и вопрос инертности мышления, конечно. Людям часто стыдно себе признаться, что есть более удачные варианты, или что нужно выкинуть весь нажитый непосильным трудом опыт как что-то ненужное и начать всё заново. Многих это сильно демотивирует. И чем больше диссонанс инструмента с потенциальными другими, тем явственнее это проявляется. И люди начинают объединяться в группы по интересам, объяснять это "правильным видением" и "пониманием инструментов", разводить псевдофилософию на тему того, что компилятор должен доверять программисту, ведь машина должна слушаться человека...
0xd34df00d
10.01.2019 01:13Людям часто стыдно себе признаться, что есть более удачные варианты, или что нужно выкинуть весь нажитый непосильным трудом опыт как что-то ненужное и начать всё заново.
А самое главное — и выкидывать-то особо ничего не нужно, на самом деле.
Ну вот люблю я темплейтами упарываться — так это ж просто определённый вид DSL с определёнными ограничениями. Оно переносится на раст (судя по тому, что я знаю о расте). Оно, блин, даже на хаскель переносится!
Ну вот люблю я в байтики и SIMD пердолиться — ну так навыки в оптимизации программ будут полезны и в таком же императивном близком к железу языке, как Rust. Особенно если учесть, что у него тот же llvm'ный бекенд.
Ну вот есть у меня полтора десятка лет отладки за плечами, так я лучше и глубже знаю машину, многопоточность, работу с памятью, работу с внешними библиотеками на сях, которые неизбежно будут всегда.
И так можно, на самом деле, долго продолжать.
Ну, да, все стопицот способов инициализировать переменную таки забыть можно будет. Но это я и сам не сказать чтобы прям так уж помню или, тем более, специально заучивал.
Hazactam
10.01.2019 06:26Вот пишу я 30 лет на С++. В слезах. Периодически поглядываю что нового. Вижу смену модных языков которые меняют как перчатки. Отличаются они набором штучек — чего хочется. Иногда попадаются наборы которые мне лично нравятся. Ну и что? Есть ли причина переходить на какой-то конкретный новый набор?
Тоже самое, только Делфи, не так, правда, долго (17-й год). Язык очень нравится. Поглядываю еще что-то. Но так ничего толком и не взлетело. Что бы всё в одном и нативное, без виртуалок.
PhoenixUA
08.01.2019 13:22Powershell:
$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.xomachine
08.01.2019 15:42+2Читаемость получше чем в Python будет
Надеюсь, это был сарказм. Разве нагромождение символов пунктуации читабельнее обычных английских слов с редкими вкраплениями пунктуации?0xd34df00d
08.01.2019 15:44Как только вы изучили эти символы пунктуации — да. Потому что ресурсов на то, чтобы распарсить и отличить
<$>
от<*>
и от прочих идентификаторов в тексте, надо меньше, чемfmap
отliftA2 id
, скажем.
Математики тоже всё время значки выдумывают, нет бы естественным языком писать!
xomachine
08.01.2019 15:49В конце концов, языки программирования пишутся для программистов, а не для парсера.
Конечно это вкусовщина. Я просто привык, что символ пунктуации в тексте программы это своего рода якорь, за который можно уцепиться глазами. Думаю и так понятно, во что превращается текст программы, почти полностью состоящий из таких якорей.0xd34df00d
08.01.2019 16:21Я про свой визуальный парсер. Поддержка кастомных закорючек, да ещё и с задаваемыми пользователем приоритетами и ассоциативностью, скорее усложняет компилятор, а не упрощает его.
Как раз часто используемые абстракции и операции и имеет смысл выражать в виде подобных якорей.
Druu
09.01.2019 07:49Как только вы изучили эти символы пунктуации — да. Потому что ресурсов на то, чтобы распарсить и отличить <$> от <*> и от прочих идентификаторов в тексте, надо меньше, чем fmap от liftA2 id, скажем.
Ну математики сокращают запись за тем, что обычно с выражениями работают. А так, утверждение о том, что ресурсов требуется меньше — не совсем очевидно. Человек слова обычно воспринимает целиком, как единый объект (если это не ребенок, который учится читать), и в этом случае кажется логичным, что проще различить два объекта которые отличаются, например, 3 буквами из 5, чем 1 из 3.
Zenitchik
09.01.2019 11:193 буквами из 5, чем 1 из 3.
Это зависит от того, насколько различны графически те буквы, которые различаются.
В этом смысле спецсимволы предпочтительнее — они изначально более разные, чем буквы.
0xd34df00d
09.01.2019 18:07Ну, прочитать
f <$> c
мне существенно быстрее, чемfmap f c
, и сразу ясно, что там как.
lair
08.01.2019 16:43Разве нагромождение символов пунктуации читабельнее обычных английских слов с редкими вкраплениями пунктуации?
Это зависит от того, к чему вы привыкли. Кому-то удобнее "X, следовательно, Y", кому-то —
X => Y
.
worldmind
08.01.2019 13:33Я не настоящий сварщик, но в начале статьи начало попахивать хаскелом, мне вот кажется что он наиболее близо к идеалу, хотя ничего реального на нём не делал.
lair
08.01.2019 13:59+5мне вот кажется что он наиболее близо к идеалу, хотя ничего реального на нём не делал
Мне кажется, эта фраза лучше всего описывает идеальные языки программирования.
AN3333
08.01.2019 14:21Мне кажется что причина в таких случаях не в языках, а в поддержке. А поддержка зависит не от качества языка. (про хаскел ничего не могу сказать)
lair
08.01.2019 14:39+2С моей точки зрения, поддержка и сообщество — это тоже характеристики языка, пусть и косвенные, которые влияют и на его выбор для использования, и на его "идеальность".
worldmind
08.01.2019 14:56-1Мне думается это проблема с образованием — учат непойми чему аля паскаль, а потом уже трудно перестраиваться, да и времени учить что-то сильно отличающеся мало, вот в итоге и получается относительно малое сообщество.
По идее именно в студенческие годы и время есть и мозги посвежее, чтобы пихать туда разные концепции, а не учить несколько ничем принципиально не отличающихся языков.
Предполагаю, что Python, Haskell, Lisp, Prolog, SQL видимо должны изучаться на хорошем уровне как примеры достаточно разных подходов. Что-то ещё?lair
08.01.2019 16:39а потом уже трудно перестраиваться
Если трудно перестраиваться — значит, учили плохо и не тому. Я вот после десяти с гаком лет C# взял в руки Python — и ничего, жив пока.
Предполагаю, что Python, Haskell, Lisp, Prolog, SQL видимо должны изучаться на хорошем уровне как примеры достаточно разных подходов
Я предполагаю, что это зависит от того, кого и для чего учат. Мне ваша подборка кажется произвольной.
worldmind
08.01.2019 16:44> после… C# взял в руки Python
для меня это языки одного класса, принципиальных отличий нет
> Мне ваша подборка кажется произвольной.
Python — традиционной императивное программирование
Haskell — чистое функциональное программирование
Lisp — не знаю как его категоризировать, но это мутант ещё тот, от остального сильно отличается
Prolog — логическое/предикатное программирование
SQL — декларативное программирование в терминах предметной области, по сути пример декларативного DSLlair
08.01.2019 16:48для меня это языки одного класса, принципиальных отличий нет
Ну так это для вас. А для меня они очень далеки.
Если что, F# я взял в руки намного раньше, и там переход был меньше.
Мне ваша подборка кажется произвольной.
Я и говорю: произвольной. Вы выбрали языки, которые, как вам кажется, представляют определенную область, хотя не факт, что эту область надо представлять ими (или что эта область вообще нуждается в представлении).
BTW, Лисп — это как раз чистое функциональное программирование.
worldmind
08.01.2019 17:08> BTW, Лисп — это как раз чистое функциональное программирование.
изначально да, но как я понял сейчас там каша из всех концепций
0xd34df00d
08.01.2019 17:19Там чистота немного в другом смысле. В хаскеле она в смысле управления эффектами, в лиспе — в смысле нетипизированного лямбда-исчисления (что вещи даже в каком-то смысле противоположные).
Hazactam
10.01.2019 06:46учат непойми чему аля паскаль, а потом уже трудно перестраиваться
Если даже Паскаль плохо даётся, я не знаю что вообще в программировании можно делать? Мне кажется, что с Бейсика/Паскаля можно перейти на любой язык почти сразу.
babylon
10.01.2019 09:04-1f..mind перестраиваться как раз и не учат. Учат соответствовать. И все оценки вытекают из того насколько вы близки или далеки от предмета обучения. Если потом вдруг выясняется, что вас учили не тому, то система оценок, зафиксированная в вашем красном дипломе, девальвируется. И хорошо если вы сами это понимаете и не страдаете от комплексов. Отличники этому подвержены реже. Так как им все равно, что изучать и чему максимально соответствовать. И чем циничнее такой спец, тем лучше.
Перестраивание и возврат при неудаче между тем основа поиска решений даже в тупиковых ситуациях. Поддержка и сообщество это сомнительные критерии идеальности. В основе идеальности лежат идеи. Насколько хороши идеи настолько язык идеален и удобен. Удобство это еще одно требование человека к языку.
Lisp хорош, но вы про JSONNET и SmallTalk забыли. Я бы предложил начать изучение языков со структур данных, AST, КС -грамматик. Допустим, не все понимают, что объект это уже упакованная структура данных и поэтому предлагают нисходящие алгоритмы там где должны быть самосборки и т.д.
Возможность воспроизведения объекта ценнее его уникальности.
От того как ты думаешь, вытекает то как ты решаешь задачи. Подход к решению важнее текущих знаний…
Поворчал немного. Возраст, однакоZenitchik
10.01.2019 15:11f..mind перестраиваться как раз и не учат.
А этому можно научить? Мне казалось, что человек либо может, либо не может. Гены…
0xd34df00d
08.01.2019 15:06Не, это фраза, которая описывает впечатления.
Мне когда-то тоже так казалось, а потом я начал на нём делать что-то реальное, и понял, что идеальных языков не бывает :(
worldmind
08.01.2019 15:34Но бывают же ближе к идеалу? Чем в итоге хаскел не понравился?
0xd34df00d
08.01.2019 15:42Ну, ближе, да, бывают.
Не понравилась неконсистентность языка и слабая выразительность системы типов. Но можно сказать, что это я зажрался.
worldmind
08.01.2019 15:47Ну типы может когда и допилят до зависимых, вроде костыли и сейчас какие-то есть, а в чём выражается неконсистентность?
0xd34df00d
08.01.2019 16:22Они сейчас именно что костыли, причём адовые и неприятные. Когда мне приходится писать действительно завтипизированный код на хаскеле, у меня сводит руки почти так же, как от мысли про написание обычного кода на, не знаю, питоне.
А неконсистентность — тоталити чекера нет и, наверное, не будет никогда, undefined есть, seq ведёт себя так себе, в итоге как логика язык неконсистентен (что сильно уменьшает полезность завтипов), и вообще Hask is not a category.worldmind
08.01.2019 16:27) конечно же я ничего не понял, напишите при случае пост об этом.
0xd34df00d
08.01.2019 17:20А там конструктива нет, не знаю, о чём писать. А статьи вроде той о фрактале плохого дизайна, или какой-нибудь крик души про Go/Python/C++ — это всё у меня очень плохо получается.
worldmind
08.01.2019 17:44Не конструктивом единым, критика тоже нужна: «Почему хаскел не идеальный язык?» — вот мол видится мне в этих местах плохой дизайн, может кто знает почему так? Или знает решения?
Обгалдить конечно могут, этот тут мастера, но шанс на фидбэк зачем упускать?
worldmind
09.01.2019 10:27Хотя может эту тему надо поднимать на буржуйский сайтах — SO, reddit и т.п., всегда есть шанс, что у описанных проблем есть какие-то важные причины и кто-то их знает.
0xd34df00d
09.01.2019 18:07Причины у всего этого исторические: когда хаскель запиливали 30 лет назад, никто не думал о том, что об вот этом всём надо задумываться. Design goals у него другие были.
andrey_ssh
08.01.2019 15:10+1Процедуры и функции — это два сильно разных способа организации кода. И не следует сливать их в один.
То, что в Pascal/Delphi они мало чем отличаются это недоработка Вирта/Хеилсберга (ну или особенность эпохи).
В Аде различие между procedure и function сильнее, но и там достаточно слабое.
В идеальном языке подпрограммы, объявленные как функции, должны контролироваться на чистоту.
iig
08.01.2019 15:33В идеальном языке подпрограммы, объявленные как функции, должны контролироваться на чистоту.
Как и почему? Можно с маленьким примером.0xd34df00d
08.01.2019 15:43+2Скорее интересно, почему только функции должны контролироваться на чистоту. Как их на чистоту контролировать, как раз ясно, чистое ФП давно это показывает.
vintage
08.01.2019 22:24Например, так: https://tour.dlang.org/tour/ru/gems/functional-programming
iig
08.01.2019 23:28Ага, спс.
То есть чистая функция может вызывать внутри только чистые функции. никакого ввода-вывода, выделения памяти…0xd34df00d
09.01.2019 00:54Выделение памяти вполне себе может быть чистым, если вы не можете отличить одно выделение от другого.
VaalKIA
08.01.2019 16:12В чём проявляется более сильное различие в Аде? И почему в идеальном языке процедуры не должны быть чистыми, ведь побочные эффекты, это — зло?
Zenitchik
08.01.2019 16:37+3Потому что процедура — не возвращает значения. Если у неё ещё и побочных действий не будет — она станет совсем не нужна.
VaalKIA
08.01.2019 16:55Вы открыли мне глаза, никогда об этом не задумывался, поскольку в Паскале есть параметры типа var в процедурах и более того, работа с динамической памятью может никак не затрагивать ссылки на эту память (работа с экранным буфером может производиться аналогично), но саму память изменять. Фактически, при наличии ссылок, мы всегда имеем побочный эффект и что с этим делать, не отказываясь от них — не понятно. Ну и у процедур и функций разная сигнатура, что может быть важно для перегрузок операторов.
0xd34df00d
08.01.2019 17:21То есть, раз она не возвращает значение, то она и управление не возвращает?
a-tk
08.01.2019 20:03Нет. Если она не возвращает значения и не имеет побочных эффектов, то с точки зрения вызывающего кода нет разницы, вызывать её или нет
0xd34df00d
08.01.2019 20:35Это вопрос другого порядка и семантики вычислений (но хороший вопрос, бесспорно).
lgorSL Автор
08.01.2019 23:07В идеальном языке подпрограммы, объявленные как функции, должны контролироваться на чистоту.
Потому что процедура — не возвращает значения. Если у неё ещё и побочных действий не будет — она станет совсем не нужна.Хм, а если на это взглянуть иначе? Допустим, функции должны быть чистыми — поэтому они обязаны что-то возвращать.
Что запрещает процедурам (с побочными действиями) тоже иметь возвращаемые значения? Например, процедура пытается сохранить текст в файл и возвращает флаг-результат.
И ещё — получается, из функций нельзя вызывать процедуры?
Zenitchik
09.01.2019 11:22Хм… Теоретически, если внутри функции используются нечистые функции и процедуры, но они работают с памятью только в пределах части стека выше вызова этой функции (которая будет вытолкнута при возврате), то сама функция побочных эффектов иметь не будет, т.е. формально будет чистой.
0xd34df00d
09.01.2019 18:08Осталось придумать, как это гарантировать на уровне системы типов, и получится что-то вроде монады ST.
vlad9486
10.01.2019 18:25Подумал о лайфтаймах раста, они немного не об этом, но если из того что у тебя есть не торчит аннотация, значит оно ничего не заимствует.
andrey_ssh
09.01.2019 13:01Вот именно.
function в Паскале — это процедура с возвращаемым значением, а не функция.
andrey_ssh
09.01.2019 12:57Я бы это немного по другому сформулировал:
Существуют императивные алгоритмы, которые работают за счёт изменения состояния. Их нельзя реализовать «чистыми» процедурами (как и чистыми функциями).Druu
09.01.2019 13:05Тут же есть хитрый прием — вместо того, чтобы программировать сам алгоритм, можно запрограммировать процесс генерации алгоритма (то есть, вместо того, чтобы актуально совершить последовательность чтений с жесткого диска, мы генерируем некое представление этой последовательности чтений, эдакий список задач на чтение). А этот процесс уже будет чистой функцией.
andrey_ssh
09.01.2019 13:08Да, будет. Но это уже другой алгоритм. А идеальный язык должен позволить реализовать оба.
Druu
10.01.2019 09:59А идеальный язык должен позволить реализовать оба.
Если язык чистый, то реализовать сам алгоритм он вам не позволит by design.
andrey_ssh
09.01.2019 12:52В Аде образца 1983 года. Функциям запрещено модифицировать входные параметры, а процедурам можно.
Позднее выяснилось, что с таким радикальным подходом не выжить в мире написанном на Си (невозможно, например, вызвать WinAPI). Ограничение ослабили и стало почти как в Delphi.
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:
- По умолчанию на откуп компилятору.
- Не инлайнить.
- Попытаться заинлайнить, а если не получится — выдать ошибку.
В языке должны быть макросы, которые принимают на вход синтаксическое дерево и преобразуют его.
Этого в D нет, но есть в его идейном последователе — Nim. Однако, в D есть мощная система шаблонов, позволяющая покрыть большую часть потребностей. И возможность генерировать код на этапе компиляции, правда в виде строки, что не очень удобно и годится лишь для мелочей. В принципе, трансформеры аст не так уж сложно реализовать библиотекой, ведь на этапе компиляции можно прочитать исходник, распарсить его, прогнать через трансформер, сериализовать и выполнить. Но видио это никому ещё не было нужно. Синтаксис D не самый простой, но и не то чтобы сильно сложный.
разрешить объявлять внутри функций какие-то локальные функции или классы
Это всё можно, даже импортировать другие модули можно локально.
void main() { { import std.stdio; writeln( "Hello!" ); } writeln( "Hello!" ); // error }
список из не менее чем одного элемента или число, которое делится на 5, но не делится на 3, но я плохо представляю, как подобное можно доказывать в достаточно сложной программе.
Вот у меня тут есть пара примеров:
- Статически ограниченные типы https://run.dlang.io/is/u0P4Mr
- Доказательство "да, я проверил" в рантайме: https://run.dlang.io/is/7qRkdt
Более нативная поддержка этого на уровне языка была бы очень кстати. Особенно не хватает ограничений на диапазоны. Тогда можно было бы ловить переполнения инта на этапе компиляции. А то сейчас компилятор думает, что программист проверит. А программист даже не думает, что у него может быть переполнение.
Сейчас состояние файла лежит на совести программиста, хотя действия с ним (теоретически) можно запихнуть в систему типов и избавиться от части ошибок.
В D для этого есть структура scoped, создающая объект на стеке и обеспечивающая вызов деструктора при выходе из скоупа.
{ auto foo = scoped!Foo(); } // foo is destructed here
Неоднозначные особенности типа наличия/отсутствия сборщика мусора я не рассматривал, потому что в жизни нужны языки и со сборщиком, и без него.
Поэтому в D можно работать как с ним, так и без него. Многие приятности стандартной библиотеки, правда, зависят от GC.
lair
08.01.2019 16:46Пример с HashSet — это особенность данной структуры, что какое-то материальное значение должно быть, чтобы отличать наличие значения от его отсутствия.
Не, не надо никакого значения. В зависимости от того, как вы сделали внутри, в хэш-таблице можно вообще не иметь ячеек с "отсутствием значения", либо же просто держать индекс последнего значения в соответствующем массиве.
vintage
08.01.2019 22:51-1Слово Hash в названии вас не смутило?
Sirikid
09.01.2019 07:17Вы упускаете то, что в сете нас волнует только присутствие или отсутствие ключа, от которого и вычисляется хэш, а на значение нам плевать. Впрочем,
HashMap[Unit, Whatever]
вполне себе валидный тип, хотя по смыслу и эквивалентен обычномуMaybe[Whatever]
.
devalone
08.01.2019 17:18Антипримером можно назвать С++, где по историческим причинам определение класса раскидывается по паре файлов
Ну, раскидывание по файлам и вправду неудобно, но разделение определения и объявления — очень удобно, особенно когда читаешь код вне IDE
devalone
08.01.2019 17:48Неоднозначные особенности типа наличия/отсутствия сборщика мусора я не рассматривал, потому что в жизни нужны языки и со сборщиком, и без него.
Можно иметь оба варианта. Проблема только когда твой проект не использует сборщик мусора, а библиотека использует, они будут работать вместе, но производительность твоего проекта пострадает из-за сборщика. Впрочем, можно описывать в пакетах, какие фичи языка используются.
Ещё ты не рассмотрел очень важный момент — обработка ошибок, тут как минимум несколько вариантов:
— Как в C++ — хаос
— Как в Go — функции возвращают ошибки, которые нужно долго и нудно обрабатывать
— Как в C#, Python — исключения, которые можно забыть обработать(не знать, что библиотека кидала исключения) и оно уронит программу
— Как в Java — ислючения, которые нужно или обработать или кинуть дальше по иерархии, имхо, самый лучший метод
И ещё парадигмы программирования: ООП, функциональное, etc, что из этого должен язык поддерживать, а что нетa-tk
08.01.2019 20:04> — Как в Java — ислючения, которые нужно или обработать или кинуть дальше по иерархии, имхо, самый лучший метод
… но при этом задолбаться управлением всем этим делом.devalone
08.01.2019 22:21ну, в Go, имхо, задолбаешься ещё больше, т.к. нужно каждую ошибку обработать, ну или также кинуть дальше, но не просто дописать throws Exception, а
if err != nil {
return err
}
Вариант, исключений, как в Python, C++ и C# плох тем, что не знаешь, какое исключение может бросить библиотека(или начать бросать с обновлением версии) где-нибудь через 10 стек фреймов.
vintage
08.01.2019 22:55Впрочем, можно описывать в пакетах, какие фичи языка используются.
Лучше реализовать оба варианта, а компилятор выберет подходящую реализацию по сигнатуре.
taujavarob
08.01.2019 19:50В некоторых случаях нам нужен именно третий тип неизменяемости — например, при чтении объекта из нескольких потоков или при вычислении чего-то, основанного на свойствах полученного объекта. Именно третий тип неизменяемости позволит компилятору проводить какие-то хитрые оптимизации. Пример использования — final поле в java.
Не нужен final компилятору. Современный компилятор в силах сам определить что значение переменной нигде не меняется.
Аналогично и const в JavaScript — он не нужен вовсе. От слова — абсолютно не нужен.0xd34df00d
08.01.2019 20:35Это нужно не компилятору, это нужно мне, чтобы случайно не менять то, что не нужно.
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 не нужен». (С)
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
.
Коммуницируйте ваш интент, как говорится.
vasiliysenin
08.01.2019 20:13А ещё идеальный язык программирования должен учитывать идеальную операционную систему, под которой будет работать программа.
А идеальная операционная система должна, разрабатываться с учётом идеальной процессорной архитектуры.
Но по историческим (экономическим) причинам, ранние процессоры были идеальными для своего времени, но технологии (производства электроники) развивались, а новые версии должны были быть обратно совместимыми. И уже становились не идеальными из-за этого.
Чтобы перейти на новый идеальный язык программирования, надо чтобы процессорные технологии сменились на столько, что станет выгоднее создавать новый язык программирования для использования новых возможностей.
ShadowTheAge
08.01.2019 22:34+1Где хранить типы объектов?
Лично мне кажется более интересным третий подход, когда тип (указатель на табличку с методами) хранится прямо в указателе.
Если мы говорим о языке, то где хранятся типы объектов не должно влиять на язык.
Если же мы говорим о деталях реализации, то у такого подхода есть проблемы. Во-первых, указателей на объект больше чем объектов. Во-вторых, указатели на объекты гораздо чаще перемещаются в памяти, а это означает всякие сопутствующие проблемы вроде того что передача указателей в функцию занимает больше регистров и меньше параметров можно передать через регистры и т.п.
Но самая концептуальная проблема в том, что такие указатели (являющиеся по сути структурами) становятся неатомарными в смысле многопоточного доступа. Что фактически ставит крест на «низкоуровневой» многопоточности.
(Или нужна система как упаковать 2 указателя в одно машинное слово, это в принципе можно сделать в 64 битной системе, но тогда это означает что указатель придется «распаковывать» при каждом доступе)iig
08.01.2019 23:34Реализовать на уровне процессора операции по работе со структурами указателей. Что-то вроде SIMD.
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 году. Альтернативные парадигмы не просто тупиковые ветви и гиковское развлечение странных ребят, а вполне рабочие лошадки.
Рекомендую на полгодика забросить джаву/котлин/скалу подальше, и хотя бы на домашних проектах попробовать плотно поработать с каким-нибудь из перечисленных языков.
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)
kiloboot
Новый, интересный zig. Построен на плечах LLVM/LLD. Что удивительно — не поделка, а очень и очень рабочий инструмент.
artemisia_borealis
Любпытно, по крайне мере. Вот здесь написано много интересного, что-то очень заманчиво, что-то неоднозначно.
Но покурить стоит.
TargetSan
Самая большая неоднозначность — парадигма полностью ручного ресурс менеджмента. Т.е. при отсутствии GC они не предоставляют ничего кроме куцего defer.