Ссылки

Главная фундаментальная особенность Аргентума - это его ссылочная модель данных, обеспечивающая не только безопасность памяти, как в Java-Rust-Go, но и полностью предотвращающая утечки.

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

В Аргентуме нет тернарного оператора ?:, но есть два бинарных - ? и :

Рассмотрим пример:

a = 4;                // `a` имеет тип int и значение 4
b = a < 0 ? -1 : 1;   // `b` имеет тип int и значение 1

На самом деле выражение a ? b : c. в этом примере - это два вложенных друг в друга бинарных оператора. И их можно записать и по отдельности:

x = a < 0 ? -1;
b = x : 1;

Тип переменной x будет optional<int> или в синтаксисе Аргентума: ?int. Остаовимся на семантике этих двух строчек подробнее:

x = a < 0 ? -1; // x будет -1, если a < 0 или "ничего" во всех остальных случаях.
b = x : 1;      // присвоить в b значение x, но если там "ничего", то присвоить 1.

Можно сказать, что оператор `?` производит optional, а `:` потребляет его.
Если они применены вместе, они работают как старый добрый тернарный оператор:

b = (a < 0 ? -1) : 1;

Если результат выражения не нужен, оператор ? работает как if

a < 0 ? log("it's negative");

А пара операторов ?: работает как if...else:

a & 1 == 0
    ? log("it's even")
    : log("it's odd");

Итак Аргентум разделил тернарный оператор на два бинарных. Что это дало?

  • Поддержаны короткие условные выражения без части else, которые возвращают значения. Язык стал ортогональнее.

  • Ничего не усложнено - не добавлены никакие новые синтаксические конструкции.

  • Появилось два конструктора optional-значений:

    • вместо (C++) optional<decltype(x)> maybeX{x}
      можно написать maybeX = true ? x

    • вместо optional<decltype(x)> maybeX{nullopt}
      можно написать maybeX = false ? x
      или maybeX = ?x

  • Распаковка с использованием дефолтного значения тоже стала проще:

    • вместо auto a = maybeX ? *maybeX : 42
      можно написать a = maybeX : 42

  • При отсутствии данных не обязательно использовать дефолтное значение, можно вызвать хендлер проблемной ситуации или удариться в панику: x : terminate()

  • Очень часто возвращая optional результат функции мы пишем
    return condition ? result : nullopt
    в Аргентуме это будет так: condition ? result

  • Мы можем не только комбинировать операторы ? и :. Мы можем комбинировать операторы : между собой.
    Например: user = currentUser : getUserFromProfile() : getDefaultUser() : panic("no user");
    Это короткое выражение попытается получить user-объект из нескольких мест, и вызовет выход из приложения если ничего не получится.

Аргентум поддерживает упрощенный синтаксис для создания optional-типов:

  • Вместо true ? x можно написать +x

  • Вместо false ? x можно написать ?x или ?int

В Аргентуме нет разделения на стейтменты и выражения

В Аргентуме есть оператор {A; B; C}, который исполняет A B C по очереди и возвращает результат C. Обратите внимание на отсутствие точки с запятой ";" после после C. Если она там будет, это будет означать, что в конце всего блока {} есть еще один пустой оператор, и его результат (void) станет результатом всего блока {}.
Блок - может группировать несколько операторов:

{
   log("hello from inner block");
   log("hello from inner block again");
};
log("hello from outer block");

Блок позволяет создавать локальные переменные:

{
   a ="Hello";
   log(a);
}
// здесь уже нет `a`

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

x = {
    a = 3;
    a += myFn(a);
    a / 5          // это выражение станет значением `x`
};

(Еще блок может быть таргетом для оператора break/return, но это тема отдельного поста).

Совместное использование блоков и оператора "?" позволяет создавать условные выражения, аналогичные оператору if..else, которого в Аргентуме, кстати, тоже нет:

a < 0 ? {
    log("negative");
    handleNegative(a);
};

// или
a < 0 ? {
    handleNegative(a);
} : {
    handleOtherValues(a);
}

В Аргентуме нет bool

Что такое значение типа optional<T>? Это или сам T или особое значение nothing, не совпадающие ни с каким значением T. Теперь попробуем новый трюк: что такое optional<void>? Это однобитное значение, позволяющее различить nothing и void. Это полный синоним логического типа данных. Получается, что если в нашем языке есть тип void и есть и optional<T>, то отдельный тип bool нам уже не нужен. Все логические операции, например операции сравнения, в Аргенуме возвращают optional<T>. А все операции принимающие логические значения теперь будут принимать любые разновидности optional<T>.

Что это нам дает?

Оператор ? на самом деле имеет тип: (?T) ? (T->X) -> (?X)

  • Cлева не только bool (который на самом деле ?void) но и вообще любой ?T

  • Cправа - выражение с результатом X (как вариант, превращающее T в X)

  • Результат самого оператора ? будет ?X.

Как работает оператор "?"

  • Вычисляется/исполняется левый операнд и анализируется его результат:

    • Если "nothing", результатом всего оператора ? становится "nothing" типа ?X.

    • Иначе

      • Создается специальная временная переменная `_` типа T, которая получает значение извлеченное из ?T.

      • Исполняется правый операнд, которое может использовать переменную "_".

      • Результат правого операнда должен иметь тип X, он упаковывется в ?X и становится результатом всего оператора ?.

Такая конструкция оператора "?" позовляет проверять условия с одновременным излечением завернутого в optional значением и передавать результаты дальше, объединяя в конвейер цепочки операций, вызовов методов, доступов к полям в безопасной контролируемой манере:

currentOrderId ? orders.findOrder(_) ? _.getPrice() ? processPrice(_);

В этом примере:

  • Если currentOrderId существует, найти по нему order.

  • Если order нашелся, взять из него price.

  • Если price есть, обработать его.

Если в языке отсутствует такой синтаксис, это простое выражение превращается в многострочную трудно сопровождаемую простыню `if`-ов и временных переменных.

Кстати о безопасности на уровне синтаксиса, и C++ и Java позволяют обратиться к значению внутри optional без проверки на его существование:

// С++
optional<int> x;
cout << *x;

// Java
var x = Optional<Integer>.empty();
System.out.println(x.get());

А в Аргентуме оператор "?" откроет вам доступ к внутреннему значению только при его наличии, а оператор ":" потребует указать код, который предоставит значение вместо отсутствующего. Других операторов для доступа к optional нет. Это непробиваемая защита от обращений к несуществующим значениям на уровне синтаксиса языка на этапе компиляции.

fn printOpt(x ?int) {
    // мы не можем обратиться к внутреннему числу без проверки
	x ? log(toString(_));

    // другой способ обратиться - предоставив значение по умолчанию
    log(toString(x : -1));

    // еще одни вариант - условная конверсия ?int в ?String
    // и значение по умолчанию уже для строки
    log(x ? toString(_) : "none");
}

Кстати, в Аргентуме есть ключевые слова bool, true, false. Они декларируют тип ?void, и создают значения +void и ?void, чтобы все было просто и привычно.

В Аргентуме нет null pointer но есть optionals

В последнее время стало модно добавлять в разные языки Null safety.  В Аргентуме Null safety обеспечивается не добавлением новых понятий, а убиранием ненужных. Указатели в Аргентуме не бывают nullable. Если нужен nullable-указатель, используется optional-обертка над указателем, где optional nothing - это аналог null:

// `a` - это не-nullable указатель, проинициализированный
// свежесконструированным экземпляром Point
a = Point;

// `b` это optional указатель на Point,
// проинициализированный значением nothing.
b = ?Point;

b := a;      // Теперь `b` показывает туда же куда `a`
b := Point;  // Теперь `b` показывает на собственный свежесозданный экземпляр класса
b := ?Point; // Теперь b снова optional-none.

Кстати, синтаксис ?T для создания пустых указателей означает, что в Аргентуме все "null pointers" строго типизированные.

В Аргентуме тип optional глубоко встроен в язык. Для разных обертываемых типов его внутреннее представление различается. Например, optional-указатели на самом деле хранятся как простые указатели, и optional-nothing кодируется в них как 0. Это обеспечивает бесплатный маршалинг в други языки через FFI, компактность внутреннего представления и высокую скорость работы. ТипыObject и ?Object с точки зрения языка различаются только на стадии компиляции - для первого не нужны проверки на null, для второго наоборот запрещается обращение без проверки.

Аналогичный прием используется для ?double, в котором optional nothing - это просто NaN.

Объявление переменных в If

Еще одна модная тенденция обвешивать условный оператор какими-нибудь вычислениями в условии, результат которых сохраняется в локальной переменной доступной в ветках условий, например в C++:

