Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow void for destructuring forms and parameters #77

Closed
Jamesernator opened this issue Oct 27, 2021 · 8 comments
Closed

Allow void for destructuring forms and parameters #77

Jamesernator opened this issue Oct 27, 2021 · 8 comments

Comments

@Jamesernator
Copy link

Jamesernator commented Oct 27, 2021

This proposals adds using const void = to avoid creating a declaration. However other declaration forms such as destructuring would benefit from having an explicit "I'm ignoring this" variable.

i.e. With array destructuring we currently have to either add dummy variables or use "holes", in both cases this can be confusing (in the former names can be fairly arbitrary or meaningless, and holes are often indicative of mistakes).

The forms I think that would be benefit from allowing void to indicate that we are intentionally discarding are:

  • Array destructuring
    • e.g. const [void, dat] = someArray;
  • Object destructuring
    • e.g. const { x: void, y } = point;
  • Parameters
    • e.g. array.filter((void, idx) => idx % 2 === 0);

The other declaration forms let/class/function/catch either don't make sense to emit or can already be emitted.

@rbuckton
Copy link
Collaborator

I think that would be a separate proposal from this one.

@fstirlitz
Copy link

Funnily enough, I was wondering whether to offer an alternative syntax that would go the opposite way and get rid of void: instead of putting using before the declaration, place a delete keyword after — or instead of — the binding. (A modification of one I brought up earlier, inspired by a similar syntax in Lua 5.4.)

Might as well sketch it here:

Current syntax Alternative syntax
using const x = expr; const x delete = expr;
using const void = expr; const delete = expr;
using await const x = expr; const x await delete = expr;
using await const void = expr; const await delete = expr;
using const $temp$ = expr; const [a, b] = $temp$; const [a, b] delete = expr;
const [a, b] = expr; using const void = a; using const void = b; const [a delete, b delete] = expr;

Pros:

  • Can seamlessly accommodate destructuring, allowing e.g. to ergonomically encapsulate the revocable disposal idiom

    code example
    const hold = (value) => {
    	let released = false;
    
    	function release() {
    		released = true;
    		return value;
    	}
    
    	const result = [value, release];
    
    	Object.defineProperty(result, Symbol.dispose, {
    		get() {
    			const doDispose = value[Symbol.dispose];
    			return () => {
    				if (released) return;
    				doDispose.call(value);
    			};
    		}
    	});
    
    	Object.defineProperty(result, Symbol.asyncDispose, {
    		get() {
    			const doDispose = value[Symbol.asyncDispose] ?? value[Symbol.dispose];
    			return async () => {
    				if (released) return;
    				await doDispose.call(value);
    			};
    		}
    	});
    
    	return result;
    };
    
    const [valueA, releaseA] delete = hold(exprA);
    const [valueB, releaseB] delete = hold(exprB);
    
    return [releaseA(), releaseB()];
  • Reusing an existing hard keyword allows omitting the binding for anonymous disposal, making the syntax shorter by one token; it also relieves syntax highlighters of adding complicated special cases

Cons:

  • Even though it seems to me the most fitting of existing hard keywords, postfix delete/await delete is not particularly intuitive or memorable at first sight (and const delete =/const await delete = even more so): it makes sense if you stop to think about it, but it still has you stop and think. A new, clearer soft keyword would necessitate bringing void back and lose the advantage of having fewer tokens.
  • const await delete = expr;, if mistakenly used in a non-async function, would be valid syntax scheduling synchronous disposal for a binding named await

@shicks
Copy link

shicks commented Oct 29, 2021

I agree that if we're introducing void in this context, while it may be more self-consistent, it's deviating in consistency with the rest of the language. Allowing void elsewhere would recover that consistency.

@rbuckton
Copy link
Collaborator

I'm not sure delete makes sense in this context, since the normal delete operation will not trigger disposal. While this seems like an interesting idea to allow destructuring, it does result in significant repetition when declaring multiple disposable resources in the same declaration. Plus it seems like it would be hard to reason over something like:

