alshdavid / BorrowScript

TypeScript with a Borrow Checker. Multi-threaded, Tiny binaries. No GC. Easy to write.
1.45k stars 16 forks source link

Why is `move` the default? #20

Closed mindplay-dk closed 2 years ago

mindplay-dk commented 3 years ago

The document says:

move is the default if omitted

I am wondering why?

I would have assumed read would be the default, as this would encourage functional programming - if somebody wants to create code with mutations, they would use copy, write or move too specify "how".

That is, read appears to be the one that stands out from the other three, all of which are about how to deal with changing values - which, in my opinion, would seem to make it the logical candidate for a default.

I'm totally open to being wrong on this. 😄

I approach this from the perspective of a Typescript developer with no Rust experience.

Coming from Typescript, I don't normally have to think of memory management, and honestly never expected I would care or even like the Rust memory model, so let me explain:

I view these annotations as more than just memory management - in fact, it's nice that they work for compile-time memory management, but what really interests me is the idea of making it explicit what a function can be expected to do with your variable.

It surprises me how much I like this idea, and how easy it is to understand - it never exactly clicked for me in Rust, where for some reason this seemed burdensome to me. I'm not sure why. I might not have been receptive at the time and maybe I owe Rust a second look. But this looks great in the context of Typescript. 👍

I would probably even have found this meaningful in a language that only enforces these rules at compile-time and doesn't need them for memory management - such as Typescript.

Anyhow, if there's a good reason why move is the default, maybe this should be explained in that section of the spec?

alshdavid commented 3 years ago

Hey mindplay-dk, thanks for the issue/question/comment I enjoyed reading through it.

I would have assumed read would be the default, as this would encourage functional programming - if somebody wants to create code with mutations, they would use copy, write or move too specify "how".

I will be improving the documentation to clarify this but ownership permissions are specified by the implementations and not the caller. Meaning, if you write a function, your function specifies the permissions it requires.

Imagine you wrote an implementation of map() where you return a new instance of an array.

// TypeScript
function map<T, U>(a: T[], cb: (value: T) => U): U[] { /* ... */ } 

// BorrowScript
function map<T, U>(read a: T[], move cb: (copy value: T) => U): U[] { /* ... */ } 

To break it down, our map function takes read access to an array, give the caller a callback which is given a copy to the item in that array.

If I was to call this function from main(), even if I had write access to the array, I could only ever give this function read access because it cannot take anything other than that.

When we talk about move being the default, we mean the default for function parameter declarations. So in the above:

function map<T, U>(read a: T[], move cb: (copy value: T) => U): U[] { /* ... */ } 

// you can remove "move" from "cb"
function map<T, U>(read a: T[], cb: (copy value: T) => U): U[] { /* ... */ } 

it never exactly clicked for me in Rust, where for some reason this seemed burdensome to me. I'm not sure why.

This is how a lot of people feel about Rust. It's not because the borrow checker is complex, it's because they have String and str, which are two different string types. Read access is & and write is &mut. There are smart pointers and all this low level stuff that's great for writing an operating system but really gets in the way when writing a web app - particularly when you didn't have to deal with this stuff before.

maybe I owe Rust a second look.

I would never discourage someone from learning more and if you do end up having a look at it, I hope that some of the ideas in this repo helped set the stage for you to pick Rust up quickly 🔢

I approach this from the perspective of a Typescript developer with no Rust experience.

A little background, I approached Rust after years of TypeScript because I wanted to use threads on the browser and not tear my (little remaining) hair out because of Web Workers.

When we look at native apps and how smooth they are, a lot of that is because they are multi-threaded. I wanted to bring that smoothness to my projects. I wanted to have applications that weren't 800kb of minified JavaScript.

Learning Rust, I really appreciated the variable ownership concepts and kept thinking "if only I could just write with TypeScript syntax".

mindplay-dk commented 3 years ago

When we talk about move being the default, we mean the default for function parameter declarations.

Yes, that's what I thought as well. 🙂

So in your example, someone who calls map will lose their own reference to cb, right?

And this is default for parameters?

The move effect is contagious, so I'm still not understanding why that would be the default - read, on the other hand, has no effects and makes things immutable by default, which seems like "a good thing".

