Управляющие конструкции в языке программирования Аргентум

от автора

Ссылки

Главная фундаментальная особенность Аргентума — это его ссылочная модель данных, обеспечивающая не только безопасность памяти, как в 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-типов бесплатно для всех указателей, и большинства типов значений.


ссылка на оригинал статьи https://habr.com/ru/articles/755672/


Комментарии

Добавить комментарий

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