4 ms·
As an ex-clojure dev that picked up Rust, I have no interest in this as long as it doesn't have ownership. Once you used lexical scoping, you're not going back
by j-pb 2y ago
As an ex-clojure dev that picked up Rust, I have no interest in this as long as it doesn't have ownership.
Once you used lexical scoping, you're not going back to dynamic scope either, the same goes for the ability to reason about "where an object currently is" lexicographically.
I think Carp (https://github.com/carp-lang/Carp https://github.com/carp-lang/Carp) is a much more interesting contender for a native clojure, simply because it has lexical ownership.
- Guthur 2y agoI think if you're overly using dynamic scoping in clojure you are probably doing it very wrong. I used clojure professionally for a number of years and I can't remember ever really using it.
- mrkeen 2y agoParent's not talking about dynamic scoping in clojure. It's a comparison to designed to elicit a reaction, e.g. "probably doing it very wrong". I.e. if you're not tracking ownership in new-lang, you're probably doing it very wrong.
- j-pb 2y agoIt's not a comparison to bash non-ownership as "very wrong", but arguing that lexical scope and ownership are literally the equivalent things. Lexical scope allows you to reason about scope on a syntactical level. Take this code: let x = 5; fn foo(): return x \* 2 fn bar(): let x = 10 return foo() fn baz(): let x = "hello world!" return foo() bar() With lexical scope, you can look at `foo` and you know immediately what it returns, the behaviour of the function is completely local to it and is a syntactical property of the program. With lexical scope you just don't know what's returned, for all you know x might not even be a number, but could be any type that just happens to be brought into scope. Ownership is similar, in that you make the existence of a value a local property. If a 'thing' is inside a variable then you can move it somewhere else, but it also means that it is no longer in that variable. And that tracking of where what is, is a syntactical property, just like with lexical scope. Clojure has software transactional memory. With ownership it wouldn't need half of the machinery (only that for rollback): let blocked_accounts = Set() let account_bob = new ExclusiveBankAccount(200) let account_alice = new ExclusiveBankAccount(600) // ExclusiveBankAccount cannot be copied or cloned fn transfer(source_acc, target_acc, ammount): if source_acc.deduce(amount.copy()): target_acc.deposit(amount.copy()) fn block(acc): let acc_identifier = acc.identifier.copy() // identifiers can be copied and are not exclusive blocked_accounts.put(acc) return acc_identifier fn unblock(acc): return blocked_accounts.take(acc) fn do_invalid_stuff(): let good_account = new ExclusiveBankAccount(200) let bad_account = new ExclusiveBankAccount(200) block(bad_account) transfer(bad_account, good_account) // <- this will fail because bad_account was moved by the block function and no longer exists here, it's invalid syntactically fn do_valid_stuff(): let good_account = new ExclusiveBankAccount(200) let bad_account = new ExclusiveBankAccount(200) let ident = block(bad_account) let restored_account = unblock(ident) transfer(restored_account, good_account, 100) The above code makes sure that: - you can only transfer funds between two accounts in a thread safe way - you can only transfer funds between accounts that are not blocked Simply by not allowing something like this on a syntactical level you get a much cleaner understanding of what your code does. After a while you're wondering why we allowed anything else in the first place. let x = thing(); let y = x; // the thing is moved from x to y here print(x) The fact that automatic memory management falls out of this is almost accidental, if you can track where a value is at any given time, you can also track the references to it, and the resources it uses. But that is not what truly makes it amazing, the fact that you can mentally think about digital objects as if they were physical objects is.
- mrkeen 2y agoOh right. Enlightening! It's interesting you mention STM. That's one of the reasons I only dabble in Rust instead of switching over to it completely. In do_invalid_stuff(), block(bad_account) transfer(bad_account, good_account) // <- this will fail This can only be determined syntactically if you and the compiler agree to stay on a single thread, right? I would expect STM to be the thing which safely bridges across threads - since it will realistically a customer will call transfer() but a bank manager will call block().
- vlovich123 2y ago> This can only be determined syntactically if you and the compiler agree to stay on a single thread, right? No. This will fail because in block the ownership of bad_account is lost. You’d need block(&bad_account) or block(&mut bad_account) to pass a reference while letting the transfer succeed. The only way the transfer would succeed silently like that is if Account implemented Copy (the compiler would automatically inject block(bad_account.clone())). This has nothing to do with threading (which Rust would still have protection mechanisms for). It would work identically if the block and transfer were member methods (i.e. bad_account.block(); good_account.transfer_from(bad_account)) since even member methods can be declared to be requiring ownership.
- mrkeen 2y ago> This has nothing to do with threading Right, but all the promises of the type system have to lead to a first-class multi-threading experience at some point. The safest concurrent program isn't a single-threaded program.
- vlovich123 2y agoIt doesn’t automatically solve every possible problem for you just like something like Lean doesn’t prevent you from writing buggy proofs. I’d argue it does lead to a first-class multi-threading experience within the systems programming domain it’s targeting. Even outside systems programming it can be quite surprisingly good and most incorrect code fails to compile to begin with. Is there a language that in your opinion offers a better multi-threading experience?
- Jeaye 2y agoCool, dude! I also dig Rust. Carp is a very neat project, though it's not a Clojure. It's just Clojure-like. jank is Clojure.
- j-pb 2y agoThis wasn't meant as a jab at Clojure (or Jank for that matter). That language and its ecosystem has heavily shaped and influenced me early on in my career to the point where I'm essentially writing Datascript in Rust (https://trible.space https://trible.space). My point was that ownership has become a language feature that I expect, like lexical scope has become a language feature that most people expect. It complements persistent immutable data-structures extremely well, especially in terms of performance via automatic transient-ication, and if you're operating in the native space I can't imagine that performance isn't on top of the list. So in that sense, great language, please consider evolving beyond Clojure (archaic?) ownership model, because I'm pretty tired of Rusts bloated syntax and lovecraftian macro system.
- widdershins 2y agoI'm not an expert in Clojure or Rust, but an interested outsider in both, so please don't take my question as a challenge but a genuine curiosity: Doesn't the focus on immutable data make most of the ownership stuff redundant? My understanding was that ownership lets you be sure that you're the only mutable owner of a resource, or otherwise that you share the resource immutably. If data is (almost) always immutable, which seems to be a founding principle of Clojure, then what does ownership/borrowing buy you?
- oldpersonintx 2y ago[dead]
- j-pb 2y agoI gave a long-ish answer to a sibling comment, that maybe sheds some light on that question, but I'll try to also give a concrete example where ownership complements immutability. (You best read that response first before continuing here.) You are right in that immutability or more precisely the persistence that copy-on-write brings somewhat solves many of the issues that ownership also solves. Persistence allows you to not care as much about who owns what, because whenever something is modified it is simply copied (with structural sharing), so you know that whatever you have is local anyways, not because you are the only one who has exclusive access to the thing, but because you make a copy of it at the point where answering that question would matter. But you miss out on two things from that: - Performance: Those copies are cheap but not free. Borrowing complements _persistent_ immutability in that regard extremely well, because it allows you to check if you're currently the only one using something and modify it mutably in that case. Clojure has a concept called transients, where you create an intermediately mutable version of an immutable thing to perform batch updates, but you still have to manage that yourself, with ownership it happens magically behind the scenes. - Exclusive types: The other comment I wrote contains an example of this. I have an `ExclusiveID` type in my Database that is similar to a `row_id`. I can recover a lot of the capabilities of transactions and software transactional memory from these, simply because a user can rely on `ExclusiveID`s existing only at a single point at a time in their code. And `ExclusiveID` itself is a completely immutable thing, but the "contract" on how it can be used is what makes it powerful. Immutability on a domain logic level also does not help you when it comes to dealing with inherently immutable things like devices and networks, whereas ownership does. So I would argue that those two concepts are complementing each other extremely well, both implementation wise, but also in terms of a the mental model of how your code works.