I wanted to have applications that weren't 800kb of minified JavaScript.

Amen to that. 🙏

Learning Rust, I really appreciated the variable ownership concepts and kept thinking "if only I could just write with TypeScript syntax".

Deciding not to learn Rust, I kept thinking "if only it looked more like TypeScript" 😄

zzzachzzz commented 3 years ago

I was initially confused by move being the default. Then I remembered that it's the default in Rust, and in Rust we opt into read behavior with a non mutable reference &param. I think it could be helpful to include a section in the docs showing how BorrowScript's syntax maps over to Rust.

alshdavid commented 3 years ago

I have updated the main readme with details on how the move operator will function.

As mentioned above, the default move action is an immutable (read only) move where you would need to specify a mutable move.

function foo(bar: string) {} // "bar" is immutable but moved into "foo"
function foo(move<const> bar: string) {} // longhand form of the default

function bar(move<let> foo: string) {} // "foo" is moved in as mutable
mindplay-dk commented 3 years ago

Isn't const and let just local distinctions to state whether you intend to replace the value of a given symbol?

Why does the function need to specify what the recipient can do with their local symbol?

Or does let and const have somehow radically different semantics from JS/TS?

shortercode commented 3 years ago

The rust borrow checker has stronger restrictions on mutability as well as deep mutability restrictions. I'm unsure if the reasoning for this is related to concurrency or object lifecycles, but it's a good restriction to have because let's be honest JS const is very weak.

On Sat, 9 Oct 2021, 09:53 Rasmus Schultz, @.***> wrote:

Isn't const and let just local distinctions to state whether you intend to replace the value of a given symbol?

Why does the function need to specify what the recipient can do with their local symbol?

Or does let and const have somehow radically different semantics from JS/TS?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/alshdavid/BorrowScript/issues/20#issuecomment-939259462, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACFB5WPDHQJQBTHBBN5G5NLUF77G3ANCNFSM5FGCF6XA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

mindplay-dk commented 3 years ago

let's be honest JS const is very weak.

JS const works perfectly well for what I expect it to do - which is just to inform the reader whether they should expect a local symbol to get replaced or not. When I see const, I know I don't have to plow through the code to figure out what else ends up in that symbol.

Should a local declaration be able to enact restrictions on mutability on an object?

That seems like something the object should decide?

I don't even know how that would work? If an object has methods that mutate the object, and you make a local declaration saying this instance will be immutable - it still has that method, so now you get, what, compile-time or run-time errors for trying to do something the object said it would be able to do?

Would these constraints be able to taint as well? If so, it's beginning to sound a lot like Pony - a language with an extremely sharp learning curve, which kind of takes these ideas to the extreme. (I would hope that a language that borrows from TS would be more accessible to people without a computer science degree.)

Isaac-Leonard commented 3 years ago

I agree that having const act the same as it does in javascript would probably be preferable and then allow readonly modifiers on object properties like what typescript does now. Although I think having readonly be the default would probably be a better option and then allow the mut keyword from rust to be used to make object properties mutable. I also think that letting read be the default for function params makes more sense although that would depend on if you believe calling a callback function counts as reading it. Alternatively you could have the let and const keywords be used instead of write and read in function params. I also don't particularly understand the difference between move and write however so I could be off track.

Isaac-Leonard commented 3 years ago

On further thought I'm not actually sure how having const and mut modifiers would work exactly. Beyond threading issues with calling methods that modify data on const objects with mut properties there's also extra stuff to deal with as its compiled to binary. so String.push("hello"); would add raw byte data in memory but not actually modify any of the objects properties, I feel like this should be pulled into its own issue though however.

alshdavid commented 2 years ago

Or does let and const have somehow radically different semantics from JS/TS?

It works differently in BorrowScript. As you say const let in JS/TS refers to symbol assignment, but the value is mutable. In BorrowScript they are aliases for let and let mut - so value mutability.

const foo = 'Hi'
let bar = 'Hi'

foo.push('char') // This would be illegal as it modifies the value

is equivalent to

let foo = String::from("hi")
let mut bar = String::from("hi")