chapel-lang / chapel

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

`WeakPointer`: upgrade/downgrade interface design #20949

Open jeremiah-corrado opened 1 year ago

jeremiah-corrado commented 1 year ago

Background:

With a working weakPointer implemented, I'm opening this issue to decide what the final interface will look like w.r.t upgrading/downgrading. These terms are inherited from Rusts implementation, and have the following meaning the context of Chapel's shared memory management strategy:

Upgrade: convert a weakPointer into a shared object if the object's memory is still valid (i.e. there is at least one other shared instance keeping it alive). Otherwise do something else to indicate that the upgrade failed: throw, return nil

Downgrade: construct a weakPointer to some existing shared object

Prototype Design:

The prototype implementation currently has two ways of doing these operations.

upgrade/downgrade methods:

proc weakPointer.upgrade(): this.chpl_t? { ... }
proc shared.downgrade(): weakPointer(this.chpl_t) { ... }

The upgrade method returns nil if the pointer is no longer valid, or if the value of the shared object itself is nil.

These might be used as follows:

use WeakPointer;

var mc = new shared myClass();

// create a weakPointer to mc
var weak_mc = mc.downgrade();

// recover a new nilable shared object from 'weak_mc'
var maybe_valid = mc.upgrade(); // has the type 'shared myClass?'

Although there is some precedent for these names from Rust, these may be slightly confusing in the sense that it is not obvious whether they return a new object/pointer or mutate the receiver.

cast/constructor: weakPointer defines a cast to shared class?, and has a constructor that accepts a shared object.

These might be used as follows:

use WeakPointer;

var mc = new shared myClass();

// create a weakPointer to mc
var weak_mc = new weakPointer(mc);

// recover a new nilable shared object from 'weak_mc'
var maybe_valid = mc : shared myClass?;

There is also a cast from a weakPointer directly to a shared t. It throws a NilClassError when the pointer is invalid or the object is nil.

Design Questions:

With the above in mind, I have a few design questions:

bradcray commented 1 year ago

I think my intuition here is to use a method, in part because casting classes often seems slightly fraught (getting the invocation just right), and in part because of .borrow() on other class types, where this seems similar to me. That said, we also support myClass: borrowed as a means of getting a borrow to a class, which suggests that if we had syntax like weak C, say, we should probably support a cast like : weak for symmetry without having to say : weak C(int, 2)?, say.

As far as method names, I'm not opposed to the Rust names, and would be even more in favor of them if they had precedent across multiple languages (i.e., we shouldn't be different just to be different if these are the common terms for this idiom, like acquire and release for locks). I like that .borrow() and borrowed are more symmetrical and no-brainer-y and wish for something like that here as well, but have mostly come up short so far. .strengthen() and .weaken() are more related to the qualifier, but also a little odd (e.g., the thing it's being called on isn't being weakened, at least in place, which is what myC.weaken() suggests to me; whereas .borrow() as a verb makes more sense that a new thing is being returned). Hmm...

jeremiah-corrado commented 1 year ago

(e.g., the thing it's being called on isn't being weakened, at least in place, which is what myC.weaken() suggests to me; whereas .borrow() as a verb makes more sense that a new thing is being returned

I noted above that I have the same concern about upgrade/downgrade. These sound to me like they could mutate the receiver.

As far as method names, I'm not opposed to the Rust names, and would be even more in favor of them if they had precedent across multiple languages

Unfortunately there isn't much consistency across other languages as far as I can tell:

lang weak -> strong strong -> weak
Rust Rc::upgrade(&my_weak) my_rc.downgrade()
C++ my_weak.lock() weak_ptr's constructor
C# .Target field
plus a lossy cast to referenced type
WeakReference's constructor
Swift ? operator:
myWeak?.doSomething()
weak keyword:
weak let myWeak = ...
Python done implicitly via data structures?
see docs
weakref.ref(obj)

As of now, I'm leaning towards a pairing like getShared() and getWeak(). I'd also like to keep the constructor on weakPointer.

bradcray commented 1 year ago

getWeak() makes sense to me. getShared() seems OK, though somehow also not quite as intuitive to me since it feels more like a promotion to something bigger/stronger/more capable. Like, in English, my head goes to phrases like 'promote to shared' when going in this direction. That said, I don't know that that feeling is strong enough to not go with the simpler and more symmetric name. It does make me think that mySharedPtr: weak and myWeakPtr: shared seem pretty attractive as far as being short-and-sweet (but I still think we should probably have a method-based way of doing this as well).

jeremiah-corrado commented 1 year ago

During an offline meeting, we voted on what the "upgrading" interface should look like.

The initial proposal was to have an upgrade method, as well as two casts. Under this proposal, each of the following would be valid:

// method
if var s: shared C = myWeak.upgrade() {
    // use s
} else { ... }

// cast to nilable C
var s = myWeak : shared C?;
if s != nil {
    // use s
} else { ... }

// throwing cast to non-nilable C
try {
   var s = myWeak : shared C
   // use s
} catch : nilClassError { ... }

The results were as follows:

We also voted on the name of the upgrade method with the following results: