Ссылки
Главная фундаментальная особенность Аргентума - это его ссылочная модель данных, обеспечивающая не только безопасность памяти, как в 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)
Panzerschrek
20.08.2023 23:39Интересный подход с
bool
какoptional
. Весьма необычно.
С тернарным оператором (которого нету) немножко страшновато - а не получится ли так, что привыкшие к C++ программисты по-ошибке его заиспользуют и получат не то, что хотели?
На счёт невозможности развернутьoptional
без проверки - а что, если прямо нужно безусловно развернутьoptional
? Есть что-то вродеunwrap
из Rust (ясно дело сpanic!
, еслиoptional
пуст)?kotan-11 Автор
20.08.2023 23:39а не получится ли так, что привыкшие к C++ программисты по-ошибке его заиспользуют и получат не то, что хотели
Если A?B:C применить по-старому, он и работать будет по-старому.
Есть что-то вроде
unwrap
из Rust ?Ели хочется, чтобы приложение прямо падало (на сервере или у клиента), напиши
x = myOptVar : terminate();
qw1
20.08.2023 23:39+1Идея очень интересная. Но появляются существенные требования к компилятору, чтобы он хорошо оптимизировал такую конструкцию, когда программист не подразумевает тут optional.
sashagil
20.08.2023 23:39+1Интересно - однако, я не понял, как ":" поступает с типами операндов и своего результата. В примерах либо слева и справа тот же тип (целое, строка) или (более интересный второй случай) справа - вызов функции, возвращающей void, и затем ";". Второй случай намекает, что тип правого параметра диктует тип результа. Первый случай намекает, что тип левого параметра - тоже в деле. Хорошо, а если справа - целое, а слева - строка? Скорее всего, произойдет ошибка приведения типов ( хотя, можно и пофантозировать, что ошибки не будет, а типом результата будет тип-сумма целого и строки, int | string, как можно делать на TypeScript-е). А если справа и слева - пользовательские типы в каких-то отношениях subtype / supertype?
kotan-11 Автор
20.08.2023 23:39Оператор ":" всегда имеет тип (?T) : (T) -> (T). Например, если T=void, то тип будет bool : void ->void, как у else-ветки в Cи-подобных языках.
Он просто распаковывет LHS, и возвращает. Если вернуть нечего, исполняет RHS и возвращает его.
qw1
20.08.2023 23:39Могу ли я написать
cat : defaultAnimal
Если cat — объект класса Cat, наследника Animal, а defaultAnimal — класса Animal.И есть ли у всех типов общий предок (как Object в C#).
Например,a > 0 : 42
может ли вернуть Object, в который упакован либо true, либо 42?
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) });
kotan-11 Автор
20.08.2023 23:391) Мне не нравится синтаксис блока-выражения:
x = {
a = 3;
a += myFn(a);
a / 5 // это выражение станет значением `x`
};
Пробема здесь, как мне кажется, в том, что
x=..
иa/5
очень далеко разнесены друг от друга. Это снижает читабельность. Вместо короткого оператора присваивания мы имеем длинную многострочную конструкцию. А если там будет не 3, а 20 строк и еще несколько вложенных блоков?x=..
иa/5
разнесены друг от друга не дальше чем оператор return от заголовка функции. Будем запрещать локальные функции?
kotan-11 Автор
20.08.2023 23:39+12) Справа от оператора
?
с помощью символа_
, видимо, создается выражение для преобразования содержимого переменной. То есть, это выражение работает только в операторе?
. Но почему бы не сделать эту конструкцию более универсальной и применимой везде, чтобы символ_
создавал анонимную функцию, [как это сделано в Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Shorthand-Argument-Names) с символами$0
,$1
и тд ?Позиционные нумерованные параметры - это последнее, что я бы хотел иметь в языке.
В Аргентуме есть анонимные функции с именованными параметрами: (parameters){body}, возможно имеет смысл добавить облегченный стрелочный синтаксис или \-синтаксис.
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() }
или как в Swiftx ? { data in data.something() }
Ну то есть, синтаксис по тяжести и запутанности ближе к C++, а не к простоте Питона, например.
qw1
20.08.2023 23:391) тут нет анонимной функции, это просто синтаксический сахарок.
2) Да уж, выглядит как quoted-printable ))) А варианты?
Идеально — со скобочками
x ?(data) { data.something(); }
но круглые скобки уже заняты под выражение, вдруг мы вообще не хотим никаких функций
x ? (2+3)*4 : (7+8)*9
Неплохо выглядит "больше", как знак перенаправления
x ?>data { data+1 }
3) А тут уже полноценная функция, которую передаём как аргумент в forEach, не надо путать с первым кейсом.
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
для введения временного имени это вкусовщина.
qw1
20.08.2023 23:39x=… и 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.
spidersarecute
20.08.2023 23:39семантика такая, что вернётся функция, а чтобы её вычислить, надо её вызвать:
Допустим, что оператор
?
имеет синтаксис:<optional>?<function>
То есть, слева указываем optional, а справа функцию, а он сам ее вызовет, если нужно. То есть, я не вижу смысла иметь один синтаксис для анонимных функций, а другой для выражения справа от
?
.qw1
20.08.2023 23:39А я не представляю, как компилятор может понять, функцию вызывать сейчас, или оставить невызванной.
spidersarecute
20.08.2023 23:39> плохо читаемый (в одном операторе смешано присваивание и проверка
это позволяет снизить вложенность скобок в конструкциях вида
Ну лучше наверно чуть больше читабельных строк, чем лаконичная запутанность. Можно наверно код как-то в функции выносить, или использовать что-то вроде pattern matching тут?
kotan-11 Автор
20.08.2023 23:393) В языке есть функционал для обработки optional значений по типу
y = x ? _ + 1
но такой функционал нужен не только для optional значений, а и для других случаев, например, Promise. Пусть у нас есть Promise, который может вернуть какое-то число, и хотелось бы получить Promise, возвращающий увеличенное на 1 число. Нельзя ли использовать аналогичный синтаксис?
Или у нас есть список чисел, а хотелось бы получить список увеличенных на 1 чисел.
Не стоит ли сделать единый синтаксис для всех таких случаев, когда надо сделать какую-то операцию над обернутым в контейнер значением?
Или вот еще один пример: у нас есть Promise, возвращающий User. Мы бы хотели из него получить промиз, возвращающий Address, а из адреса - Promise, возвращающий улицу. Было бы хорошо, если бы была возможность сделать это кратко и логично, вроде:
street_promise = user_promise->address->street
Монада - удобная математическая абстракция. К сожалению как все обстракции при инженерном применении в реальной жизни она протекает. Промис, как асинхронный процесс должен иметь возможность прерывания и опроса прогресса. Список, как итеративный процесс должен иметь возможность определения порядка и направления обхода и прерывания, поскольку реальный код всегда работает в IO-монаде. Если у вас есть идеи, как вписать все многообразие вариантов маппинга содержимого контейнера с помощью одно примивной низкоуровневой операции "?", поделитесь.
spidersarecute
20.08.2023 23:39Промис, как асинхронный процесс должен иметь возможность прерывания и опроса прогресса.
Так а это принципиально невозможно реализовать с описанным выше синтаксисом? Если уж на то пошло, то прерывание street_promise вполне может прерывать и user_promise, если у него нет других потребителей.
qw1
20.08.2023 23:39currentOrderId ? orders.findOrder(_) ? _.getPrice() ? processPrice(_);
Это мне напомнило из C#
orderService.GetOrder(OrderId)?.GetCustomer()?.GetAddress()?.GetCity()?.ToString()
гораздо компактнее, чем каждый раз обращаться к предыдущему результату через
_
, но требует, что функция-продолжение была у левого операнда?.
(в C# это частично решается через extension methods).Вроде хочется такую компактную запись, но непонятно, как это сделать в общем виде.
qw1
20.08.2023 23:39+1Хотя, вроде нормально выглядит
orderService.GetOrder(OrderId)?_.GetCustomer()?_.GetAddress()?_.GetCity()?_.ToString()
spidersarecute
20.08.2023 23:39Минус в том, что это работает только с nullable типами, но не сработает, например, с промисами или с массивом объектов Order (у меня есть массив Order, и я хочу получить массив городов, не используя циклы и сложный синтаксис).
А разве плохо было бы, если бы эти 3 случая (обработка nullable, Promise и коллекций) реализовались одним синтаксисом?
qw1
20.08.2023 23:39у меня есть массив Order, и я хочу получить массив городов, не используя циклы и сложный синтаксис
А в чём проблема использовать ф-цию map?
cityNames = orderIds .Select(OrderId => orderService.GetOrder(OrderId)?_.GetCustomer()?_.GetAddress()?_.GetCity()?_.ToString()) .ToList()
samsergey
Очень классно! Душа хаскеллиста радуется при виде аппликативного условного оператора. Дайте, пожалуйста ссылку на формальное описание языка и на его репозиторий, чтобы не задавать обычные вопросы о его назначении.