Closed disruptek closed 1 year ago
try:
needs to open a scope since values of the variables declared in the try
block are potentially undefined if an exception is raised.
Huh, good point. :-)
Also, what's wrong with defer
?
You think that defer:
probably opens a new scope, like try:
, but it doesn't. It requires the reader to understand the semantics of defer
to make sense of the fact that code within the body may not run ahead of code presented later. Or that code within the block shares scope with its parent.
Worse, what about when the reader is a macro? Fun special case.
I used defer
in templates like this because it seems to be the best tool for the job, but I wish it wasn't... Spooky action at a distance.
template gitTrap*(allocd: typed; code: GitResultCode; body: untyped) =
## trap an api result code, dump it via logging,
## run the body as an error handler
defer:
if code == grcOk:
free(allocd)
gitTrap(code, body)
What about moving it to become a peer of finally
?
var slapper = newSlapper()
try:
slapper.commenceSlapping
sleep 10_000
var midpoint = slapper.count
sleep 10_000
slapper.join
except SlapError:
echo "backhands detected: ", slapper.backhands
finally:
echo "slaps: ", slapper.count, " smacks: ", slapper.smacks
defer:
free slapper
What I really want is to be able to use my try:
scope in its clauses; you can tear it up after I'm done with it, thanks.
Honestly, I want to make constructs like this and control scoping behavior. I want to be able to create my for: else:
and things like promise: then: done:
or whatever. I know it's problematic for the compiler for some reason, but I want it anyway.
Maybe syntax like this?
try:
finally {.dirty.}:
Here is a concrete example:
let (s, exitCode) = execCmdEx(exe & " --version")
was the code, now it should be in a try
, let's try:
try:
var (s, exitCode) = execCmdEx(exe & " --version")
except:
exitCode = 1
Thankfully at least this works:
let (s, exitCode) = try: execCmdEx(exe & " --version") except: ("", 1)
I agree that the behavior that @disruptek expects from defer
in templates is better than the current one. The current behavior of defer
and finally
in templates allows you to create some other forms of arguably unsafe code:
type Resource = object
proc createResource: Resource =
echo "Creating resource"
proc useResource(x: Resource) =
echo "Using resource"
proc close(x: var Resource) =
echo "Closing resource"
template createTempResource: untyped =
var resource = createResource()
try:
resource
finally:
close resource
useResource createTempResource()
The proper output of this program should be
Creating resource
Using resource
Closing resource
... but the current output is
Creating resource
Closing resource
Using resource
If the Resource
type had a destructor, I would expect the same proper output as well, preferably with the C++'s rules that destructors execute at the semicolon of the enclosing statement.
IIUC, you want to change this behavior:
proc foo(): int =
result = try:
1
finally:
inc result
echo foo() # 2
...to now have an embedded defer
statement (introduced by the library author) in the form of a finally
clause. Where it runs is a function of where I evaluate the try
expression, so if I wrap it in another call, it runs at the bottom of that call's scope, right? Does it precede other destruction?
This sounds objectively worse to me, and super fun to implement, but maybe I'm not grasping your suggestion.
My example above concerns only the use of defer
in templates. I guess the easy way to think about it is that defer
should be equivalent to creating an object with a destructor.
Consider the makeObject
template in the following template:
type
Destructible = object
value: int
proc createDestructible(value: int): Destructible =
echo "Creating"
result.value = value
proc `=destroy`(x: var Destructible) =
echo "Destroying"
proc useDestructible(x: Destructible) =
echo "Using ", x.value
template makeObject(): auto =
createDestructible(100)
useDestructible makeObject()
You would expect the code to print:
Creating
Using 100
Destroying
What should change if we rewrite the code like this?
template makeObject(): auto =
var x = createDestructible(100)
x
My expectation is that nothing should change.
Now, every time you use defer
inside a template, that should be equivalent to creating a local anonymous object with a destructor. This leads to quite consistent and easier to explain rules.
I don't mean to be obtuse, but your example works and your modification does not change the output...
What I was explaining is that I expect the same behavior to be produced by defer
used in the following way:
template makeObject(): auto =
var resource = createResource(100) # Please note that `Resource` is a type with a destructor
defer: close resource
resource
My point was that one can always answer "what should defer
do in situation X?" by answering "what would a destructor do in the same situation?"
The line defer: close resource
is semantically equivalent to creating a variable capturing resource
by reference (in C++ terms) that has a destructor that calls close resource
.
With defer
and finally
, the current output is:
Creating resource
Closing resource # <--- closing before using
Using resource
This RFC is stale because it has been open for 1095 days with no activity. Contribute a fix or comment on the issue, or it will be closed in 7 days.
The
try:
currently creates a new scope, consistent with other block:
keywords. But this promotes the use ofdefer:
to replacefinally:
without opening scope. Is there a good reason not to change this behavior (and maybe removedefer
?) before scope-based destruction becomes more widespread?