if (auto v = expression()) use(v);

Чтобы это работало, достаточно, чтобы v приводился к bool. Однако очень быстро выяснилось, что приводить один и тот же тип к логическому типу можно по разному. Например для строки иногда полезно считать как бы false пустую строку, иногда строку с текстовым значением "false", иногда к этому можно добавить строку "0" или еще как-то. Поэтому в C++17 появился вот такой удобный вариант if

if (auto v = expression; predicate(v)) use(v);

Как это работает? Вначале вычисляется expression его результат помещается в локальную переменную v, потом она передается в predicate который на ее основе делает bool, которые выбирает нужную ветку в которой эта v доступна. Например:

if (auto i = myMap.find(name); i != myMap.end()) use(*i);

Вот как это делается в Аргентуме без никакого дополнительного синтаксиса:

predicate(expression) ? use(_)

Где: expression по-прежнему отдает значение T, predicate анализирует его и превращает в ?T со значением внутри, a оператор ? при удачном решении предиката передает распакованное из `optional` значение T в use. Пример:

isAppropriate(getUserName(userId))
   ? log(_)
   : log("username is so @#$ing @#$it that I can't even say it");

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

Иногда переменная "_" неудобна или занята

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

profuceSomeConditionalData() ?=data {
     data.method();  // Используем имя `data` вместо "_"
}

Рассмотрим менее абстрактный например:

fn applyStyleToLastParagraph(text TextBlock, styleName String) {
	text.getLastParagraph() ?=last
	text.getDocument().styles.findByName(styleName) ?=style
		last.forEach((span){
            span.applyStyle(style)
        });
}

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

Optionals и приведение типов

Оператор expression ~ ClassOrInterface выполняет два вида приведения типов - если класс является базовым для выражения, то результат такого приведения гарантирован, и операция имеет тип ClassOrInterface во всех остальных случаях операция выполняет быструю рантайм проверку типа, и результат операции будет optional:?ClassOrInterface. В отличие от кастов в Java и C++ где проверка результата каста является необязательной (и очень многословной), в Аргентуме синтаксически невозможно обратиться к зачению завернутому в optional-тип, и поэтому приложение на Аргентуме просто не может упасть из-за ошибочных типов:

pointerExpression() ~ MyClass ? _.myClassmethod() : handleIfNot();

Optionals и weak pointers

В аргентуме ассоциативные ссылки (так же известные как weak pointers) - это один из трех базовых встроенных в язык типов указателей. Такие ссылки легко создаются, копируются передаются и хранятся, они nullable сами по себе:

class MyClass {
    field = &MyClass;   // поле класса; weak-ссылка; "null"
}
a = &MyClass;           // локальная переменная; weak; "null"
realObject = MyClass;   // временная ссылка на свеже-созданный объект класса

a := &realObject;       // теперь `a` ссылается на наш объект
realObject.field := a;  // теперь поле объекта ссылается на него самого

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

Вышеописанное разыменование не имеет никакого синтаксиса. Оно выполняется автоматически везде, где &T преобразуется в ?T. Например, в опреаторе "?":

fn doSomething(obj &Object) {
  obj ? _.something();
}

Оператор ? хочет получить слева ?T, поэтому weak-pointer obj будет локнут, и при успехе передан в правый операнд в виде имени "_".

В результате:

  • в Аргентуме невозможно обратиться по weak-ссылке, которая потерялась

  • эта проверка имеет супер легковесный синтаксис

  • она порождает временное значение ("_" или имя определенное программистом через ?=name), и это временное имя имеет время жизни органиченное правым операндом оператора?.

Optionals и индексы/ключи контейнеров

Все стандартные контейнеры  в результате операции индексации x[i] возвращают ?T. Поэтому Аргентум делает невозможным обращение за пределы массива или по не валидному ключу.

a = Array(String);
a.append("Hello");
a[0] ? log(_);   // вывести нулевой элемент массива, если он есть
log(a[0] : "");  // вывести нулевой элемент массива, или пустую строку

// Функция принимает контейнер, ключ и лямбду, создающую новые элементы
fn getOrCreate(m MyMap, key int, factory ()@Object) {
    // если контейнер содержит элемент, это и будет результат.
    // иначе вызовется лямбда,
    // ее результат сохранится в контейнере по ключу и вернется в виде результата
	m[key] : m[key] := factory()
}

