Как стать автором
Обновить

Комментарии 321

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

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

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

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


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

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

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

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


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


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


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

НЛО прилетело и опубликовало эту надпись здесь

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

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

Вот пример.


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

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

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

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


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

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

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

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


class A {
    public void a(double c){}
    public int a(){return 1;}
}

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

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


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

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


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


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

val a = new A()
a.x(a.x)
val method: String => Unit = a.x(_)

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

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

Не, не работает. Вы не можете сделать 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# создали проблемы на пустом месте.

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

НЛО прилетело и опубликовало эту надпись здесь

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


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


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?

НЛО прилетело и опубликовало эту надпись здесь

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


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

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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


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

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


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

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

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


А зачем?

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


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

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


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


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

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

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


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

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


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

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


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

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


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

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


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

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

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

А нужно запретить такое объявление класса?
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;
  ...
}
Второе. Нужно сделать обращение к проперти и взятие ссылки на метод синтаксически различными.
А зачем кстати одноименные свойства и методы? Обычно у них разные цели и разные названия, ни разу не сталкивался с такой необходимостью.
Логически связанные сущности могут иметь одинаковые имена, но то, что приведено выше, совсем-совсем непохожее одно на другое, должны иметь разные имена. Ну, если мы не на конкурсе быдлокода, конечно.
Недавно именно это понадобилось при написании 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)
    }
}

это уже дефротная группа, но с переопределенной стоимостью занятий
Потому что у свойств получаются «геттеры» с одинаковой сигнатурой и именем.
Методы int Get_A(){} string Get_A(){} тоже объявить не получится.
А теперь давайте представим, что у проперти тип не int, а делегат, имеющий сигнатуру совпадающую с одним из методов. И тут уже возникает неразрешимая проблема при попытке сделать obj.a().
потому что это не несколько методов а перегрузка. а перегрузить property не получится потому что он принимает только один тип (set), который связан с его get
В VB6.0 проперти могло принимать несколько аргументов, при этом аргументы геттера и агрументы сеттера, кроме последнего, должны были совпадать.
Так можно было эмулировать массивы.

Индексеры и сейчас есть в том же .net.

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

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


define MAX_INT _OLD_MAXINT — 1
define void _OLD_MAXINT

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

Я не очень чётко выразился, размер не 0 чтоб например масив из void не был равен 0.
#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];
          ^


Откуда неопределенность?
Эт хорошо что не компилируется.
#include <stdio.h>
class A {
public:
A() = default;
};

int main(){
A a;
printf("Hello World %d\n",sizeof(a));
return 0;
}

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


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

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



Вряд ли массив 0 размера чем-то хуже переменной 0 размера. ;)
Просто обьектов переменных 0 размера в языке С(++) нет.
НЛО прилетело и опубликовало эту надпись здесь
Забавно ;)
Есть, значит, способ сломать стандартный трюк с sizeof(array)/sizeof(array[0]).

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

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

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

Вот этого точно делать не стоит, потому что возвращённый указатель может куда-то каститься и дальше разыменоваться, и бум случится в рантайме.
С другой стороны, даже пустой (в смысле без полей) объект может иметь методы, на которые может захотеться получить указатель, который потом захочется вызвать. И что делать?
Указатель на метод это указатель на функцию, там проблем нет.
А если нужен указатель на объект без полей для передачи его в качестве «this», то он все равно не должен использоваться (полей-то нет) — поэтому компилятор может или выкинуть неявный аргумент this вовсе, или передавать null. Если же есть виртуальные методы, по появляется поле vfptr, значит уже не 0 байт.
Но тогда появляются различия между реализациями пустых и не пустых структур, которые надо везде проводить. Проще сделать фиктивное безымянное поле-байт, которое никому не нужно и всё равно будет исключено при оптимизации, и не делать частных случаев.
Не факт. Например, если я объявляю массив на 1000 таких структур в драгой структуре, у меня внезапно тратится килобайт памяти, хотя структуры пустые. Можно конечно спросить — зачем делать массив из пустых структур, но такое может получиться случайно при метапрограммировании.
Вообще ситуация интересная, нужно думать.
Если бы в питоне можно было объявлять лямбды более коротким способом, хотя бы 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]
НЛО прилетело и опубликовало эту надпись здесь
В текущем виде то что справа от for x in list_a — это фильтры, что слева — условие обработки отфильтрованного. Может и не очень логично, но запомнив один раз легко потом вычленять в коде и понимать.
В Вашем случае оба типа условий идут подряд, визуально воспринимать тяжелей.
Вот как раз этот код я бы читабельным не назвал, из-за двойственного использования if — как тернарный оператор в теле и как фильтр в генераторе.
Я много программировал на Scala и Haskell, где генераторы применяются довольно часто. Не всегда это делает код понятнее, иногда явное использование map, filter и flatMap(>>=) более выразительно.
Так можно и в Python явно использовать filter, причем даже в генераторе списка:
list_c = [x**3 if x < 0 else x**2 for x in filter(lambda x: x % 2 == 0, list_a)]

