Hacker News new | ask | show | jobs
by happens 836 days ago
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.

22 comments

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.
If it's syntactically messy, though, it's not really the easy path. Ergonomics matter just as much as semantics.

I do think that a superset of Rust that provided first-class native syntax for ARC would be much more popular.

Yes! Thank you! Dunno what it is about Rust that makes everyone forget what premature optimization is the root of all of.
The zero cost abstraction is so tantalizingly close enough to reach!

I tell everybody to .clone() and (a)rc away and optimize later. But I often struggle to do that myself ;)

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 only distinction between Arc and Rc is that the former is very slightly more expensive, but the latter is not thread-safe.

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

An Arc is an Rc that uses an atomic integer for its ref count. This ensures updates to its count are safe between threads, so the Arc can thus be shared between threads. In practice the two become identical assembly, at least on amd64 because most load and stores have memory ordering guarantees, but on other architectures the atomics can in fact be a tad slower. marking the operations as atomic also prevents the compiler from doing instruction reordering that might cause problems
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.

This conversation could be referring to https://fable.io/

Other than that, the question is indeed strange and I agree with your statements.

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

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.

You have awoken the ocaml gang
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/

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.
F# has better syntax but is ignored. :(
> Frankly, if it had a more familiar syntax but the same semantics

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
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.
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.
Yeah, I like the underlying ideas and I can deal with the syntax, but I wouldn't expect anyone else to :-/
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.

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.

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)".
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.
> 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

The problem is when it doesn't complain but instead infers some different type that happens to match.

I dabbled a bit with ReasonML which has global type inference, and the error messages from the compiler became very confusing. I assume that's a big reason for not gaining more adoption.
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.
In my view you have compared it to the only other language for which it is small by comparison :) But different strokes for different folks! I have nothing against Scala, its multi-paradigm thing is cool and impressive, it just isn't for me except by way of curiosity.
Could you list all the features you are thinking of?
I think all the links in the first two sections in the What Is Scala[0] docs give the flavor pretty well. It contains a full (and not small) set of OO language functionality, alongside an even more full-featured functional language.

There are a lot of adjectives you can use to describe Scala - mostly good ones! - but "small" just isn't one of them.

0: https://docs.scala-lang.org/tour/tour-of-scala.html#what-is-...

It's a personal preference but I'm not a big fan of JVM languages - big startup costs and not having one "true" runtime that is just compiled into the binary are my main reasons. I've spent so much time fiddling with class paths and different JRE versions...
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.

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.
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.
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.
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)
> The whole point of rusts type system is to try to ensure safe memory usage.

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

> It is the first and foremost goal of every language choice in rust.

It ... just ... isn't, though.

I mean, I get what you're saying, it's certainly foundational, Rust would look incredibly different if it weren't for that goal. But it just isn't the case that it is "the first and foremost goal of every language choice in rust".

I followed the language discussions in the pre-1.0 days, and tons of them were about making it easier and more ergonomic to create correct-if-it-compiles code, very often in ways that had zero overlap with safe memory usage.

Traits don't "muddy that goal", they are an important feature of the language in and of themselves. Same thing with the way enums work (as arithmetic data types), along with using Option and Result for error handling, rather than exceptions. Same thing with RAII for tying the lifecycle of other resources to the lifecycle of values.

The memory safety features interact with all these other features, for sure, and that must be taken into account. But there are many features in the language that exist because they were believed to be useful on their own terms, not in subservience to safe memory usage.

And it's not just about "providing types and generics to the language", it's a whole suite of functionality targeted at static correctness and ergonomics. The ownership/lifetime/borrowing system is only one (important!) capability within that suite.

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.
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.
That's interesting - so you used Arc even if you didn't need thread safety?

Lifetimes elision works pretty well so you don't often need to specify lifetimes

It usually pops up when you use generics / traits (what concrete type does it match to?)

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.
For Arc/Rc? I don't recall that! What was it? I recall it being `&borrowed`, `~boxed`, `@garbage_collected`.

Aaaah, I'm realizing in typing this that the `@foo` syntax was actually implemented via reference counting? I think my intuition at the time was that the intention was for those to eventually be backed by a mark-and-sweep GC, which I did think was a poor fit for the rest of the language. But as just a syntax for reference counting, I honestly think it might have been an ok fit.

Or maybe not, I'm ambivalent. But the syntax thing in my comment is more of a red herring for what I think is more of a cultural "issue" (to the small extent it is an issue at all), which is that most Rust projects and programmers seem to try to write in a style that defaults to only choose reference counting when they must, rather than using a style of optimizing them out if they show up in a hotspot during profiling.

Yes, I’m referring to @foo, which IIRC maybe in the VERY old days had a GC but from when I got involved in 2012 was reference counting, iirc.

Regardless of the specifics here, the same problems apply. Namely that it privileges specific implementations, and makes allocation part of the language.

@gc references were Arc under the hood!
Totally makes sense! Not sure if I never knew or if that knowledge got lost in the sands of the past decade (or more, I think?) of time.
By "rusts type system" I mean enums with exhaustive pattern matching and associated structs, generics, conventional option and results types, and so on. None of that necessarily has anything to do with lifetimes as far as I understand.
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!

Take a look at Gleam!

For me it seems like the perfect match.

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...

> the kind of language Rust wanted to be

That has changed through the years: https://graydon2.dreamwidth.org/307291.html

The @blah references were actually just Arc sugar
The expressive type system of Rust is backed by use-site mutability; use-site mutability is backed by single ownership; single ownership is made usable by borrow checking. There's a reason no language before Rust has been like Rust without being a functional language (and if that's no object, then you can use OCaml).
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.
> but in the end it's adoption and the ecosystem that keeps me from using them.

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.

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.

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 might like Kotlin. It'll also give you access to the entire JVM ecosystem.
I've written a lot of kotlin and it does indeed come very close! Now if only it wasn't bound to java's bytecode under the hood...

Whenever I've had to write kotlin for Android in the past I did quite enjoy it. It seems like the entire ecosystem is very enterprise-y when it comes to web though. Forced adherence to object orientedness and patterns like 100 files, 5 folders deep with 10 lines of code each keep cropping up in most kotlin projects I've seen.

Is that a blessing or a curse?
A blessing. Do you really want to write all the libraries from scratch for a new language? Do you want to come up with portable abstractions that work well on Windows? (and don't think you can skip that, people will ask).

Most people don't. That's not the fun part of language design.

Isn't that just the Boehm GC with regular Rust?
checkout Gleam.
... so OCaml or StandardML then
I do like the underlying ideas, and OCaml has been on my radar for a while. However, from my experience, functional languages with a big F always tend to feel a bit too "academic" when writing them to gain enough mainstream adoption.

Imperative code with functional constructs seems like the most workable approach to me, which rust, go, and other languages like kotlin, crystal etc. all offer.

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#
use scala
Right? One day... sigh