nim-lang / RFCs

A repository for your Nim proposals.
136 stars 23 forks source link

try/except scope -- should it be a thing? #218

Closed disruptek closed 1 year ago

disruptek commented 4 years ago

The try: currently creates a new scope, consistent with other block : keywords. But this promotes the use of defer: to replace finally: without opening scope. Is there a good reason not to change this behavior (and maybe remove defer?) before scope-based destruction becomes more widespread?

Clyybber commented 4 years 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.

Araq commented 4 years ago

Huh, good point. :-)

zah commented 4 years ago

Also, what's wrong with defer?

disruptek commented 4 years ago

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.

disruptek commented 4 years ago

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.

disruptek commented 4 years ago

Maybe syntax like this?

try:
finally {.dirty.}:
Araq commented 4 years ago

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)
zah commented 4 years ago

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.

disruptek commented 4 years ago

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.

zah commented 4 years ago

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.

disruptek commented 4 years ago

I don't mean to be obtuse, but your example works and your modification does not change the output...

zah commented 4 years ago

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
github-actions[bot] commented 1 year ago

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.