const [{ x: { z } } delete, y delete] = ...

Just knowing where to put the deletes could become arbitrarily complex.

@fstirlitz
Copy link

I am not too attached to delete in particular; though if it has to be a keyword, it better be an existing one, as a new soft keyword will necessitate bringing back void (I thought about finally as an alternative). Punctuation may work just as well as a disposal marker. In fact, I think ~ would be pretty fitting, as it is evocative of C++ destructors (which this feature resembles to an extent) and since it is only one character, it will be very convenient to use, and therefore encourage adoption.

Current syntax Alternative syntax (~ variant)
using const x = expr; const x~ = expr;
using const void = expr; const ~ = expr;
using await const x = expr; const x await~ = expr;
using await const void = expr; const await~ = expr;
using const $temp$ = expr; const [a, b] = $temp$; const [a, b]~ = expr;
const [a, b] = expr; using const void = a; using const void = b; const [a~, b~] = expr;

Just knowing where to put the deletes could become arbitrarily complex.

That sounds like a documentation problem. I would expect an API reference to say things like ‘this call returns an object which should be disposed’ or ‘this call returns a wrapper object containing properties X, Y and Z, which should be disposed’. Which they probably should do anyway: as #69 points out, handing out a reference to an object does not always confer ownership transfer, so (in the absence of language mechanisms preventing access to the disposal method) programmers will have to rely on documentation to know of which objects they should dispose, and of which they should not. I suppose a type checker might be able to validate disposal as well.


† not equivalent if destructuring throws

@shicks
Copy link

shicks commented Feb 14, 2022

I like this idea. ~ is a pretty open part of syntax space, and the limited syntax expansion makes it still mostly available. The C++ analogue is a nice perk, and getting anonymous closers for free is also great.

FWIW, nearly any non-identifier symbol could go here, though for the reasons above I agree that ~ is a particularly good choice.

@rbuckton
Copy link
Collaborator

I'd prefer a keyword to a token like ~, as we have a very limited budget for non-keyword tokens in the language due to ASI hazards. There's also the issue that await is a legal identifier in the Script module goal outside of strict mode, so const await would be potentially ambiguous.

Also, the value of using a token like using is improving its discoverability in documentations, search engines, StackOverflow, etc. Reusing an existing keyword like delete would conflate the existing term and like result in confusion or difficulty in finding relevant learning materials.

As for allowing void in other destructuring forms, many of the initial examples already have a legal form:

  • Array destructuring
    • Proposed: const [void, dat] = someArray;
    • Existing: const [, dat] = someArray;
  • Object destructuring
    • Proposed: const { x: void, y } = point;
    • Existing: const { y } = point; or const { x: {}, y } = point;

For parameters, indicating a void parameter is interesting, but out of scope for this proposal.

@fstirlitz
Copy link

fstirlitz commented Jul 29, 2022

I'd prefer a keyword to a token like ~, as we have a very limited budget for non-keyword tokens in the language due to ASI hazards.

Seems a non-issue in this particular case. Since disposal is only allowed for const declarations, a semicolon inserted between the binding pattern and ~ is a guaranteed syntax error, because a const declaration requires an initial value. The same would be the case with a postfix keyword; it makes no difference what the postfix token is.

It is the current syntax which is more prone to ASI hazards, as a semicolon inserted between using and const will change the semantics of the code while keeping its validity.

Also, the value of using a token like using is improving its discoverability in documentations, search engines, StackOverflow, etc.

The tilde character has a name, which can be searched for in those search engines that ignore punctuation marks (which is not all of them). Failing that, the reader may look up the construct by searching for material about const declarations in general. The impact of picking a punctuation mark on searchability seems a bit overstated: it’s not like disambiguating the use of a common English word as a programming language keyword from its natural-language use (i.e. ‘declare a variable using const’ versus ‘declare a variable using const’) is all that trivial either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants