You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
(Leaving this here for consideration, perhaps in a distant future. :-) )
Here's an idea of a construct that would make writing code with multiple-assignment and error checking much cleaner. I'll present via an example which should be self-explanatory:
localfoo, bar | nil, err=func()
ifnotfoothenprint("func failed: " ..err)
end
Using the name of a return value like bar in the above example to mean not what the variable name says but an error message is an awkward part of using Lua, and having types around makes something like this even more desirable.
Note the use of nil in the lvalue. This would be a shorthand for _: nil. A rule for valid destructuring should be that at least one variable would have to be explicitly typed in an unambiguous manner (i.e. the nth entry of the multiple-return tuple so that no two cases have the same type in the nth entry). Usually, having a nil entry like this would suffice to satisfy this rule.
Level 1: Minimal implementation
Serves only a syntactic aid to be able to write err instead of bar where it's more logical to.
Merely treat err as an alias for bar at the compilation stage, generating this code:
localfoo, bar=func()
ifnotfoothenprint("func failed: " ..bar)
end
No flow analysis required. It wouldn't even be necessary to enforce the rule for valid destructuring. It's simplistic, but already better than plain Lua.
Using bar and err interchangeably in the code regardless of the value of foo would look a bit odd, though. With flow analysis enforcing the disciplined use of the variables (you can only use err if you tested that foo is nil), the types of bar and err could be simpler from the get-go.
Another con is that using the debug library to inspect locals would reveal the trick.
Level 2: Generating all locals
Generate this code:
localfoo, bar, err; local_t1, _t2=func(); foo, bar, err=_t1, _t2, _t2;
ifnotfoothenprint("func failed: " ..err)
end
Solves the debug.getlocal problem. The extra multiple-assignment of locals after the function call cannot produce any side-effects and cannot produce runtime errors, so it will be essentially invisible at runtime (in the sense that, if generated right beside the end of the assigment, it cannot affect error messages).
The same observation on flow analysis above applies.
A complication when we think of these values as being smartly typed and not just aliases: boolean types. Typing if not foo for a situation like the above is a lot more idiomatic than is foo ~= nil or is type(foo) == "string". Adding a restriction such as "can't disambiguate on boolean vs. nil" would be problematic, since "true or nil, message" is typical idiom too.
Level 3: Only assign to the correct case
The solution for this complication might be something else entirely. In an even nicer world, what I'd like to write instead in my code would be this:
localfoo, bar | nil, err=func()
iferrthenprint("func failed: " ..err)
end
This would mean generating code like this:
localfoo, bar, err; local_t1, _t2=func(); if_t1==nilthenerr=_t2elsefoo, bar=_t1, _t2end;
iferrthenprint("func failed: " ..err)
end
Note that, in this scenario, if the type of func() is * -> (boolean, * | nil, string), then the original code with if not foo then should cause a compiler error on the use of err, since if not foo can't work as a type assertion when foo is boolean. A solution for this would be typing func() as * -> (true, * | nil, string) (which I suspect would be nicely inferrable in most functions which use return true, value and return nil, "message" in their implementations.)
When using literal types it should be possible to use literals in the lvalue (such as nil, true or even numbers and strings) and generate equality comparisons in the generated if code (destructuring tables would open a whole other can of worms because of the risk of triggering metamethods in the == test, but testing for type(_t1) == "table" is harmless). Disambiguating with explicit type annotations would generate type() tests. For example, given a function f with type () -> (string, number | number, boolean) this
Assuming the type system has built-in knowledge that pcall() is a special kind of apply, then this kind of usage should be possible as well:
local ok, foo, bar | ok, nil, err | nil, perr = pcall(func)
if ok then
if foo then
usefoobar(foo, bar)
else
print("func failed: " .. err)
end
else
print("pcall failed: " .. perr)
end
Note nil being used in two different places of a three-way union to satisfy the disambiguation rule.
Also, note that ok is declared twice: this should be allowed if the name appears in the same position of the tuple and has the same type.
The relevant line above would be translated in Level 2 to:
localok, foo, bar, err, perr; local_v1, _v2, _v3=pcall(func); ok, foo, bar, err, perr=_v1, _v2, _v3, _v2, _v3;
In a language supporting the above feature, I believe the use of tests like if type(x) == "string" then would reduce a lot.
Especially in Level 3, there would be little use for type() as a director of control flow:
n:number | s:string=func()
ifnthen...end
I don't believe the performance impact of the extra generated code of Level 3 would be significant; typical use would incur in a nil test and a few extra local assignments; in the cases where type() tests would be generated, we'd usually have to write those in the code anyway, they just become implicit.
(Whew! That was a lot, but I thought I'd share this spur of sudden creativity here than to let it fizzle away. :-) )
The text was updated successfully, but these errors were encountered:
(Leaving this here for consideration, perhaps in a distant future. :-) )
Here's an idea of a construct that would make writing code with multiple-assignment and error checking much cleaner. I'll present via an example which should be self-explanatory:
Using the name of a return value like
bar
in the above example to mean not what the variable name says but an error message is an awkward part of using Lua, and having types around makes something like this even more desirable.Note the use of
nil
in the lvalue. This would be a shorthand for_: nil
. A rule for valid destructuring should be that at least one variable would have to be explicitly typed in an unambiguous manner (i.e. the nth entry of the multiple-return tuple so that no two cases have the same type in the nth entry). Usually, having anil
entry like this would suffice to satisfy this rule.Level 1: Minimal implementation
Serves only a syntactic aid to be able to write
err
instead ofbar
where it's more logical to.Merely treat
err
as an alias forbar
at the compilation stage, generating this code:No flow analysis required. It wouldn't even be necessary to enforce the rule for valid destructuring. It's simplistic, but already better than plain Lua.
Using
bar
anderr
interchangeably in the code regardless of the value offoo
would look a bit odd, though. With flow analysis enforcing the disciplined use of the variables (you can only useerr
if you tested thatfoo
is nil), the types ofbar
anderr
could be simpler from the get-go.Another con is that using the
debug
library to inspect locals would reveal the trick.Level 2: Generating all locals
Generate this code:
Solves the
debug.getlocal
problem. The extra multiple-assignment of locals after the function call cannot produce any side-effects and cannot produce runtime errors, so it will be essentially invisible at runtime (in the sense that, if generated right beside the end of the assigment, it cannot affect error messages).The same observation on flow analysis above applies.
A complication when we think of these values as being smartly typed and not just aliases: boolean types. Typing
if not foo
for a situation like the above is a lot more idiomatic thanis foo ~= nil
oris type(foo) == "string"
. Adding a restriction such as "can't disambiguate on boolean vs. nil" would be problematic, since "true or nil, message" is typical idiom too.Level 3: Only assign to the correct case
The solution for this complication might be something else entirely. In an even nicer world, what I'd like to write instead in my code would be this:
This would mean generating code like this:
Note that, in this scenario, if the type of
func()
is* -> (boolean, * | nil, string)
, then the original code withif not foo then
should cause a compiler error on the use oferr
, sinceif not foo
can't work as a type assertion whenfoo
is boolean. A solution for this would be typingfunc()
as* -> (true, * | nil, string)
(which I suspect would be nicely inferrable in most functions which usereturn true, value
andreturn nil, "message"
in their implementations.)When using literal types it should be possible to use literals in the lvalue (such as
nil
,true
or even numbers and strings) and generate equality comparisons in the generatedif
code (destructuring tables would open a whole other can of worms because of the risk of triggering metamethods in the==
test, but testing fortype(_t1) == "table"
is harmless). Disambiguating with explicit type annotations would generatetype()
tests. For example, given a functionf
with type() -> (string, number | number, boolean)
thiswould generate this
Bonus advanced usage:
Assuming the type system has built-in knowledge that
pcall()
is a special kind of apply, then this kind of usage should be possible as well:Note
nil
being used in two different places of a three-way union to satisfy the disambiguation rule.Also, note that
ok
is declared twice: this should be allowed if the name appears in the same position of the tuple and has the same type.The relevant line above would be translated in Level 2 to:
or in Level 3 to:
Establishing a typed idiom
In a language supporting the above feature, I believe the use of tests like
if type(x) == "string" then
would reduce a lot.Especially in Level 3, there would be little use for
type()
as a director of control flow:I don't believe the performance impact of the extra generated code of Level 3 would be significant; typical use would incur in a
nil
test and a few extra local assignments; in the cases wheretype()
tests would be generated, we'd usually have to write those in the code anyway, they just become implicit.(Whew! That was a lot, but I thought I'd share this spur of sudden creativity here than to let it fizzle away. :-) )
The text was updated successfully, but these errors were encountered: