It's weird, I want pretty much the exact opposite of this: a language with the expressive type system and syntax of rust, but with a garbage collector and a runtime at the cost performance. Basically go, but with rusts type system.
I'm aware that there are a few languages that come close to this (crystal iirc), but in the end it's adoption and the ecosystem that keeps me from using them.
That sounds… bad?
The whole point of rusts type system is to try to ensure safe memory usage.
Opinions are opinions, but if I’m letting my runtime handle memory for me, I’d want a lighter weight, more expressive type system.
I like Rust’s type system just fine but for me it’s types combined with language features like matching that draw me to Rust. When I was still learning I made an entire project using Arc<> with no lifetimes at all and it was actually a great experience, even if it’s not the textbook way to use Rust.
Honestly, I think syntax for Arc (and/or Rc or some generalization of the two) and more "cultural" support for writing in that style would have benefitted rust back when 1.0 was being finalized. But I think the cow is out of the barn now on what rust "is" and that it isn't this.
Yes, if you think about it, it's a bit weird that async gets first syntactical class treatment in the language but reference counting does not. A similar approach of adding a syntactical form but not mandating a particular impl could have been taken, I think.
Same for Box, but in fact Rust went the opposite way and turfed the Box ~ sigil.
Which I actually feel was a mistake, but I'm no language designer.
Async has to get first-class treatment in the syntax because the whole point of it is a syntax-level transformation, turning control flow inside out. You can also deal with Future<> objects manually, but that's harder. A special syntax for boxed variables adds nothing over just using Box<> as part of the type, similar for Rc<> (note that in any language you'll have to disambiguate between, e.g. cloning the Rc reference itself vs. duplicating its contents, except that Rust does it without having to use special syntax).
Yeah, but personally I think Rc/Arc is more deserving of syntax than Box!
A long time ago, it did have specialized syntax! We fought to remove it. There’s a variety of reasons for this, and maybe it would make sense in another language, but not Rust.
I’m assuming by rust’s type system they mean without lifetimes. In which case it’s existed in lots of GC languages (OCaml, Haskell) but no mainstream ones. It isn’t really related to needing a GC or not.
You still want RAII and unique references, but rely on GC for anything shared, as if you had a builtin refererence counted pointer.
I do also believe this might be a sweet spot for a language, but the details might be hard to reconcile.
I haven’t used Swift so I might be totally wrong but doesn’t it work sort of like you describe? Though perhaps with ARC instead of true GC, if it followed in the footsteps of Objective-C.
Possibly, yes. I haven't used swift either though. Does it have linear/affine types?
Edit: I would also prefer shared nothing parallelism by default so the GC can stay purely single threaded.
Without lifetimes, Pins, Boxes, Clone, Copy, and Rc (Rc as part of the type itself, at least)
Rust's type system prevents bugs far beyond mere memory bugs. I would even go as far as claiming that the type system (together with the way the standard library and ecosystem use it) prevents at least as many logic bugs as memory bugs.
The type system was built to describe memory layouts of types to the compiler.
But I don’t think it prevents any more logic bugs than any other type system that requires all branches of match and switch statements to be implemented. (Like elm for example)
It prevents a lot more than that. For example, it prevents data race conditions through Send/Sync traits propagation.
Besides preventing data races (but not other kinds of race conditions), it is not at all unique. Haskell, OCaml, Scala, F# all have similarly strong type systems.
It isn't though. The whole trait system is unnecessary for this goal, yet it exists. ADTs are unnecessary to this goal, yet they exist. And many of us like those aspects of the type system even more than those that exist to ensure safe memory usage.
It is the first and foremost goal of every language choice in rust.
I think traits muddy that goal, personally, but their usefulness outweighs the cost (Box<dyn ATrait>)
I should’ve probably said “the whole point of rusts type system, other than providing types and generics to the language”
But I thought that went without saying
The whole reason I got interested in Rust in the first place was because of the type system. I viewed it as "Haskell types but with broad(er) adoption". The fact that it also has this neat non-GC but memory safe aspect was cool and all but not the main sell for me.
If you do not want to mess with Rust borrow checker, you do not really need a garbage collector: you can rely on Rust reference counting. Use 1.) Rust reference-counted smart pointers[1] for shareable immutable references and 2.) Rust internal mutability[2] for non-shareable mutable references checked at runtime instead of compile time. Effectively, you will be writing kind of verbose Golang with Rust's expressiveness.
[1] https://doc.rust-lang.org/book/ch15-04-rc.html
[2] https://doc.rust-lang.org/book/ch15-05-interior-mutability.h...
A language has a paved road, and when you go off of that road you are key with extreme annoyance and friction every step of the way.
You’re telling people to just ignore the paved road of Rust, which is bad advice.
No, not really. Firstly, there is no significant "friction" to using Rust smart pointers and internal mutability primitives, as those constructs have been added to Rust for a reason: to solve certain borrow checker edge cases (e.g., multiply interconnected data structures), so they are treated by the Rust ecosystem as first-class citizens. Secondly, those constructs make a pretty good educational tool. By the time people get to know Rust well enough to use those constructs, they will inevitably realize that mastering the Rust borrow checker is just one book chapter away to go through out of passion or boredom.
I find quite a lot of friction in being demanded to understand all of the methods, what they do, when you’d use them, why you’d choose one over another that does a slightly different thing, but maybe still fits.
The method documentation alone in reference counting is more pages than some entire programming languages. That’s beside the necessary knowledge for using it.
I don't think it's necessary to understand every single `Rc<T>` method[1] to use Rust smart pointers to learn Rust. Perhaps try a different learning resource such as "Rust By Example"[2], instead?
[1] https://doc.rust-lang.org/std/rc/struct.Rc.html
[2] https://doc.rust-lang.org/rust-by-example/std/rc.html
Reference counting and locks often is the easy path in Rust. It may not feel like it because of the syntax overhead, but I firmly believe it should be one of the first solutions on the list, not a last resort. People get way too fixed on trying to prove to the borrow checker that something or another is OK, because they feel like they need to make things fast, but it's rare that the overhead is actually relevant.
I strongly disagree that smart pointers are "off the paved road". I don't even care to make specific arguments against that notion, it's just a terrible take.
It's telling people to avoid the famously hard meme-road.
Mutexes and reference counting work fine, and are sometimes dramatically simpler than getting absolutely-minimal locks like people seem to always want to do with Rust.
This is what Swift does, and it has even lower performance than tracing GC.
(To be clear, using RC for everything is fine for prototype-level or purely exploratory code, but if you care about performance you'll absolutely want to have good support for non-refcounted objects, as in Rust.)
An interesting point, but I would have to see some very serious performance benchmarks focused specifically on, say, RC Rust vs. GC Golang in order to entertain the notion that an RC PL might be slower than a GC PL. Swift isn't, AFAIK, a good yardstick of... anything in particular, really ;) J/K. Overall PL performance is not only dependent on its memory management, but also on the quality of its standard library and its larger ecosystem, etc.
Can you help me understand when to use Rc<T> instead of Arc<T> (atomic reference counter)?
Edit: Googled it. Found an answer:
The distinction between `Rc<T>` and `Arc<T>` exists in the Rust world only to allow the Rust compiler to actually REFUSE to even COMPILE a program that uses a non- thread-safe primitive such as a non-atomic (thus susceptible to thread race conditions) reference-counted smart pointer `Rc<T>` with thread-bound API such as `thread::spawn()`. (Think 1-AM-copy-and-paste from single-threaded codebase into multi-threaded codebase that crashes or leaks memory 3 days later.) Otherwise, `Rc<T>`[1] and `Arc<T>`[2] achieve the same goal. As a general rule, many Rust interfaces exist solely for the purpose of eliminating the possibility of particular mistakes; for example, `Mutex<T>` `lock()`[3] is an interesting one.
[1] https://doc.rust-lang.org/rust-by-example/std/rc.html
[2] https://doc.rust-lang.org/rust-by-example/std/arc.html
[3] https://doc.rust-lang.org/std/sync/struct.Mutex.html
You have awoken the ocaml gang
Yeah, ocaml is awesome! Frankly, if it had a more familiar syntax but the same semantics, I think its popularity would have exploded in the last 15 years. It's silly, but syntax is the first thing people see, and it is only human to form judgments during those moments of first contact.
That's what ReasonML is? Not quite "exploding" in popularity, but perhaps more popular than Ocaml itself.
Interesting! I'm actually unaware of this, but will look into it.
Don't forget ReScript
Funny, because the semicolons and braces syntax is one of the things that puts me off Rust a bit, and I was not excited to see it in Dada
It isn't necessarily my preference either, but it's the most familiar style of syntax broadly, and that matters more for adoption than my personal preferences do.
Syntax in programming languages are a question of style and personal preference. At the end of the day syntax is meant to help programmers communicate intent to the compiler. More minimalist syntax trades off less typing and reading for less redundancy and specificity. More verbose and even redundant syntax is in my opinion better for languages, because it gives the compiler and humans "flag posts" marking the intent of what was written. For humans, that can be a problem because when there are two things that need to be written for a specific behavior, they will tend to forget the other, but for compilers that's great because it gives them a lot of contextual information for recovery and more properly explaining to the user what the problem was. Rust could have optional semicolons. If you go and remove random ones in a file the compiler will tell you exactly where to put them back. 90% of the time, when it isn't ambiguous. But in an expression oriented language you need a delimiter.
F# has better syntax but is ignored. :(
That is probably the closest, especially if they add ownership. That was the rust inventor's original goal, not just safety at minimal performance cost. I think ownership should be a minimal requirement for any future language, and we should bolt it on to any that we can. Fine grained permissions for dependency trees as well. I like static types mostly because they let me code faster, not for correctness, strong types certainly help with that though. Jit makes static types have some of the same ergonomic problems as dynamic ones though. I think some sort of AGI enslaved to do type inference and annotate my code might be ok, and maybe it could solve ffi for complex types over the c abi while it is at it.
There's no ownership concept, but in the JaneStreet fork, there is something resembling lifetimes[1].
[1]: https://blog.janestreet.com/oxidizing-ocaml-locality/
You might enjoy F#. It's a lot like OCaml (which others have mentioned) but being part of the .NET ecosystem there are libraries available for pretty much anything you might want to do.
Yes, F# is an often forgotten gem in this new, brighter cross-platform .NET world. :)
:-) Is F# a contender outside the .NET world?
What do you mean by "outside the .NET world"? F# is a .NET language (more specifically a CLR language). That question seems to be like asking "are Erlang and Elixir contenders outside of the BEAM world?" or "is Clojure a contender outside of the JVM world?".
F# being on top of the CLR and .NET is a benefit. It is very easy to install .NET, and it comes with a huge amount of functionality.
If you're asking if the language F# could be ported to another VM, then I'd say yes, but I don't see the point unless that VM offered similar and additional functionality.
You can use F# as if C# didn't exist, if that's what you mean, and by treating .NET and CLR as an implementation detail, which they effectively are.
You are generally right, but Clojure is a bad example, it is quite deliberately a “hosted” language, that can and does have many implementations for different platforms, e.g. ClojureScript.
Yea, that's true. I forgot about that. I did think of Clojure CLR, but I don't get the impression that this is an all that natural or used implementation so I ignored it. ClojureScript is obviously much more used, although it is still a "different" language.
https://github.com/clojure/clojure-clr
This conversation could be referring to https://fable.io/
Other than that, the question is indeed strange and I agree with your statements.
There aren't many languages that can do server-side and browser-side well. F# is one of them!
Non .NET server-side?
You can do Node.js with F#
But these days .NET is a great server-side option. One of the fastest around, with a bit of tuning.
I've always wondered if global type inference wouldn't be a game changer. Maybe it could be fast enough with caching and careful language semantics?
You could still have your IDE showing you type hints as documentation, but have inferred types to be more fine grained than humans have patience for. Track units, container emptiness, numeric ranges, side effects and idempotency, tainted values for security, maybe even estimated complexity.
Then you can tap into this type system to reject bad programs ("can't get max element of potentially empty array") and add optimizations (can use brute force algorithm because n is known to be small).
Such a language could cover more of the script-systems spectrum.
one of the other reasons global inference isn't used is because it causes weird spooky action at a distance - changing how something is used in one place will break other code.
I've heard that, but never seen an example*. If the type system complains of an issue in other code after a local change, doesn't that mean that the other code indeed needs updating (modulo false positives, which should be rarer with granular types).
Or is this about libraries and API compatibility?
* I have seen examples of spooky-action-at-a-distance where usage of a function changes its inferred type, but that goes away if functions are allowed to have union types, which is complicated but not impossible. See: https://github.com/microsoft/TypeScript/issues/15114
Try writing a larger OCaml program and not using interface files. It definitely happens.
I've never used OCaml, so I'm curious to what exactly happens, and if language design can prevent that.
If I download a random project and delete the interface files, will that be enough to see issues, or is it something that happens when writing new code?
If you delete your interface files and then change the type used when calling a function it can cascade through your program and change the type of the function parameter. For this reason, I generally feel function level explicit types are a fair compromise. However, making that convention instead of required (so as to allow fast prototyping) is probably fine.
The problem is when it doesn't complain but instead infers some different type that happens to match.
Type inference is powerful but probably too powerful for module-level (e.g. global) declarations.
Despite type systems being powerful enough to figure out what types should be via unification, I don't think asking programmers to write the types of module declarations is too much. This is one area where forcing work on the programmer is really useful to ensure that they are tracking boundary interface changes correctly.
People accept manually entering types only at a relatively high level. It'd be different if types were "function that takes a non-empty list of even numbers between 2 and 100, and a possibly tainted non-negative non-NaN float in meters/second, returning a length-4 alphanumeric string without side effects in O(n)".
... so OCaml or StandardML then
Or Haskell!
Ocaml, yes, but not haskell. It does include these things the parent wants, but similar to how Rust ends up being quite "captured" by its memory semantics and the mechanics necessary to make them work, haskell is "captured" by laziness and purity and the mechanics necessary to make those work.
Also, syntax does actually matter, because it's the first thing people see, and many people are immediately turned off by unfamiliarity. Rust's choice to largely "look like" c++/java/go was a good one, for this reason.
I learned SML/NJ and OCaml a bit over 20 years ago and liked them, but when I tried my hand at Haskell my eyes glossed over. I get its power. But I do not like its syntax, it's hard to read. And yes, the obsession with purity.
Exactly right. I quite like haskell in theory, but in practice I quite dislike both reading and writing it.
But I like ocaml both in theory and practice (also in part due to having my eyes opened to SML about 20 years ago).
I actually preferred SML/NJ when I played with writing it, but OCaml "won" in the popularity contest. Some of the things that made OCaml "better" (objects, etc.) haven't aged well, either.
Still with OCaml finally supporting multicore and still getting active interest, I often ponder going back and starting a project in it someday. I really like what I see with MirageOS.
These days I just work in Rust and it's Ok.
or F#
TypeScript maybe?
If we are going that far, I suggest hopping off just one station earlier at Crystal-lang.
Yep, I think Crystal is the thing that is making a real go at essentially this suggestion. And I think it's a great language and hope it will grow.
Do you know how Crystal compares with Haxe? That's another one that might fit the requirements nicely.
I don't understand the Haxe documentation but it seems to also have some kind of algebraic data type.
Maybe ReScript?
You’ve just described scala.
Ha, no. Scala does contain this language the parent described, but alongside the huge multitudes of other languages it also contains.
Scala is an absolutely small language. It is just very expressive, but its complexity is quite different than, say, Cpp’s, which has many features.
You might like Kotlin. It'll also give you access to the entire JVM ecosystem.
Is that a blessing or a curse?
The funny thing is that rust used to have things like garbage collection. For the kind of language Rust wanted to be, removing them was a good change. But there could always be a world where it kept them.
https://pcwalton.github.io/_posts/2013-06-02-removing-garbag...
That has changed through the years: https://graydon2.dreamwidth.org/307291.html
checkout Gleam.
Right? One day... sigh
Totally agree! But I think it's a "both and" rather than an "either or" situation. I can see why people are interested in the experiment in this article, and I think your and my interest in the other direction also makes sense.
There are a bunch of languages that fit-the-bill already. F#, OCaml, Haskell and Scala all come to mind.
You might have to lose a few parens though!
Isn't that just the Boehm GC with regular Rust?
Kotlin scratches that itch well for me. My only complaints are exceptions are still very much a thing to watch, and ADT declarations are quite verbose when compared with more pure FP languages.
Still, the language is great. Plus, it has Java interop, JVM performance, and Jetbrains tooling.
Take a look at Gleam!
For me it seems like the perfect match.
Well, since you can't really use without high adoption even if something comes up with all features you want, you still won't be able to use it for decades or longer.
use scala
Isn't that F#?
Yeah, same for a scripting language too - something like Lua but as expressive as Rust.
There is Rune, but like you mentioned the issue is adoption, etc.