Кстати, операция индексации - это сахар для вызова метода getAt | setAt. Определяя такой метод (с произвольным количеством параметров произвольных типов) вы превращаете свой класс в многомерный контейнер.

Вложенные Optionals, &&, ||

Optional-обертка может содержать в себе любой тип, включая optional. Теоретически это позволяет иметь типы ?int, ??int, ???int и т.д.

Зачем это нужно?

Например, при индексации контейнера с bool или optional- элементами нужно как-то различать отсутствие элемента (выход за границу массива) и элемент со значением nothing.

Или при использовании вложенных ?-операторов. Результат будет зависеть от того, какое из условий не сработало.

Рассмотрим пример:

token = name ? findUserByName(_) ? getUserToken(_);

// Ассоциативность операторов правая. Поэтому этот пример можно записать так:
token = name ? (findUserByName(_) ? getUserToken(_));

Предположим, что getUserToken возвращает значение ?String, которое будет или токеном, или nothing, если у этого user-a нет токена.

Тогда правый самый внутренний оператор ? вернет ??String, который буден nothing, если нет user-a, just(nothing) если нет токена, и just(just(string)) если есть токен.

Тогда левый оператор ? будет иметь тип ???String который буден nothing, если нет name, и все сорта just(...) сигнализирующие о проблемах с поиском user-a и его токена. Получившийся пакет из optional-значений можно проанализировать  тремя операторами ":"