Это аналог моего примера выше с тем же выводом.
НО! Мой первый пример эффективней, так как там исходная последовательность проходится один раз, по ходу прохода идет и фильтрация и преобразование, а в случае фильтра — вначале проход для фильтрации, потом проход для преобразования, иногда это может быть важно!
Если на список создается итератор, то filter/map/flatMap будут столь же эффективны, как и генератор.
(Коментарий исправлен после проверки гипотезы в коде)
У меня получается самый быстрый вариант на генераторе списка, решение на 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

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

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


Это 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

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

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


Это C# к слову
НЛО прилетело и опубликовало эту надпись здесь
Если убрать явный массив и сделать как у Вас, то с точностью до форматирующих пробельных символов код получается одинаковым по длине — 83...85 символов, в обеих реализациях.
Знаки препинания в Вашем коде разменялись на буквы в моём. Но буквы может прочесть относительно неподготовленный разработчик, то в Вашем коде, имхо, в каждую закорючку вкладывается свой смысл. Я лично только из контекста того, что делает код, понял различие между x < — … и let x =…
Но всё это вкусовщина, конечно.
НЛО прилетело и опубликовало эту надпись здесь
Ну, можно new[]{...} заменить на Range(...) (предполагается using static… Enumerable)
Условия добавляются прозрачно: ещё одно where пишете и всё.
map (\x -> x ^ if x < 0 then 3 else 2) $ filter odd [-2 .. 5]

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


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

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


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

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


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


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 действия сделать я молчу.

В шарпе можно сделать так:


int value = array.Distinct().Single();
Ну это да. Но удобных агрегаторов в питоне вроде я не нашел. Я просто перевел максимально похоже, чтобы было понятно, чего в целом я пытался добиться.
list_a.filter { it % 2 ==0 }.map { if (it < 0) x*x*x else x*x }

У вас:


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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

val a = A(y = b5)

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

Более того такой подход позволит решать задачи из разных областей, а явное декларирование позволит разработчику, не читая код, понять, какие знания ему нужны для понимания данной программы.
Околотемы вопрос, больно не пинайте.
Добавление в Python списков с чётко заданным одним типом, вроде: listint, listfloat, listbool или liststr — помогло бы ускорить язык тогда когда это нужно?
В актуальном питоне можно указывать типы, но сам питон их указание просто игнорирует, поэтому указанием ускорить ничего не получиться.
Но для вычислений есть специальные типы списков, например numpy добавляет свой тип Array заточенный под числовые данные.
Ну я и хотел спросить/предложить, почему в Python не добавят такие списки (в сам язык или в стандартную библиотеку), чтобы даже без numpy можно было, там где это нужно, указать тип всех элементов в списке.
Не знаю, но вероятно это не решает никаких проблем за пределами тех задач где всё равно нужен numpy и товарищи, а может потребовать значительной переделки языка и реализации.
Надо искать, может что-то такое уже спрашивали на SO или в python-ideas

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


Посмотрите в сторону PyPy — они активно работают именно в этом направлении, начиная с того, что сам PyPy пишется на RPython — типизированном подмножестве Python, заканчивая тем, что их JIT-компилятор умеет в векторизацию и как итог numPyPy способна конкурировать с numpy по производительности. Хотя я давненько уже не следил за их прогрессом, не знаю, насколько это "готово в продакшн".

mashedtaters.net/var/language_checklist.php
тут можно сгенерировать сразу готовый ответ по рандому, в большинстве случаев сразу подходит.

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


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

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

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

constrained types

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

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


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

Такие типы легко реализуются с помощью обычных классов в любом языке программирования.
constrained types: например, целое число от 1 до 10

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

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


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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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


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

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

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

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

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

а можно с этого момента чуть поподробнее?
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

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

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

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

Я бы перешел на аналог С++ с приличным синтаксисом, если бы он имел сравнимую с С++ поддержку. Чего нет. Есть ли еще какая-то причина?
Dlang был как претендент, но не взлетел.
Еще был Eiffel и тоже не взлетел. Вот его я хотел попробовать.
Точнее говоря, до сих пор пытается взлететь «с толкача», неся тяжёлые потери в борьбе со сборщиком мусора.
Причина — инерционность человеческого мышления.
Ну и порог вхождения самого языка. Это же еще и библиотеки/фреймворки, и среды разработки, и отладчики.
Игры с новенькими наборами не интересны, это молодежные развлечения
Ну так «курица, или яйцо». Опытные программисты не переходят потому что «молодежные развлечения», а «молодежные развлечения» потому что не переходят опытные.

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

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

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


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


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

НЛО прилетело и опубликовало эту надпись здесь
Вот пишу я 30 лет на С++. В слезах. Периодически поглядываю что нового. Вижу смену модных языков которые меняют как перчатки. Отличаются они набором штучек — чего хочется. Иногда попадаются наборы которые мне лично нравятся. Ну и что? Есть ли причина переходить на какой-то конкретный новый набор?
Тоже самое, только Делфи, не так, правда, долго (17-й год). Язык очень нравится. Поглядываю еще что-то. Но так ничего толком и не взлетело. Что бы всё в одном и нативное, без виртуалок.
Powershell:
$filteredLst = $lst | where y -gt 2
Есть скриптблоки — анонимные функции — аналог лямбда.
Их можно выполнить на месте, а можно передать как параметр функции.
Каринг тоже можно реализовать методом GetNewClosure():
function f ( $x ) {
{
param( $y )
$x + $y
}.GetNewClosure()
}
Set-Item function:GetPlus2 -Value (f 2)
GetPlus2 3


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

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

Конечно это вкусовщина. Я просто привык, что символ пунктуации в тексте программы это своего рода якорь, за который можно уцепиться глазами. Думаю и так понятно, во что превращается текст программы, почти полностью состоящий из таких якорей.
НЛО прилетело и опубликовало эту надпись здесь
Как только вы изучили эти символы пунктуации — да. Потому что ресурсов на то, чтобы распарсить и отличить <$> от <*> и от прочих идентификаторов в тексте, надо меньше, чем fmap от liftA2 id, скажем.

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

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

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

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

О плотной абракадабре, вроде как речи не шло.
НЛО прилетело и опубликовало эту надпись здесь

Выигрыш в читаемости начинается с нескольких операторов:


createCustomer <!> idResult <*> nameResult <*> emailResult
fmap (fmap (apply createCustomer idResult) nameResult) emailResult

createCustomer |> apply idResult |> fmap nameResult |> fmap emailResult


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

НЛО прилетело и опубликовало эту надпись здесь

А что делает здесь fmap?

НЛО прилетело и опубликовало эту надпись здесь

Я к тому, что если функторы с существенно разной семантикой (например maybe vs IO), лучше их явно поименовать.

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь
Ну и замечательно!

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

полиморфизм = неявные касты

Не равно.


Получается из хаскеля джаваскрипт.

Не получается.


Там и обычный-то полиморфизм запрещать стоит

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

Без полиморфизма нет инверсии контроля

Мы вообще-то про параметрический полиморфизм говорили, а не про то, что вы подумали.

НЛО прилетело и опубликовало эту надпись здесь

Например?


Почему нельзя? Берёте интересующий вас инстанс и смотрите, что у него в fmap понаписано.

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


Вот это я сейчас вообще не понял.

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

НЛО прилетело и опубликовало эту надпись здесь
с пачкой инстанс-методов. И функция

Это аналог интерфейса с набором реализаций, инверсия контроля тут откуда возникает?


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

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


А ещё каст неявный, а функции (пусть и из типов в термы) вызываются чуть более явно.

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


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

НЛО прилетело и опубликовало эту надпись здесь
Вызывающий код пихает нужные реализации.

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


Или с другой стороны: как выглядит инверсия контроля в ООП?

ну вот так без инверсии: yoba(Data data) = new Solver(data), а вот так с инверсией: yoba(Data data, ISolver solver) = solver(data).


«Заворачивает создание кастомера в монаду» — достаточно хорошее понимание принципа работы для этого уровня абстракции, как по мне.

Сказать "заворачивание кастомера в монаду" — это то же самое, что ничего не сказать, то есть этот код не дает программисту никакой информации. А если код не дает никакой информации — то он не нужен, его следует удалить.


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

Это у кого так с обычными функциями?
Если по функции непонятно, что она делает, то эта ф-я не нужна, удалите ее.


А это меня как раз не печалит совсем.

А меня печалит, т.к. это делает код бесполезным.


Почему неизвестен? Вот же ж написано прям в сигнатуре: Functor f.

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


Значит, это библиотечный код.

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

НЛО прилетело и опубликовало эту надпись здесь
Зачем? Вон в коде сверху вызывающий код пихает, а экзистенциальных типов нет.

Ну так если рассуждать, в хаскеле сам факт вызова ф-и на инстансе уже DI :)
Штука в том, что DI должен уметь разрешаться в рантайме.


Не соглашусь. Почему это?

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


И я вам любую функцию, использующую экзистенциальный тип, перепишу через полиморфную функцию:

Так экзистенциальные типы это и есть кусок rank-1+ полиморфизма, что тут удивительного?


Я не понимаю ни первую, ни вторую строчку. Это типа определение функции yoba, принимающей data и возвращающей Solver(data)? Или что там происходит?

Тьфу ты, я немного ошибся, конечно. Должно быть:
yoba(Data data) = new Solver().solve(data) — без DI, йоба создает солвер и применяет его к данным
yoba(Data data, ISolver solver) = solver.solve(data) — с инверсией, солвер передается, т.о. йоба теперь не зависит от конкретной реализации солвера.


А в пределе код функции и не должен давать информации, информацию должен давать тип.

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


А это нужный код. На библиотечном уровне, конечно, но нужный.

Ну тогда его следует оставлять на библиотечном уровне и не применять в бизнес-логике абстрактные фмапы :)
Вроде, мы оба с этим согласны?


Но так хоть можно в код не смотреть.

Но нельзя узнать тип, если в код не смотреть :)


Это ограничение на семантику функции.

Но Functor, Applicative, Monad и т.п. вещи не накладывают никаких содержательных ограничений на семантику ф-и! В том-то и дело! Под "заворачивает в функтор" могут подразумеваться практически какие угодно действия. В этом суть. Сказать, что что-то функтор или монада = ничего не сказать в плане того, что конкретно делает код.


Например, там точно не будет вещей вроде fromJust/fromMaybe на Id и прочих параметрах, которые могли бы быть в случае Maybe вместо f.

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


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

Все именно так.


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

Не бывает.

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

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


Ну я тут точно не соглашусь. Почему это кусочек System F похож на сабтайпинг?

Ну вот похож, исторически сложилось. А кусочек ЗТ похож на полиморфные типы. Так уж выходит, что фрагменты одной системы могут быть очень близки к фрагментам другой.


rank-2+, если быть точным

Под 1+ подразумевалось >1


А удивительно то, что вы при этом находите его похожим на сабтайпинг — ну не получается у меня углядеть эту параллель!

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


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

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


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

С-но, оно выбирает определенный элемент массива вполне определенным методом (которые в реализации pivot'a). Чего мы знаем, что делает pivot. Чего мы не знаем без контекста — это где потом используется результат.

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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


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

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


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

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

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

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

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

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

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

А этому можно научить? Мне казалось, что человек либо может, либо не может. Гены…
НЛО прилетело и опубликовало эту надпись здесь
Не уловил мысль. Каким образом трудности тех, кто не умеет перестраиваться, помогут мне избавиться от иллюзии, что это врождённая черта?
НЛО прилетело и опубликовало эту надпись здесь
Но бывают же ближе к идеалу? Чем в итоге хаскел не понравился?
НЛО прилетело и опубликовало эту надпись здесь
Ну типы может когда и допилят до зависимых, вроде костыли и сейчас какие-то есть, а в чём выражается неконсистентность?
НЛО прилетело и опубликовало эту надпись здесь
) конечно же я ничего не понял, напишите при случае пост об этом.
НЛО прилетело и опубликовало эту надпись здесь
Не конструктивом единым, критика тоже нужна: «Почему хаскел не идеальный язык?» — вот мол видится мне в этих местах плохой дизайн, может кто знает почему так? Или знает решения?
Обгалдить конечно могут, этот тут мастера, но шанс на фидбэк зачем упускать?
Хотя может эту тему надо поднимать на буржуйский сайтах — SO, reddit и т.п., всегда есть шанс, что у описанных проблем есть какие-то важные причины и кто-то их знает.
НЛО прилетело и опубликовало эту надпись здесь
понял, что идеальных языков не бывает

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

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

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


Как и почему? Можно с маленьким примером.
Мне тоже интересно.
НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь
А… речь о функциональном программировании.
В чём проявляется более сильное различие в Аде? И почему в идеальном языке процедуры не должны быть чистыми, ведь побочные эффекты, это — зло?
Потому что процедура — не возвращает значения. Если у неё ещё и побочных действий не будет — она станет совсем не нужна.
Вы открыли мне глаза, никогда об этом не задумывался, поскольку в Паскале есть параметры типа var в процедурах и более того, работа с динамической памятью может никак не затрагивать ссылки на эту память (работа с экранным буфером может производиться аналогично), но саму память изменять. Фактически, при наличии ссылок, мы всегда имеем побочный эффект и что с этим делать, не отказываясь от них — не понятно. Ну и у процедур и функций разная сигнатура, что может быть важно для перегрузок операторов.
НЛО прилетело и опубликовало эту надпись здесь
Нет. Если она не возвращает значения и не имеет побочных эффектов, то с точки зрения вызывающего кода нет разницы, вызывать её или нет
НЛО прилетело и опубликовало эту надпись здесь
В идеальном языке подпрограммы, объявленные как функции, должны контролироваться на чистоту.

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

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


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


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

Хм… Теоретически, если внутри функции используются нечистые функции и процедуры, но они работают с памятью только в пределах части стека выше вызова этой функции (которая будет вытолкнута при возврате), то сама функция побочных эффектов иметь не будет, т.е. формально будет чистой.
НЛО прилетело и опубликовало эту надпись здесь
Подумал о лайфтаймах раста, они немного не об этом, но если из того что у тебя есть не торчит аннотация, значит оно ничего не заимствует.
НЛО прилетело и опубликовало эту надпись здесь
Вот именно.
function в Паскале — это процедура с возвращаемым значением, а не функция.
Я бы это немного по другому сформулировал:
Существуют императивные алгоритмы, которые работают за счёт изменения состояния. Их нельзя реализовать «чистыми» процедурами (как и чистыми функциями).

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

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

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

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

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


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

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

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

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


