chapel-lang / chapel

a Productive Parallel Programming Language
https://chapel-lang.org
Other
1.78k stars 420 forks source link

Should/Can postfix-! preserve ownership type when changing nilability? #18460

Closed bradcray closed 1 year ago

bradcray commented 3 years ago

As a Chapel programmer, when I write:

class C {
  var x: int;
}

var myval: shared C? = new shared C(23);
proc myfunc(): shared C {
  return myval!;
}

myfunc();

I would expect this to work since I intuitively imagine ! to change a shared C? into a shared C. But instead, it seems to give a borrowed C resulting in a type mismatch error.

Is this something that could be changed in the language?

mppf commented 3 years ago

Right, today postfix ! converts from owned/shared to borrowed. We did consider alternatives (see https://github.com/chapel-lang/chapel/issues/12614#issuecomment-488006254 in particular). The main issue here is something like this example:

var x: owned C?  = ...;
x!.someMethod();
x!.someOtherMethod();

If we make x! in the example result in something of type owned C, that would mean doing ownership transfer out of the x variable, which would be surprising (and in particular, it would lead to a failure on the 2nd method call, because the receiver would be nil).

It is true that this specific problem does not come up with shared, but I would view it as a pretty big performance issue if, for a var y: shared C?, every time we wanted to call a method like y!.someMethod() we incremented and decremented the reference count. Which is what having y! result in a shared C would do. At the same time, I like that the current design has similar behavior for owned and shared.

Lastly, we are aware that this pattern comes up. There is just a different way to write it with a cast:

class C {
  var x: int;
}

var myval: shared C? = new shared C(23);
proc myfunc(): shared C {
  return myval: shared C;
}

myfunc();
bradcray commented 3 years ago

Thanks @mppf — I had a vague memory of this conversation, but couldn't re-rationalize our choice well enough on my own (and wouldn't have been able to find the original conversation quickly), so appreciate the explanation here. I'm leaving this open for now, pending counterarguments from users, but if there are none, we can close this and then hopefully I'll find it more easily the next time the question comes up.

bradcray commented 3 years ago

@mppf: Following up, when you say:

If we make x! in the example result in something of type owned C, that would mean doing ownership transfer out of the x variable

would it be possible to have x! not do ownership transfer, and instead to have it be treated as a variable sharing the same memory location as x but simply with a distinct type from the compiler's perspective? (i.e., taking advantage of the fact that classes have a built-in nil value that doesn't require extra storage?)

mppf commented 3 years ago

I must be missing something...

would it be possible to have x! not do ownership transfer, and instead to have it be treated as a variable sharing the same memory location as x but simply with a distinct type from the compiler's perspective? (i.e., taking advantage of the fact that classes have a built-in nil value that doesn't require extra storage?)

That is what we have now. The distinct type is borrowed C.

Ownership transfer amounts to setting x to nil which takes advantage of the built-in nil value.

bradcray commented 3 years ago

Sorry, I wasn't as complete or clear in my previous comment as I should've been. I meant to imply "not have it do ownership transfer, but alias it and preserve the base managed type" but didn't say that. That is, rather than have the compiler view the owned C? as a borrowed C, what are the challenges to having it view it as an owned C? (perhaps a const owned C as a simplified case?)

mppf commented 3 years ago

I'm still not following the idea. Could you show a code example with comments about what you are thinking about?

bradcray commented 2 years ago

[We ended up taking this discussion offline, where I'll try to capture some salient points here:

Intuitively, users may want to write:

    proc foo(c: shared C) { }

    var myC: shared C? = new shared C?();

    foo(myC!);

Or similarly with owned, though that seems less likely to show up in patterns like this

The workaround we discussed was:

foo(try! myC: shared C);

which avoids needing to change the rules of the ! operator. If this was insufficient, we discussed:

bradcray commented 1 year ago

@mstrout was asking me about this issue today, and looking at it again after a year, I don't feel particularly worried about the status quo of what we have here. I do think that this is likely to be a common question for users using nilable classes ("Why doesn't applying ! to a variable of type t? turn it into a variable of type t?") but I think we can handle that through a combination of documentation, FAQs/SO Q&A, and/or specialized errors in the compiler ("Try using a cast instead") rather than a language change.

I also want to point out that the way I wrote foo(try! myC: shared C); is a bit ugly and that moving the try! outside of the call to foo() or putting it within a larger try ... catch block makes it look less ugly/cluttered.

bradcray commented 1 year ago

(I'm going to close this based on the argument just above.