alshdavid/BorrowScript

[Proposal] Introduce shorthand ownership operators to reduce verbosity

Closed this issue ยท 9 comments

Currently the complete notation for describing an ownership operation could cause a lot of verbosity. This issue is here to track ideas for reducing verbosity.

Suggestions must remain in line with the project goal of ensuring the borrow checker concept is easy to read and understand and is backwards compatible with the longhand notation.

function foo(write bar: number) {}

One option to explore is the shortening of the operators:

Longhand Shorthand
move m
read r
write w
copy c
function foo(w bar: number) {}

I like.

Makes sense to me.

Make sense for me, but try to keep both ways of write it, for beginners the longhand notation is more readable, easy to understand.

FWIW, I don't think this is a good idea. You lose readability for a measly gain of 3-4 characters, and end up perilously close to Rust's "line noise" operators. I think a better approach is to make the ownership operators implicit whenever possible, as suggested in a different proposal.

Broadly, one of my ambitions for this project is to offer the advantages of Rust but with a time-to-productive competitive with a language like Go.

However, while I am keeping these optimisations in mind, at this stage it's premature to consider them seriously. My thinking is the priority is to get a language specification that is logical, even though it may be a bit verbose initially.

Rust's ownership is tricky to port to a language that's trying to avoid symbol overload (&, *, etc) and it might be impossible to simplify. So at this stage it's all about a language design that makes sense and can actually be built.

That said, I share your sentiment in that shortening the operators might not be the right answer.

If we look at languages like Go, people hate the verbosity - but it's also something that contributes to its readability. Conversely, trying to read through a JavaScript application that makes use of ternary statements and similar shortcuts can really negatively impact the code's "glanceability".

It might be annoying to type, but when you or someone else comes to read it later, that verbosity might make it quicker to understand.

The language is theoretical at this stage so we don't know how people will use it. It might be that we need to trial different shorthand variations before mainlining them.

I think a better approach is to make the ownership operators implicit

Potentially, yes.

If a consumer of a dependency defines what ownership permissions it expects from its parameters, then the caller does not really need to specify them when calling.

function fooRead(read foo: string) {}
function fooWrite(write foo: string) {}
function fooMove(move foo: string) {}
function fooMoveImplicit(foo: string) {}

function main (){
   const foo = 'Hello World'

   fooRead(read foo) // perhaps there is no need to be explicit?
   fooRead(foo)
   fooWrite(foo)
   fooMove(foo)
   fooMoveImplicit('Hello World 2')
}

It's also then possible to dive a level deeper and have the compiler understand how a variable is used within a function and omit the ownership operator entirely.

On the flipside, engineers will then need to inspect the implementation of the function in order to understand what ownership permissions that function wants. It might additionally limit possible future features (like getters/setters or proxies)

Say you go on a 12 month holiday then come back to this. What's better for readability?

Say you go on a 12 month holiday then come back to this. What's better for readability?

That's really hard to say -- ask me again in 12 months. ๐Ÿ˜ I think it depends heavily on how helpful compiler errors are. If the borrow checker is happy, I probably don't care what semantics a parameter is passed with. If the borrow checker signals an error and indicates the effective ownership operator at all conflicting sites, then perhaps that's good enough?

A different question to ask might be: does it make more sense to define ownership permissions at the declaration site or at the call site? My naive thinking is that it's a declaration-site concern, similar to how you declare arguments as const in C++. Are there good reasons to define functions with ambiguous argument ownership, where the caller decides how to pass a value?

So... this is a bit of a mixed bag. Languages that require explicit types on parameters often produce complex function signatures, and the addition of these ownership prefixes is going to make the parameters even longer. For example:

function lazy_initialise (write cache: Map<string, string[]>, read key, read url): Promise< string[]> {
  // ...
}

Swapping to a shorthand notation will help a little, but relying on a single character to provide additional meaning can be problematic.

Lets stop copying C: ! operator

That single ! is ridiculously subtle, which seems wrong to me when it makes an expression mean its polar opposite. Surely it should stick out like a sore thumb. The left parenthesis makes it worse, too; it blends in slightly as just noise.
It helps a bit to space after the ! [...]

Perhaps this isn't quite as bad, ! is a subtle character and our proposed syntax enforces a space. But it's worth consideration.

One thought that occurs to me is that the average person(or at least programmer) will be familiar with the terms r and w for read and write respectively. move is the implicit mode and there's an argument to be made that a copy prefix isn't required on a parameter. So perhaps you only need the shorthand options r and w ( or R/W might be clearer ).

Rust is a good example of requiring too much memorization and makes it difficult for beginners to remember what all the symbols mean (&, *, etc). Especially when these symbols already have means in another context.

Likewise, the shorthand for read, write, copy, and move leans towards the confusion of their true meaning, especially when these symbols have meaning in another context such as file permissions.

What do you gain? A few less letters to type, but in turn add confusion. The tradeoff is not worth it.

I think keywords should be short and explicit in their meaning. Otherwise, why not just build the entire language to use shortcuts for all keywords:
var -> v
let -> l
const -> cn
if -> i
else -> e
async -> a
while -> wl

The reason we do not is that we need to read the language efficiently, and having to learn all these symbols to remember basically brings us into the realm of symbolism and almost learning an entirely new written language to understand our code.

As for implicit, since your inspiration is TypeScript, the JS world and TS world are used to implicit parameters. I am not a super fan of implicit and do my best to be explicit as possible. However, with the right IDE tools or linter, the implicit becomes explicit. So it is quite possible to allow for implicit but support explicit behavior.

My opinion is that because borrowing is such an abstract concept (when reading the code) that you should require explicit use of the terminology read, write, copy, and move when writing the function signature, but not require it when calling the function.

So

function fooRead(read foo: string) {} 
function fooWrite(write foo: string) {}
function fooMove(move foo: string) {}
function fooMoveImplicit(foo: string) {} // <- not allowed

This way you solve the problem of "move" being the default because there is no default, it must be explicit.

I think this is a very bad idea.

When you scan through text on a page, words automatically stand out in your mind - whereas abbreviations and "certain letters in specific places" requires the reader to parse and make inferences from context, effectively slowing you down.

I'm generally opposed to keyword abbreviations - an optimization for writing rather than reading code.

You are going to read probably hundreds of times more code in your life than you're going to write.

Being able to read code is an absolutely critical property of any programming language.

Please don't sacrifice that in favor of saving a few keystrokes.