bool[100500] hashset;

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

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


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

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


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

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

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

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

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


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

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


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

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


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

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

    writeln( prefix , res );
}

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

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

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


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

int foo;

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

const int foo;

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


class Foo { Bar bar; }
class Bar {}

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

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

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


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

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

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


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

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


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

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

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

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

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


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

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


struct Foo {}

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

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

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

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


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

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

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

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


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

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

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

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

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


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

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


void foo( int a ) {}

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

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

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


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

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


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

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


long.sizeof.writeln; // 8

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

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


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

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

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


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

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

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


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

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


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

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

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



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


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

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


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

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

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

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

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

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

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

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

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

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

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

Признак «есть ключ» — и есть это значение.

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


Еще раз: HashMap<T, Boolean> означает, что для каждого ключа типа T, занесенного в таблицу, хранится, кроме ключа, еще и булево значение. В "нормальном" хэшсете от T для каждого ключа типа T кроме самого ключа ничего не хранится.

Как без булева значения отличить заполненную ячейку от незаполненной?

Проблема в том, что в HashMap<T, Boolean>, кроме способа отличить заполненную ячейку от незаполненной (даже если для этого используется булевый признак, что не обязательно), будет хранится еще одно булево значение, которое, собственно, значение.


Кажется, здесь нужен пример. Каждый день оракул достает из сумки шарик с числом и бросает его в корзину — либо попадает, либо нет. Числа на шариках не повторяются. Надо сохранить то, было признано число счастливым (попал) или несчастливым (не попал). Типичная задача на hashmap int -> bool, где int — число на шарике, а bool — попал, или нет. Как, по-вашему, должна быть организована хэштаблица, чтобы мы могли отличать ситуации "это число никогда не доставали" от "шарик с этим числом не попал в корзину"?


PS Желательно, конечно, учитывать, что имплементация хэш-таблицы — обобщенная, и ничего не знает ни о типе ключа, ни о типе значения.

То, о чём вы говорите — это уже HashMap<T, Optional<Boolean> >. Но вполне допускаю, что в Яве как и в Яваскрипте Optional добавляется под капотом для всех значений в мапе и создать честный HashMap<T, Boolean > без собственной реализации невозможно.

То, о чём вы говорите — это уже HashMap<T, Optional<Boolean>>.

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


Хэштаблица не обязана содержать все множество значений t-ключа, и нам надо отличать те элементы этого множества, которые она содержит, от всех остальных. В питоне у dict есть has_key (x in y в третьем), в .net у IDictionary и в Java у HashMap есть ContainsKey. Это типовая операция.

С вами как обычно очень "интересно" спорить. Вы не слышите собеседника, а только продавливаете свои привычки. Последняя попытка:


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

Map — операция отображения

Вот только мы говорим не об операции, а о структуре данных. Между ними, конечно, есть связь, но речь шла именно о структуре данных. Более того, более распространенное название этой структуры — hash table, а та, в свою очередь, является одной из возможных реализаций ADT associative array. Так что "операция отображения" тут ни при чем (хотя, повторюсь, можно совершить логический переход между структурой и операцией, просто это избыточно).


Что еще забавнее — изначально речь шла о другой структуре данных, которая называется hash set, и является имплементацией ADT set. И вот тут-то тем более "операция отображения" ни к чему: вы, конечно, можете представлять множество выброшенных чисел как отображение множества всех чисел на множество булевых значений, но это совершенно не обязательно (и, зачастую, избыточно).


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

Давайте сойдёмся на том, что HashTable<A,B> === HashMap< A , Optional<B> >, а HashSet<T> === HashMap< T , bool > и займёмся чем-то более полезным?

Не, не "давайте".


Во-первых, в Java (а именно про нее идет речь в том пассаже, на который вы изначально отвечали) HashMap<A,B> — это обычный HashTable<A,B>, безо всякого Optional (начиная с фразы "The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls" в документации и заканчивая имплементацией; кстати, тоже никакого open addressing, обычный список в ячейке); следовательно, вы под HashMap понимаете что-то свое, и мне остается только догадываться, что это такое, а соглашаться с догадками я не вижу смысла.


А во-вторых, у HashTable и HashSet разные интерфейсы, поэтому одна и та же имплементация не может быть "строго равна" обоим из них: в лучшем случае, одна и та же имплементация может реализовывать оба интерфейса.


Ну и напоследок. Если говорить о вашем понимании HashMap, то что должен вывести вот такой кусок кода:


map = new HashMap<string,bool>()
map["1"] = true;
map["2"] = false;

for key in map
  print(key)
И почему я не удивлён… Статья не о Яве, а об идеальном ЯП и предложении класть в хешмапу нематериальные значения.

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

Ну то есть вы невнимательно прочитали "пример с HashSet": "Например, в Java HashSet<T> реализовано как HashMap<T, Boolean>. Тип boolean — просто заглушка, костыль. Он там не нужен, в HashMap не требуется значение, чтобы сказать, что ключа нет."


либо будут выведены все ключи, включая кучу нулевых.

А что такое "все ключи" в вашем случае?


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

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

А что в вашем посте обозначает "==="?

Эквивалентность. Ну или слева алиас от второго.
Эквивалентность.

Эквивалентность бывает очень разной.


Ну или слева алиас от второго.

Ну вот пример с отсутствием изоморфизма эквивалентность по алиасу так же ломает.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Ага.


lookup :: k -> Map k v -> Maybe v

Даже в этой нотации поведение Map k v соответствует поведение Hashtable k v в ее обыденном понимании (ну вот в википедийном например), без необходимости вводить Map k (Maybe v). Что, собственно, и говорилось выше.

НЛО прилетело и опубликовало эту надпись здесь
Количество бит, необходимое для хранения этого значения — это бинарный логарифм от мощности множества значений. Если во множестве один элемент, то для хранения нужно ровно 0 бит.

Не поделитесь, как вы пришли к этой интересной формуле?

https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D1%82


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

Ссылки на списки.

Ну вот возьмем к примеру Rust. Там HashSet<T> это HashMap<T, ()>. Никаких ссылок на списки там нет.


Откуда они должны взяться?

Там нет ответа на этот вопрос.

Предлагаю ответить прямо, а не отвечать в духе «ищите, там все есть». Очень уж нехороши подобные ответы.
О каки-'‘м’'+'‘х’' восьмибайтных указателях речь?
Предполагаю, что речь идёт о том, что размер [любого] указателя в программах на 64-разрядной платформе — 8 [=64/8] байт.
В данном случае речь идёт об указателях на списки для каждого ключа субхэша (типично субхэш = хэш_ключа % количество_элементов_в_хэштаблице). Ещё их [эти списки] изредка называют списками коллизий.
В общем случае — нет. От модели адресации зависит. На x86 указателем может считаться сегмент+смещение, например. А можно использовать плоскую модель памяти, где сегменты равнозначные.
Антипримером можно назвать С++, где по историческим причинам определение класса раскидывается по паре файлов

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

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

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

И ещё парадигмы программирования: ООП, функциональное, etc, что из этого должен язык поддерживать, а что нет
> — Как в Java — ислючения, которые нужно или обработать или кинуть дальше по иерархии, имхо, самый лучший метод

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

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

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

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

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

Теперь с вами:
Вот пишите вы 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 не нужен». (С)

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

final в Java используют только тогда когда это просит компилятор.
С каждой новой версией компилятора таких просьб всё меньше и меньше.

final в Java не используют никогда в методе класса с целью чтобы «случайно не менять то, что не нужно» — от слова никогда не используют (хотя и могут).

А если там написано просто PI = 3.14, то программист вообще не думает?
«просто» — означает глобальная видимость PI — это не комильфо.
«var PI = 3.14» — означает видимость в пределах всей функции, где PI объявлено.
«let PI = 3.14» — означает видимость в пределах блока {...}.

Если бы «просто» (то есть отсутствие) означало видимость в пределах блока {...} — то никто и никогда бы в Javascript ничего не ставил бы вовсе. Но, поезд ушёл…

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

То const auto foo =… мне говорит сильно больше, чем просто auto foo = ....
Хм, что вам сказало?: const PI = 3.14;

НЛО прилетело и опубликовало эту надпись здесь
Почему? Должна же быть какая-то причина.
Никому из них и в голову не приходит, что он далее, в своём коде метода, изменит то что не нужно менять.

А чего не комильфо, раз уж речь об этом зашла?
IDE ругается. Поэтому не комильфо.

Что PI дальше нигде не меняется (ну и что программист не ленив).
Нет — это означает, что программист не поленился и проверил, что его программа не падает при этом значении. А вот за 3.1 или 3.141 он не ручается. (первый путь, а вы пошли по третьему пути)
Никому из них и в голову не приходит, что он далее, в своём коде метода, изменит то что не нужно менять.

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


IDE ругается.

… а IDE почему ругается? (шаг "потому что так настроили" можно пропустить)

НЛО прилетело и опубликовало эту надпись здесь
0xd34df00d
Я не такой сверхчеловек, я забывчивый, рассеянный и вообще ленивый.
Неиспользования final в методах Java определяется традицией и только традицией.
public int getSalaryForUser(final String name) {
  final int id = server.getId(name);
  final int salary = server.getSalary(id);
  return salary;
}


public int getSalaryForUser(String name) {
  int id = server.getId(name);
  int salary = server.getSalary(id);
  return salary;
}


