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

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

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

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

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

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

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

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

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

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

x = myOptVar : terminate();

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

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

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

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

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


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

Пока так:

class Animal {
    say() { log("I'm an animal, my parents were animals") }
}
class Cat {
    +Animal {
        say() { log("Meow") }
    }
}
default = Animal;
a = +Cat;         // если поменять на ?Cat, скажет I'm an animal...

x = a?_~Animal:default;
x.say();    // скажет Meow

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

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

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) Мне не нравится синтаксис блока-выражения:

x = {

a = 3;

a += myFn(a);

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

};

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

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

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

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

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

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

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) тут нет анонимной функции, это просто синтаксический сахарок.


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


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


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


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


x ?>data { data+1 }


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

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

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

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

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

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

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

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

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

fn getOptInt(x: int) -> ?int { x != 0 ? 42 / x }

x = getOptInt(42) ?| + 3; //x == 4

x = x ?| * 5 + 1 | getOptInt; //x == 2

x ?| == 2 : panic("фигня какая то")

Ну помимо сахара собственно от пайпа это ещё решает проблему _, оно становится не нужно. А если нужно (пере)именование, то можно так

x = x ? { |x; x + 5 }

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

x ? { | = x; x + 5 } = x;

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.

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

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

<optional>?<function>

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

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

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

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

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

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

y = x ? _ + 1

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

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

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

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

street_promise = user_promise->address->street

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

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

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

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

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


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


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


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

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


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

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

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

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

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


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

Блок может присутствовать в других выражениях, например быть инициализатором переменной.
x = { a / 5 };

А что вернёт подобное выражение, будучи применённым к циклу (если такой трюк возможен)?

x = for( ... )
{
    // some code
    expression
}

Он вернёт результат выполнения expression или результат некоего действия в инициализаторе цикла ? Ведь после выполнения expression произойдёт проверка на завершение цикла, так ?

В Аргентуме есть цикл loop expression, который требует, чтобы expression возвращал значение типа ?T.Цикл крутится, пока возвращается nothing. Как только возвращается не-nothing-значение, оно распаковывается из optional и становится результатом loop.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории