Повайбкодил немного на daslang на джеме, сделал вот такую игру.
Наткнулся на то, что ИИ регулярно спотыкается об одну особенность синтаксиса, решил посмотреть, как ему можно помочь с этим.
struct A {
f1: int
f2: int
}
[export]
def main {
// Есть массив структур
var arrayOfA = [
A(f1=1, f2=2),
A(f1=3, f2=4),
]
// хотим поменять поля первой структуры через ссылку
var a : A& = arrayOfA[0]
a.f1 = 5
a.f2 = 6
}Компилятор ругается тут:
c:\src\daScript\bin\Debug>daslang.exe base_sample.das
error[31300]: local reference to non-local expression is unsafe
base_sample.das:15:8
var a : A& = arrayOfA[0]Программисты на C++ могут немного удивиться, но Daslang считает этот код небезопасным, потому что между получением ссылки и обращением к структуре через эту ссылку можно изменить arrayOfA:
var a : A& = arrayOfA[0]
// массив может перевыделить память - push/resize/erase/clear могут сделать ссылку висячей
arrayOfA |> push(A(f1=10, f2=20))
a.f1 = 5Какие более безопасные варианты обращения к первому элементу массива могут быть?
Отбросим сразу варианты обращения по полному имени (раздувает код) и копирования всей структуры (пессимизация с двойным копированием на ровном месте).
Хранение структур в куче
struct A {
f1: int
f2: int
}
[export]
def main {
// Есть массив структур
var arrayOfA = [
new A(f1=1, f2=2),
new A(f1=3, f2=4),
]
var a : A? = arrayOfA[0]
a.f1 = 5
a.f2 = 6
arrayOfA |> clear
//print("arrayOfA[0].f1 = {arrayOfA[0].f1}, arrayOfA[0].f2 = {arrayOfA[0].f2}\n")
print("a.f1 = {a.f1}, a.f2 = {a.f2}\n")
}Цена — дополнительный уровень индирекции, многовато ради простого удобства синтаксиса, хоть и не самый плохой вариант.
With
Для того, чтобы не писать имя переменной, задумано выражение with:
with (arrayOfA[0]) {
f1 = 5
f2 = 6
}Но оно не подходит, если нужно передать структуру в функцию:
with (arrayOfA[0]) {
set_f1(arrayOfA[0]) //нет сокращенного имени
set_f2(arrayOfA[0])
}Assume
Есть ещё абсолютно ужасный assume:
assume a0 = arrayOfA[0]
set_f1(a0)
set_f2(a0)Он подходит по синтаксису, но опасен тем, что вычисляет выражение каждый раз. Т.е. имеет все те же эффекты, что и define в C++ — взорвётся не только при перевыделении памяти arrayOfA, но и если выражение в assume имеет побочные эффекты или зависит от других выражений:
var i = 0
assume a0 = arrayOfA[i]
set_f1(a0)
i += 1
set_f2(a0) //обращение к другой структуре из-за изменения iТ.е. assume просто сам по себе даже более опасен, что ссылки, но dascript не требует помечать его unsafe.
Передача ссылок в функцию
def do_with(var self: A&; b : block<(var a : A): void>) {
invoke(b, self)
}
def do_with(var self: A&; b : block<(var a : A)>) {
return invoke(b, self)
}
let f1 = do_with(arrayOfA[0]) $(var a : A) {
a.f1 = 42
return a.f1
}
do_with(arrayOfA[0]) $(var a : A) {
a.f2 = 43
}
print("{f1}, {arrayOfA[0].f2}")Такой вариант не самый красивый по синтаксису, зато хотя бы безопаснее assume.
Если arrayOfA глобальный, то всё ещё можно изменить его внутри самого блока, переданного в do_with. Это уже не поймает компилятор — нет способа выразить или проверить время жизни массива и ссылки, как делает rust. Тут в любом случае нужно поддерживать ограничение на изменение глобального массива (например, введя правило — менять его только через очередь сообщений).
Syntax matters
В kotlin есть такой синтаксис для scope-функций:
with(arrayOfA[0]) {
f1 = 5
println(this)
}В daslang можно сделать макрос, который также позволит сэкономить символы:
let f1 = with_(arrayOfA[0]) {
_.f1 = 42
return _.f1
}
with_ (arrayOfA[0]) {
_.f2 = 43
}
print("{f1}, {arrayOfA[0].f2}")Работает это за счёт синтаксического сахара, описанного тут — Last block pipes itself. Теперь синтаксис выглядит также, как и выражение with, встроенное в язык.