0xd34df00d
Почему это?
Потому что вы почему-то выбрали третий путь, а я пошёл по первому пути. И пути наши не сошлись.
НЛО прилетело и опубликовало эту надпись здесь
0xd34df00d
Третий путь не отменяет первый путь, это ещё последующее уточнение.
Третий путь НЕ подразумевает вовсе первый путь, а также не подразумевает вовсе и второй — но они же есть (могут быть) — эти "три путя"!

Но вообще это какая-то наркоманская мотивация ставить const.
Ставя const вы никак не выдаёте свои намерения — переменную дальше ЗАПРЕЩЕНО менять или переменная дальше ПРОСТО НЕ меняется.

С тем же успехом можно писать префикс t перед именем.

«let PI = 3.14» — PI написана заглавными. Это традиция. Что она говорит? — что PI — это константа. А уж как и сколько раз ей можно присвоить значение и с какой точностью можно присваивать значение — это неизвестно и, даже применяя const, никак не определяется.

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

final в Java используют только тогда когда это просит компилятор.
С каждой новой версией компилятора таких просьб всё меньше и меньше.

final позволяет писать многопоточный код. Он пишется не только для программиста, а для того чтобы Java Memory Model гарантировала безопасность несинхронизованного многопоточного доступа.


Теперь про оптимизации:
Если я объявлю переменную c final, то копилятор имеет полное право инлайнить её где ему угодно. Если же слова final нет, то в один прекрасный момент с помощью рефлексии, простого присовоения значения или загрузки класса, который захочет поменять поле, оно может поменяться. (Если я рефлексией поменяю final поле, то я сам себе злобный буратино). Это касается практически всех языков, в которых есть виртуальные машины.


Поскольку существует проблема останова, мы не всегда можем сказать, что какой-то код будет исполнен или нет. Нет никаких гарантий, что компилятор в каждом случае сам догадается, что переменная неизменяемая. Нет никаких гарантий, что программист не ошибётся и случайно не напишет где-нибудь в дебрях кода pi = pi / 2. В java новые классы могут подгружаться прямо во время исполнения программы, а так же есть рефлексия, так что доказательство "неизменяемости" становится ещё сложнее, и его надо будет постоянно поддерживать в актуальном состоянии, и потом в один прекрасный момент переменная может стать изменяемой.


Кроме того, jit — очень сложная штука, вот пример: https://habr.com/post/305894/.
Когда я добавляю константность для полей объектов, я даю больше возможностей jit-компилятору для проведения оптимизаций.

Не нужен final компилятору.

А как же initialization safety?
Современный компилятор в силах сам определить что значение переменной нигде не меняется.

А можно пример компилятора, который в силах определить, что переменная может поменяться из соседнего потока?
В расте можно статически запретить переменным определенного типа быть доступными из разных потоков.
impl !Send for Foo {}

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

А потом написали одну маааааленькую лямбду...

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

Эту лямбду мы тоже должны как-то в другой поток передать, а в Rust компилятор это так же может отследить.

И что, компилятор сам их расставляет?
Он контролирует, чтобы значения не безопасные для многопоточного доступа не были доступны из других потоков.
А какое это отношение имеет к исходному вопросу?
Очевидно, что если переменная не может быть передана в другой поток, то она не сможет там и поменяться.
Речь шла о final и о том зачем оно нужно компилятору, нет?
Ну вот смотрите: habr.com/post/280378/#d-2
Тут запускаются две задачи в пуле потоков. Если убрать immutable, то не скомпилируется.
Т. е. все же нужен?
Перечитал тред. Да, извиняюсь, не так вас понял.
retran
А можно пример компилятора, который в силах определить, что переменная может поменяться из соседнего потока?
ок. Перепишем: «Современный компилятор в силах сам определить что значение переменной нигде не меняется в одном потоке. »

С java 8 появилось понятие — effectively final. Применяется оно только к переменным (в том числе аргументам методов). Суть в том, что не смотря на явное отсутствие ключевого слова final, значение переменной не изменяется после инициализации. Другими словами, к такой переменной можно подставить слово final без ошибки компиляции. effectively final переменные могут быть использованы внутри локальных классов (Local Inner Classes), анонимных классов (Anonymous Inner Classes), стримах (Stream API).


public void someMethod(){
    // В примере ниже и a и b - effectively final, тк значения устанавливаютcя однажды:
    int a = 1;
    int b;
    if (a == 2) b = 3;
    else b = 4;
    // с НЕ является effectively final, т.к. значение изменяется
    int c = 10;
    c++;

    Stream.of(1, 2).forEach(s-> System.out.println(s + a)); //Ок
    Stream.of(1, 2).forEach(s-> System.out.println(s + c)); //Ошибка компиляции
}


Javascript — однопоточный. -> «const не нужен». (С)
Речь то про java и final. Про const и js я ничего не писал.

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

Компилятору вообще ничего не нужно. Все эти final/const/… нужны программисту.

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

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

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


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

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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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


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

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


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

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