log(token : "No name" : "No user" : "No token)

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

Он работает совсем как maybe >>= Хаскеля. Его левый операнд возвращает ?T, а правый - преобразует T в ?X. Результат оператора - ?X. Вначале он исполняет левый операнд:

  • если он nothing, результат становится nothing типа ?X.

  • иначе он связывает внутренне значение из ?T с переменной "_" и исполняет правый операнд, результат которого и становится результатом всего оператора &&.

По сравнению с оператором ? оператор && имеет всего одно отличие - он не упаковывает результат правого операнда в optional, вместо этого он требует, чтобы он сразу был optional.

Если подставить bool (optional void) вместо ?T и ?X в оператор && он становится полностью идентичным оператору && во всех Си-подобных языках.

Естественно, как и оператор ?, оператор && имеет форму &&=name, для задания имени вместо "_".

Перепишем пример выше:

token = name && findUserByName(_) && getUserToken(_);

Теперь token имеет тип ?String. В нем пропала вся информация о том, почему токен не удалось получить. Теперь это или "нет токена" или значение токена.

Иногда вложенность optional-оберток полезна, иногда - нет, поэтому и оператор ? и оператор && найдут себе применение.

Последний из не рассмотренных операторов - "||". Он похож на ":". Его единственное отличие - он требует, чтобы и справа и слева был один и тот же optional тип. Если оператор ":" возвращает свой левый операнд распакованным из optional-a, то "||" этого не делает. И в этом он также аналогичен оператору || изо всех Си-подобных языков.

Примеры использования:

x = a < 0 || a > 100 ? -1 : 1;

myConfig = getFromFile() || getFromServer() || getDefaultConfig() : terminate();

Итоги

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

Аргентум не вводит ни одной новой конструкции, наоборот, убираются дублирующие.

Аргентум упрощает и инфорсит проверки на null, потерю weak, результатов приведения типов, индексов и ключей контейнеров с тем, чтобы эти проверки были естественной частью бизнес-логики, а не обработчиков исключений.

Внутреннее представление optional-типов бесплатно для всех указателей, и большинства типов значений.

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


  1. samsergey
    20.08.2023 23:39
    +1

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


  1. Panzerschrek
    20.08.2023 23:39

    Интересный подход с bool как optional. Весьма необычно.

    С тернарным оператором (которого нету) немножко страшновато - а не получится ли так, что привыкшие к C++ программисты по-ошибке его заиспользуют и получат не то, что хотели?

    На счёт невозможности развернуть optional без проверки - а что, если прямо нужно безусловно развернуть optional? Есть что-то вроде unwrap из Rust (ясно дело с panic!, если optional пуст)?


    1. kotan-11 Автор
      20.08.2023 23:39

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

      Если A?B:C применить по-старому, он и работать будет по-старому.

      Есть что-то вроде unwrap из Rust ?

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

      x = myOptVar : terminate();


  1. qw1
    20.08.2023 23:39
    +1

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


  1. sashagil
    20.08.2023 23:39
    +1

    Интересно - однако, я не понял, как ":" поступает с типами операндов и своего результата. В примерах либо слева и справа тот же тип (целое, строка) или (более интересный второй случай) справа - вызов функции, возвращающей void, и затем ";". Второй случай намекает, что тип правого параметра диктует тип результа. Первый случай намекает, что тип левого параметра - тоже в деле. Хорошо, а если справа - целое, а слева - строка? Скорее всего, произойдет ошибка приведения типов ( хотя, можно и пофантозировать, что ошибки не будет, а типом результата будет тип-сумма целого и строки, int | string, как можно делать на TypeScript-е). А если справа и слева - пользовательские типы в каких-то отношениях subtype / supertype?


    1. kotan-11 Автор
      20.08.2023 23:39

      Оператор ":" всегда имеет тип (?T) : (T) -> (T). Например, если T=void, то тип будет bool : void ->void, как у else-ветки в Cи-подобных языках.

      Он просто распаковывет LHS, и возвращает. Если вернуть нечего, исполняет RHS и возвращает его.


      1. qw1
        20.08.2023 23:39

        Могу ли я написать
        cat : defaultAnimal
        Если cat — объект класса Cat, наследника Animal, а defaultAnimal — класса Animal.


        И есть ли у всех типов общий предок (как Object в C#).
        Например,
        a > 0 : 42
        может ли вернуть Object, в который упакован либо true, либо 42?


  1. spidersarecute
    20.08.2023 23:39

    Интересные идеи, но хотел бы добавить несколько замечаний.

    1) Мне не нравится синтаксис блока-выражения:

    x = {

    a = 3;

    a += myFn(a);

    a / 5 // это выражение станет значением `x`

    };

    Пробема здесь, как мне кажется, в том, что x=.. и a/5 очень далеко разнесены друг от друга. Это снижает читабельность. Вместо короткого оператора присваивания мы имеем длинную многострочную конструкцию. А если там будет не 3, а 20 строк и еще несколько вложенных блоков?

    2) Справа от оператора ? с помощью символа _ , видимо, создается выражение для преобразования содержимого переменной. То есть, это выражение работает только в операторе ? . Но почему бы не сделать эту конструкцию более универсальной и применимой везде, чтобы символ _ создавал анонимную функцию, [как это сделано в Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Shorthand-Argument-Names) с символами $0, $1 и тд ?

    3) В языке есть функционал для обработки optional значений по типу

    y = x ? _ + 1

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

    Или у нас есть список чисел, а хотелось бы получить список увеличенных на 1 чисел.

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

    Или вот еще один пример: у нас есть Promise, возвращающий User. Мы бы хотели из него получить промиз, возвращающий Address, а из адреса - Promise, возвращающий улицу. Было бы хорошо, если бы была возможность сделать это кратко и логично, вроде:

    street_promise = user_promise->address->street

    4) Мне кажется, этот синтаксис неудачный:

    Если результат выражения не нужен, оператор ? работает как if

    a < 0 ? log("it's negative");

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

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

    5) синтаксис для создания nothing, ?Point, мне кажется немного неожиданным, так как выглядит, как будто мы создаем Point, а на деле создаем пустоту.

    6) На мой взгляд, этот синтаксис не удобный, а ужасный, запутанный и плохо читаемый (в одном операторе смешано присваивание и проверка):

    Поэтому в C++17 появился вот такой удобный вариант if

    if (auto v = expression; predicate(v)) use(v);

    7) Здесь у вас зачем-то используется 2 разных синтаксиса для создания анонимной функции:

    text.getDocument().styles.findByName(styleName) ?=style

    last.forEach((span){

    span.applyStyle(style) });


    1. kotan-11 Автор
      20.08.2023 23:39

      1) Мне не нравится синтаксис блока-выражения:

      x = {

      a = 3;

      a += myFn(a);

      a / 5 // это выражение станет значением `x`

      };

      Пробема здесь, как мне кажется, в том, что x=.. и a/5 очень далеко разнесены друг от друга. Это снижает читабельность. Вместо короткого оператора присваивания мы имеем длинную многострочную конструкцию. А если там будет не 3, а 20 строк и еще несколько вложенных блоков?

      x=.. и a/5 разнесены друг от друга не дальше чем оператор return от заголовка функции. Будем запрещать локальные функции?


    1. kotan-11 Автор
      20.08.2023 23:39
      +1

      2) Справа от оператора ? с помощью символа _ , видимо, создается выражение для преобразования содержимого переменной. То есть, это выражение работает только в операторе ? . Но почему бы не сделать эту конструкцию более универсальной и применимой везде, чтобы символ _ создавал анонимную функцию, [как это сделано в Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Shorthand-Argument-Names) с символами $0$1 и тд ?

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

      В Аргентуме есть анонимные функции с именованными параметрами: (parameters){body}, возможно имеет смысл добавить облегченный стрелочный синтаксис или \-синтаксис.


      1. spidersarecute
        20.08.2023 23:39

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

        1) короткая анонимная функция с подчеркиванием: x ? _ + 1

        2) x ?=data { data.something() } - на мой взгляд, это ?=data очень страшно выглядит, страшнее чем C++. Выглядит, как будто письмо неправильно декодировалось.

        3) анонимная функция в forEach: last.forEach((span){ span.something() })

        Ну посмотрите сами, три (!) синтаксиса анонимных функций, (причем второй просто страшилище). Разве не лучше было бы сократить все до 2 вариантов:

        • для коротких выражений - как в Swift x ? $0 + 1 или ваш вариант с подчеркиванием x ? _ + 1 (выражение с подчеркиванием создает анонимную функцию, а оператор ? применяет её к содержимомуx). Пример использования короткой функции с forEach: forEach(print(_))

        • для длинных - анонимная функция со скобками - x ? (data) { data.something() } или как в Swift x ? { data in data.something() }

        Ну то есть, синтаксис по тяжести и запутанности ближе к C++, а не к простоте Питона, например.


        1. qw1
          20.08.2023 23:39

          1) тут нет анонимной функции, это просто синтаксический сахарок.


          2) Да уж, выглядит как quoted-printable ))) А варианты?


          Идеально — со скобочками
          x ?(data) { data.something(); }


          но круглые скобки уже заняты под выражение, вдруг мы вообще не хотим никаких функций
          x ? (2+3)*4 : (7+8)*9


          Неплохо выглядит "больше", как знак перенаправления


          x ?>data { data+1 }


          3) А тут уже полноценная функция, которую передаём как аргумент в forEach, не надо путать с первым кейсом.


        1. kotan-11 Автор
          20.08.2023 23:39

          Можно конечно спроектировать эзотерический язык, в котором всякий операнд любой операции будет лямбдой, но это не практично и ведет к долгой компиляции. Поэтому в Аргентуме лямбда - это то, что программист будет вызывать, не в том же самом месте, где оно объявлено. И у лямбды есть единственный синтаксис (параметры){тело}.

          А то, что стоит справа (и слева) в операторах (? : && ||) это операнды. И у них тоже единственный синтаксис - это просто "выражение".

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

          x ? (data) { data.something() }

          Такой синтаксис в Аргентуме уже занят. Он означает, что x - это optional лямбда, и мы тут распаковываем ее из optional, предоставляя (data){data.something()} в качестве результата, если в optional ничего нет.

          на мой взгляд, это ?=data очень страшно выглядит

          В век, когда нормой считается такой синтаксис struct RP<'a,'b>(&'a u32,&'b u32)говорить о страшности синтаксиса ?=dataдля введения временного имени это вкусовщина.


    1. qw1
      20.08.2023 23:39

      x=… и a/5 очень далеко разнесены друг от друга. Это снижает читабельность. Вместо короткого оператора присваивания мы имеем длинную многострочную конструкцию. А если там будет не 3, а 20 строк и еще несколько вложенных блоков?

      Это претензия не к языку, а к конкретному плохому коду. Можно на обычном C++ написать выражение со 150 вложенными скобками, на 30 строках.


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

      Анонимная функция подразумевает
      1) тело функции, которое ограничено например { } (хотя, тут можно условиться, что если тело состоит из 1 выражения, скобки опускаются)
      2) способ вызова этой функции.
      То есть, в примере x ? _+1
      если его развёрнуть подробнее, x ? (arg) => { arg+1; }
      семантика такая, что вернётся функция, а чтобы её вычислить, надо её вызвать:
      (x ? (arg) => { arg+1; }) ()
      или как?


      Или вот еще один пример: у нас есть Promise, возвращающий User. Мы бы хотели из него получить промиз, возвращающий Address, а из адреса — Promise, возвращающий улицу

      Автор ещё не дошёл до асинхронщины, до тасков, до await, до SyncronizationContext. Когда (если?) дойдёт, придётся кое-что переосмыслить, если захочет удобно интегрировать в язык.


      a < 0 ? log() Если вам нужен if, то лучше явно писать if

      Да, меня тоже сильно запутывают всякие unless из Perl, где условие можно писать и до, и после значения, и Питоновский
      12 if (x < 6) else 13


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


      плохо читаемый (в одном операторе смешано присваивание и проверка) if (auto v = expression; predicate(v)) use(v);

      это позволяет снизить вложенность скобок в конструкциях вида


      if (...) {
      
      } else if (...) {
      
      } else if (...)

      Иначе, пришлось бы каждый else if выносить в отдельный блок вложенности, добавляя инициализацию переменной перед if.


      1. spidersarecute
        20.08.2023 23:39

        семантика такая, что вернётся функция, а чтобы её вычислить, надо её вызвать:

        Допустим, что оператор ? имеет синтаксис:

        <optional>?<function>

        То есть, слева указываем optional, а справа функцию, а он сам ее вызовет, если нужно. То есть, я не вижу смысла иметь один синтаксис для анонимных функций, а другой для выражения справа от ?.


        1. qw1
          20.08.2023 23:39

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


      1. spidersarecute
        20.08.2023 23:39

        > плохо читаемый (в одном операторе смешано присваивание и проверка

        это позволяет снизить вложенность скобок в конструкциях вида

        Ну лучше наверно чуть больше читабельных строк, чем лаконичная запутанность. Можно наверно код как-то в функции выносить, или использовать что-то вроде pattern matching тут?


    1. kotan-11 Автор
      20.08.2023 23:39

      3) В языке есть функционал для обработки optional значений по типу

      y = x ? _ + 1

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

      Или у нас есть список чисел, а хотелось бы получить список увеличенных на 1 чисел.

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

      Или вот еще один пример: у нас есть Promise, возвращающий User. Мы бы хотели из него получить промиз, возвращающий Address, а из адреса - Promise, возвращающий улицу. Было бы хорошо, если бы была возможность сделать это кратко и логично, вроде:

      street_promise = user_promise->address->street

      Монада - удобная математическая абстракция. К сожалению как все обстракции при инженерном применении в реальной жизни она протекает. Промис, как асинхронный процесс должен иметь возможность прерывания и опроса прогресса. Список, как итеративный процесс должен иметь возможность определения порядка и направления обхода и прерывания, поскольку реальный код всегда работает в IO-монаде. Если у вас есть идеи, как вписать все многообразие вариантов маппинга содержимого контейнера с помощью одно примивной низкоуровневой операции "?", поделитесь.


      1. spidersarecute
        20.08.2023 23:39

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

        Так а это принципиально невозможно реализовать с описанным выше синтаксисом? Если уж на то пошло, то прерывание street_promise вполне может прерывать и user_promise, если у него нет других потребителей.


  1. qw1
    20.08.2023 23:39

    currentOrderId ? orders.findOrder(_) ? _.getPrice() ? processPrice(_);

    Это мне напомнило из C#


    orderService.GetOrder(OrderId)?.GetCustomer()?.GetAddress()?.GetCity()?.ToString()


    гораздо компактнее, чем каждый раз обращаться к предыдущему результату через _, но требует, что функция-продолжение была у левого операнда ?. (в C# это частично решается через extension methods).


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


    1. qw1
      20.08.2023 23:39
      +1

      Хотя, вроде нормально выглядит


      orderService.GetOrder(OrderId)?_.GetCustomer()?_.GetAddress()?_.GetCity()?_.ToString()


      1. spidersarecute
        20.08.2023 23:39

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

        А разве плохо было бы, если бы эти 3 случая (обработка nullable, Promise и коллекций) реализовались одним синтаксисом?


        1. qw1
          20.08.2023 23:39

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

          А в чём проблема использовать ф-цию map?


          cityNames = orderIds
            .Select(OrderId =>
              orderService.GetOrder(OrderId)?_.GetCustomer()?_.GetAddress()?_.GetCity()?_.ToString())
          .ToList()