Так вот, в идеальном языке должен быть тип 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 году. Альтернативные парадигмы не просто тупиковые ветви и гиковское развлечение странных ребят, а вполне рабочие лошадки.


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

> 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)
Call-by-name позволяет создавать новые "конструкции языка", стилистика синтаксиса которых неотличима от "нативных" для языка.

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


macro_rules! until($cond:expr, $body:tt) => {
   while !($cond) {
      $body
   }
}

Я большой фанат идеи expression tree — поддержки в языке типа «дереао выражений» и его преобразования из лямбды.
IQueryable x = getDbQuery();
var result = x.Where(row => row.Id > 1000).ToList();


Тут выражение row => row.Id > 1000 будет доступна в рантайме как структура данных. Код провайдера может преобразовать это в SQL и отправить на сторону сервера.


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

Какой-то саркастический работодатель попросил улучшить Яву, и, хотя, я не президент Индонезии, но, вот что накатал быстренько, мало ли кого заинтересует. Мне хотелось бы вот так:
Общий принцип — максимальное упрощение без потери логического контроля.
1. Требование, чтобы программист явно указывал значения, которые не используются, но возвращаются из вызываемых функций. Это было бы хорошим дополнением к контролю типов, как в Golang.

2. Возможность вернуть более одного аргумента из функции

3. Методы по умолчанию для нулевого объекта, чтобы он мог выступать в качестве экземпляра и называть его «правилом для экземпляров по умолчанию».

4. Запретить модификатор «private» для метода и «public» для поля и сделать автогенерацию сеттеров и геттеров с возможностью перегрузки

5. Автогенерация интерфейсов из всех public методов класса. Типы классов сравниваются по хеш-коду, который будет сгенерирован именами методов, именами аргументов, типами и возвращаемыми значениями, поскольку вероятность совпадения всего этого незначительна и может быть обнаружена во время компиляции с предупреждениями.
Таким образом, нам больше не нужен «интерфейс» как таковой, только класс и «абстрактный» модификатор. По поводу множественного наследования — в пункте 6.

6. Добавление правил слияния для «extends». В результате объект будет наследовать код и реализовывать все родительские типы. Это будет не множественное наследование, а лучше.

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

8. Подметоды как в Паскале.

9. Все методы как полноценные объекты. В результате видимая разница между методами и внутренними классами класса исчезнет. И все это станет таким же, как «final» поле. Больше не понадобятся специальные ссылки на метод, как в Java 8. Ссылка на метод будет означать ссылку на объект по умолчанию из пункта 3 («правило для экземпляров по умолчанию»).
Вызов метода и создание временного объекта без внешних ссылок на него становится идентичным, все это может быть оптимизировано компилятором как простой вызов метода. С другой стороны, создание «метода» с оператором «new» означало бы создание нового экземпляра «класса». Для этого конструктор класса вернет значение. Конструктор класса будет телом метода по умолчанию, который будет создан во время компиляции, в соответствии с пунктом 3 «Правило для экземпляров по умолчанию».
Все похожие методы с различными наборами аргументов будут сгруппированы, и их и их области, с которыми они работают, будет легче разрабатывать и исследовать.

A. Новый оператор для шифрования во время выполнения или сжатия в памяти экземпляров классов ключом доступа.

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

C. Предоставление дополнительной базовой альтернативы синхронизации блокировщиками — самый простой потокобезопасный метод в классе Thread, который отправляет пользовательское сообщение другим потокам, и метод, который может получает пользовательские сообщения из других Thread. Кроме того, в методе «run» объекта Runnable, который передается в Thread, требовать явно вызывать метод, прерывающий поток, и чтобы без этого была ошибка компиляции. Ещё, добавить метод, который вызывает пользовательские события Runnable из других потоков, принимает-отправляет сообщения каждый раз после этого прерывания. Все это принципиально ничего не меняет, но упростит поточно-ориентированную разработку в 90% случаев. Особенно в тех случаях, когда разработка идет на скорую руку, а это большинство реальных случаев.

D. Фактически, граф всех объектов в памяти всегда имеет только примитивные типы, массивы или null во всех своих вершинах. Будет очень полезен базовый инструмент, который может сохранять весь этот граф в определенной текстовой форме. Большие и бинарные массивы могут храниться отдельно, маленькие — закодировать inline. Я бы использовал мою собственную разработку com.mtk.map, которая доступна на gitlab и sourceforge, или что-то еще проще. Такая структура может быть восстановлена ​​в памяти.

E. Полезный инструмент для поиска любых объектов в графе объектов по заданным критериям с учетом модификаторов доступа, по типу простого SQL. Эта задача значительно упрощается с помощью 3… 10 параграфа. Таким инструментом можно не только упростить иерархию объектов, но и организовать различные тесты и контроль допустимых значений.
Это лучше удалить, переписал и опубликовал

Не, могу только принять-не принять комментарий от read-only пользователя, а удалить никак. Я, правда, далеко не сразу оповещение заметил.

Публикации

Истории