[HN Gopher] Removing garbage collection from the Rust language (...
___________________________________________________________________
Removing garbage collection from the Rust language (2013)
Author : mattrighetti
Score : 163 points
Date : 2023-09-11 10:23 UTC (11 hours ago)
(HTM) web link (pcwalton.github.io)
(TXT) w3m dump (pcwalton.github.io)
| raincole wrote:
| I think it's quite unfortunate that there isn't a language with
|
| 1) Rust's type system or something as expressive 2) C#-level
| performance 3) GC by default 4) Ecosystem that is as big as Rust
|
| The closest thing seems to be TypeScript (weirdly).
| wizwit999 wrote:
| yes something like kotlin + go would be excellent
| nu11ptr wrote:
| I think #1 needs to be expanded to imply what it DOESN'T have
| as well:
|
| 1. No null pointers 2. No exceptions
| evntdrvn wrote:
| F#
| chris_nielsen wrote:
| Not trying to be cheeky, but why not c#? Has pts 2, 3 and 4.
| For pt 1, what in Rusts type system are you looking for?
| jen20 wrote:
| Sum types, which can only be approximated in C# via records,
| and no exceptions, which is even a problem for F#.
| tazjin wrote:
| I don't know C#, so maybe it has these, but it's unlikely:
|
| - absence of null pointers
|
| - proper sum types which can be statically checked for
| exhaustiveness
|
| - statically controlled mutability (yes, borrow-checking is
| still useful if you have a GC!)
|
| Also no exceptions, but that's not a type system feature.
| seabrookmx wrote:
| C# calls the first one "nullable reference types." When
| that build option is enabled, all types are non-nullable by
| default, and you can make them optionally nullable by
| declaring them like "MyClass? cls = null;"
|
| The compiler will ensure you check for null before de-
| referencing a nullable type.
| tazjin wrote:
| How does this interact with third-party dependencies you
| use? In my experience, most gradual typing things break
| down at that boundary.
| raincole wrote:
| > Not trying to be cheeky, but why not c#?
|
| C# is my main language. I consider it a very good all-round
| language.
|
| Rust's type system has some advantages over C# tho, for
| example Sum Type, Option (C# has ? but it was added later so
| you need to be careful when interacting with old code, kinda
| like TypeScript <-> JavaScript to a lesser extent),
| exhaustive enum, etc.
|
| Another thing I don't like about C# is the runtime startup
| time which prevents me from using it for command line tools
| (Yes I prefer static typed languages even for "scripting"). I
| think Go has proven that you can have both GC and extremely
| fast startup time.
| digibeet wrote:
| Runtime startup might be a fixed issue soon for smaller CLI
| programs. Correct me if I'm wrong, but I believe they are
| working on (and it is already available with a compiler
| option) to compile your C# program ahead of time (C# AOT).
| MrBuddyCasino wrote:
| Tried Kotlin native?
|
| - sum types via sealed classes (not great, admittedly)
|
| - enum "when" expressions are exhaustive
|
| - option type via built-in nullable types (honestly the
| superior solution)
| [deleted]
| kirse wrote:
| _Rust 's type system has some advantages over C#_
|
| Pick up some F# then. Both can interop, I've built many
| things in a combo of the two languages, plus it'll make you
| a better C# developer and your type system power greatly
| increases. Be careful though you might not want to go back
| to C#.
| neonsunset wrote:
| Could you try a few sample scenarios for a CLI tool written
| in C# with the following publish options?
|
| JIT: dotnet publish -c release -o publish
| -p:PublishSingleFile=true -p:PublishTrimmed=true
|
| AOT: dotnet publish -c release -o publish
| -p:PublishAot=true
|
| Either one has really good startup time (below ~100ms and
| 20-30ms respectively depending on what you do), compact
| binary size and require no external dependencies. Just like
| in Go except without all shortcomings of Go :)
|
| p.s.: AOT on macOS requires .NET 8 preview (will be
| released in November)
| raincole wrote:
| Interesting, I remembered I've tried something similar to
| your JIT example but maybe my memory is playing tricks on
| me. I'll try these again later
| cb321 wrote:
| 20 milliseconds? On my 7 year old Linux box, this little
| Nim program
| https://github.com/c-blake/bu/blob/main/wsz.nim runs to
| completion in 109.62 +- 0.17 _micro_ seconds when fully
| statically linked with musl libc on Linux. That's with a
| stripped environment (with `env -i`). It takes more like
| 118.1 +- 1.1 microseconds with my usual 54 environment
| variables. The program only does about 17 system calls
| total, though.
|
| Additionally, https://github.com/c-blake/cligen makes
| decent CLI tools a real breeze. If you like some of Go's
| qualities but the language seems too limited, you might
| like Nim: https://nim-lang.org. I generally find getting
| good performance much less of a challenge with Nim, but
| Nim is undeniably less well known with a smaller
| ecosystem and less corporate backing.
|
| EDIT: I make only observations here, not demands. Another
| observation on the same machine is `python-3.11.5
| </dev/null` with an empty environment taking 7.85 +- 0.02
| ms.
| neonsunset wrote:
| 20ms as measured with `time` on Fish shell on macOS.
| Let's be real, comparing versus Nim here is the same as
| comparing with C - both are a much lower level languages
| and don't do as much work on startup (threadpool, GC,
| etc.) and, for the reference, 60fps is 16.6ms per frame.
| The difference is unlikely to be noticeable. And this
| isn't measuring program execution time but back to back
| console receiving a command to launch a binary, OS doing
| so, binary starting, doing useful work (displaying hint
| that the command was not in the correct format) and only
| then exiting.
| cb321 wrote:
| I agree this particular C# being >180x slower may not
| matter much for one-off commands keyed-in by and watched
| by humans, _but_ that may not be all that matters. E.g.,
| some people might `find . -print | xargs -n1 cmd`. Almost
| everything almost always "all depends". (On a whole lot.
| E.g., only @raincole can elaborate on his use cases and
| what might be missing from the Nim ecosystem.)
|
| EDIT: Also, it's misleading to bundle Nim with C and C's
| many & storied footguns. While "low-level" is somewhat
| subjective and you can opt-in to go as low as C ( _if_
| you so desire), most Nim code is as high-level as Python
| or C# with various choices in automatic memory
| management, and the language has very high-level
| capabilities. E.g., Nim has user-defined operators like
| Julia. Want to add `=~` or `~` for regex pattern
| matching? No problemo. In that aspect, Nim is arguably
| higher-level than C#.
| ReleaseCandidat wrote:
| > C# is my main language.
|
| Then the answer is F#. OCaml has everything except the big
| ecosystem. Haskell has a way bigger Ecosystem than OCaml,
| but is still not comparable to Rust.
| madeofpalk wrote:
| C#'s type system is nowhere near as expressive. Lack of sum
| types/discriminated unions and pattern matching/type
| refinement.
| John23832 wrote:
| Then what about F#?
| indeyets wrote:
| F# probably?
| skrebbel wrote:
| Yeah GP's points 1 through 4 seem like they could be on an F#
| pitch deck. It's an ML with full access to the .net ecosystem
| and the .net VM's speed and stability.
| mxz3000 wrote:
| we have a massive mixed c# and f# codebase at work.
|
| F# is not magical. Yes it's an ML, but frankly, C# is
| better in every way, to the point we're slowly moving away
| from F# entirely.
|
| The main reason is perf, it's really easy to shoot yourself
| in the foot with performance in c# (e.g. huge allocations,
| accidentally evaluating seqs twice, etc). Also IDE support
| for F# sucks when you get to the hundreds of project
| solutions like we have.
|
| For side projects, sure use F#. For everything else, stick
| with C#.
| lysecret wrote:
| Yes wanted to comment that as well.
| AndreasHae wrote:
| Kotlin ticks all of your boxes, plus you benefit from the vast
| ecosystem of the JVM.
| richbell wrote:
| It's unfortunate that Kotlin lacks pattern matching,
| especially now that Java has it. I can only hope that the
| release of the new compiler will spur further language
| features.
| simonkagedal wrote:
| What kinds of pattern matching that Java now has is Kotlin
| missing?
| richbell wrote:
| At the moment Kotlin only has smart casts and exhaustive
| type checking (e.g., making sure you didn't forget a
| switch case). It doesn't let you destructure records, add
| guards to cases, etc.
|
| https://www.infoq.com/articles/pattern-matching-for-
| switch/
| corethree wrote:
| Haskell has a better type system. In fact rusts type system is
| derived from ML languages ...
|
| Tons of languages have better type systems.
| sa-code wrote:
| Mojo could potentially fill this gap in a year. They still have
| a long way to go, but they're working on traits and sum types
| right now
| manicennui wrote:
| OCaml probably has everything but 4.
| manicennui wrote:
| Also just remembered that F# exists, which is probably
| exactly all of that and runs on the CLR like C#.
| thelittlenag wrote:
| Scala's type system is about as expressive as it gets in
| mainstream languages. By virtue of running on the JVM it is
| GC'd and has Java/C#-like performance. And the ecosystem of
| Scala implemented libraries is huge.
| qalmakka wrote:
| D (Dlang) checks 3 of those marks. If you like cursed things,
| Vala isn't that bad...
| ThomasTJdev wrote:
| Nim-lang fits 1, 2 and 3! And in a couple of years the
| ecosystem is up to level ;)
| faewjpofiajwifa wrote:
| Nim doesn't have sum types and pattern matching, which are an
| essential part of an expressive type system. It also appears
| to have `nil` be a valid value for most types by default?
| cb321 wrote:
| This comment is misleading &| misinformed.
|
| Sum types are built-in [1] for formal parameters. `nil` is
| only for `ref|ptr` types. In much code you can just use
| stack allocated value types and there is neither GC concern
| nor nil concern, but there is also a mode to help:
| https://nim-
| lang.github.io/Nim/manual_experimental_strictnot...
|
| Nim has an easy-ish to use Lisp-like syntax macro system
| where you just receive & process an AST. So, to do the rest
| you can make libraries adding the feature without relying
| upon upstream compiler: such as
| https://github.com/beef331/sumtypes for variables with sum
| types or pattern matching libs like
| https://andreaferretti.github.io/patty/ |
| https://github.com/alehander92/gara.
| cmrdporcupine wrote:
| Sounds like you want OCaml (or StandardML but OCaml is more
| "active")
|
| Since, as I feel it, Rust was/is started as an attempt to bring
| roughly the ML (Hindley-Milner) type system to the area of
| `systems` (non-garbage collected) development.
|
| In the early days of Rust I thought of it as type inference &
| algebraic data types meets C++ (now kiss!). But then the borrow
| checker stuff went in there and it took a different turn.
|
| And I think you'd be surprised by how large the OCaml package
| community is.
|
| People who work in Rust would likely find OCaml very familiar
| after a week or two of hacking.
|
| The other option is Swift, but it's pretty ghetto-ized into the
| Apple ecosystem.
| nvm0n2 wrote:
| Isn't Rust's type system mostly about not having GC? What
| specific type system features are you looking for outside of
| borrow checking? Macros?
|
| Probably the closest would be Kotlin. It has the huge JVM
| ecosystem, it uses GC, programs run fast, they can be compiled
| to native binaries using at least two different native
| compilers (kotlin/native and graalvm). The type system is
| fairly expressive, albeit not quite as much as TypeScript. It
| makes up for it with just being a much cleaner and more logical
| language in general, as it didn't inherit much historical
| baggage.
|
| There's also a compiler plugin API which is maybe the closest
| equivalent of macros. It's not really documented or stable in
| Kotlin 1.x, they're fixing that for Kotlin 2, but there are
| already a bunch of useful plugins that add various features via
| compile-time generation and reflection.
| veber-alex wrote:
| > Isn't Rust's type system mostly about not having GC? What
| specific type system features are you looking for outside of
| borrow checking? Macros?
|
| 1. ADTs with exhaustive pattern matching
|
| 2. No exceptions
|
| 3. Zero cost generics
|
| 4. Traits
|
| 5. No nulls
|
| 6. No inheritance
| fweimer wrote:
| Rust has exceptions. They are used throughout the standard
| libraries to report some errors, and as far as I can tell,
| they are deeply ingrained into the default testing
| framework.
|
| As a rule of thumb, languages that loudly claim not to have
| exceptions actually use them in some way (see POSIX C,
| Perl, Go).
| TwentyPosts wrote:
| Saying that Rust "has exceptions" is dishonest. What
| matters isn't whether they technically exist, but if and
| how they're used.
|
| In idiomatically written Rust you will never obtain an
| exception/panic (unless there is a bug), and exceptions
| are not used for control flow. This is not the case in
| Java or C++ or many other languages.
| skitter wrote:
| In JRE terms, Rust doesn't have Exceptions, but it has
| Errors.
| masklinn wrote:
| > Isn't Rust's type system mostly about not having GC? What
| specific type system features are you looking for outside of
| borrow checking?
|
| Sun types, affine types, traits.
| loeg wrote:
| OCaml?
| masklinn wrote:
| One issue with GC'd language and the associated promiscuity is
| it's quite hard to mix with affine or even linear types. Yet
| those turn out to be quite handy.
| rwmj wrote:
| OCaml or F#?
| losvedir wrote:
| Ha, I feel exactly the same. I asked HN about it a year ago[0],
| and there was some interesting discussion.
|
| [0] https://news.ycombinator.com/item?id=32984776
| fweimer wrote:
| What do you consider Rust's relevant properties in this
| context? Traits? Inner object references? Associated types and
| constants? Compilation tending towards monomorphization?
|
| C++/CLI covers some of these aspects, but I haven't used it and
| don't know how large the vcpkg ecosystem is.
| EwanToo wrote:
| I expected someone to write a rust-based scripting language
| which tightly integrated with rust itself.
|
| In reality, it seems like the python developers and toolchain
| are embracing rust enough to reduce the benefits to a new
| alternative.
|
| https://github.com/PyO3/pyo3
| BenoitP wrote:
| Java?
|
| * Type system is even accidentally Turing complete
|
| * Very good perf, but language doesn't help by being
| indirection-friendly. Value types will help a lot.
|
| * SOTA GCs
|
| * Ecosystem big
|
| * Cheap threads now. Don't do async! Just block.
|
| * Structured concurrency soonish
| kaba0 wrote:
| Or if someone really want to play around with types: Scala,
| especially Scala 3. People can't even say that it is not
| nullsafe, as it is.
| Zambyte wrote:
| I haven't used Scala since 2. Did something change related
| to null with 3? Can you no longer use null? I know it has
| the Option type which can be used to safely represent
| nullable values, but that is (or at least was) in addition
| to, rather than in replacement of null.
| kaba0 wrote:
| It has a compiler flag which will remove the null value
| from most type's sets, that is a String will never
| contain null, if you want it to be nullable you have to
| write `String | None` (or Null? Something like that).
| masklinn wrote:
| > Type system is even accidentally Turing complete
|
| A turing complete type system is easier to stumble into than
| to avoid.
|
| That doesn't mean the type system is expressive. Turing
| tarpits are turing complete by definition, and nobody would
| call Thue, Iota, or the average OISC expressive.
| dcuthbertson wrote:
| Excuse my ignorance (I have only just started learning Rust),
| but why is GC by default desirable? If a programming language
| can tell when a variable goes out of scope, or its lifetime
| ends and use that information to automatically release
| allocated memory, then why is having a garbage collector
| important? Does Rust (as a result of not having GC) put
| restrictions on the kind of code you can write, or force one to
| write code in such a way that it's difficult to reason about?
| Someone wrote:
| > If a programming language can tell when a variable goes out
| of scope, or its lifetime ends
|
| Whether a _variable_ goes out of scope is trivial in many
| languages. The problem is with determining whether the
| lifetime of a _value_ ends.
|
| For example: func foo(items, moreItems) {
| b = new bar() if coinflip() {
| items.append(b) } if coinflip() {
| moreItems.append(b) } // 'b' goes out
| of scope here, but its value // may live on
| inside 'items' and/or moreItems // and will have
| to be destroyed when it's no // longer stored in
| either. }
|
| In general, once you allow for dynamic allocation and
| references that can be copied (so that multiple objects
| 'know' of the allocated object), it can be very difficult to
| determine exactly when the last reference to such an object
| ceases to exist.
| nu11ptr wrote:
| Yes, it has a borrow checker which restricts some valid code
| and occasionally makes you jump through hoops and write in a
| different style.
|
| Also, a garbage collector that is state of the art (bump
| allocation, generational, and compacting) is faster overall
| typically (throughput-wise, but with less predictable
| latency) than naive Rc/Arc all over the place (due to usage
| of "free list" allocator and counter bumps). That isn't to
| say blisteringly fast Rust isn't possible to write, and in
| fact it is pretty easy to write by avoiding Rc/Arc except
| where necessary and using the stack with the borrow checker,
| but this entails a writing style that is more "low level" and
| thus takes a bit more thinking.
| HankB99 wrote:
| IANAGCE (I am not a GC expert.) I thought there were
| potential costs to using GS.
|
| 1. Interrupting the program to sweep, resulting in
| unpredictable performance.
|
| 2. Possibility that a circularly linked group of variables
| could be impossible to sweep, resulting in memory leaks.
|
| 3. Need to check reference counts (along with bounds
| checks) that degrade performance.
|
| Lack of GC (and bounds checking) are factors that make
| C/C++ performant and then lead to the kind of bugs that
| result in programs that don't do what they're supposed to
| do (and at worst, result in security vulnerabilities.)
|
| I thought a key goal of Rust was to fix these problems
| without sacrificing performance.
| nu11ptr wrote:
| > 1. Interrupting the program to sweep, resulting in
| unpredictable performance.
|
| See my comment regarding "less predictable latency"
| (although new advances are making for much more
| predictable latency - see some of the work done in Java
| GCs for example)
|
| > 2. Possibility that a circularly linked group of
| variables could be impossible to sweep, resulting in
| memory leaks.
|
| That is not possible in a precise tracing collector, only
| in reference counting (Rc/Arc)
|
| > 3. Need to check reference counts (along with bounds
| checks) that degrade performance.
|
| I think you are referring to reference counting again.
| Both Rust and C++ have reference counting types as an add
| on.
|
| State of the art tracing collectors don't collect/check
| dead objects, they compact live ones, and often don't use
| reference counting directly. Most objects die young.
|
| > Lack of GC (and bounds checking) are factors that make
| C/C++ performant and then lead to the kind of bugs that
| result in programs that don't do what they're supposed to
| do (and at worst, result in security vulnerabilities.)
|
| Replace "performant" with "predictable performance"
|
| > I thought a key goal of Rust was to fix these problems
| without sacrificing performance.
|
| Rust, like C/C++, wants you to be able to predict
| performance and latency and without the baggage of a
| runtime. Nothing more than trade offs - neither is better
| or worse. GCs often do perform better with the trade off
| of latency predictability, but as always it depends on
| use case as to what is appropriate. Rust likely made the
| right choice for its domain.
| kaba0 wrote:
| What kind of GC do you talk about here? (RC is also a GC
| algorithm, but you seem to mix some of its shortcomings
| with that of trading GCs).
|
| 1) this is generally true, but many part of this can be
| done concurrently, and if we want to improve latency at
| the cost of some throughput than there are low-lat GCs
| that maximize the pause times (it is basically
| independent from the heap size), so in practice you have
| similar interrupts as you would from the OS alone.
|
| 2) this is not a problem with tracing GCs, only with
| refcounting (most well known is Python, ObjC and Swift
| for this perhaps). You can still leak memory everywhere
| by e.g. storing them in a huge list forever, though, but
| that is a much rarer and easy to debug bug.
|
| 3) this is again only true for RC, and it is the reason
| why it is slower than tracing GCs, especially when it is
| multithreaded and the increment/decrement has to be an
| atomic operation.
| dcuthbertson wrote:
| Thanks for that explanation! I'm going to have to dive into
| Rc/Arc [0]. Reference counting built into the standard
| library is really interesting. Several years ago I wrote a
| Windows kernel minifilter and reference counting is used a
| lot by anything that interacts with the filter manager. RC
| was very helpful to ensure the minifilter didn't leak
| memory when it was unloaded.
|
| [0]: https://doc.rust-lang.org/std/sync/struct.Arc.html
| mhh__ wrote:
| With a GC you can basically write whatever you want with
| reckless abandon and because it's cleaned up at runtime, it's
| mostly kosher.
|
| Without a GC (i.e. rust), in order to be able to make
| guarantees about things that it cannot determine at compile
| time (this is a mathematical impossibility) it restricts the
| set of programs you can write.
| kaba0 wrote:
| > Does Rust (as a result of not having GC) put restrictions
| on the kind of code you can write
|
| Depending on what you mean by kind: yes. That is, Rust does
| limit your code's architecture to a subset that is fine for
| most things, but not everything.
| vaylian wrote:
| And only 2 years later Rust 1.0 was released with a borrow
| checker (which is not mentioned in the blog post). I think one of
| the reasons why Rust turned out to be so well-designed is that
| Rust has a very open development approach where people with many
| different experiences can share their wisdom. Many other
| languages follow the design of a few people, but Rust is really a
| language of the internet community.
| bachmeier wrote:
| > Rust has a very open development approach where people with
| many different experiences can share their wisdom
|
| Interesting take, but as someone that drifted away from Rust
| with the publication of the linked blog post, and wrote their
| last meaningful code in the language a year later, it does not
| describe my experience. YMMV, but compared to other languages,
| I'd say as a first approximation that they had no interest in
| comments from outside the inner circle.
| faitswulff wrote:
| > I think one of the reasons why Rust turned out to be so well-
| designed is that Rust has a very open development approach
| where people with many different experiences can share their
| wisdom.
|
| Probably also why Rust has such highly publicized drama, as
| well. The drama is out in the open, too.
| mhh__ wrote:
| Rust seems to attract people prone to drama (I have been
| privy to much private open source drama but nothing like what
| I see from rust in the open, at a glance at least)
| tensor wrote:
| I don't feel that Rust is actually well designed. The borrow
| checker and lack of GC makes it well suited to the tasks that
| traditionally one would use C or C++ for, and it is definitely
| safer than those language.
|
| But in terms of overall design it doesn't feel very cohesive,
| and priority is put in odd places. For example, error handling
| is an area where people almost always resort to 3rd party
| libraries. That, to me, having to rely on third party libraries
| for such basic features is a sign of a serious design flaw.
| Meanwhile, "clever" things like zero cost map and filter are
| prioritized and in the language for a long time.
|
| Overall it feels like a language that prioritizes "clever"
| features at the expense of boring but basic needs. Also any
| criticism is met with hostility rather than accommodation.
|
| It's a fine language, but I can't say that I love it.
| Klonoar wrote:
| _> For example, error handling is an area where people almost
| always resort to 3rd party libraries._
|
| IME people mostly resort to those libraries just to avoid
| verbosity, since implementing error types is frankly boring
| and macros/etc make it a non-issue.
|
| Some have neat context-attachment functionality that can be
| useful as well, though I've personally never needed it.
| zanellato19 wrote:
| > Meanwhile, "clever" things like zero cost map and filter
| are prioritized and in the language for a long time.
|
| This is because of what the language is trying to do.
| Providing map and filter with zero is a priority 0. I think
| people think Rust had more development manpower than it
| actually had. Java/C# (and I would bet Go) have had
| significantly more money poured into them than Rust.
| rhodysurf wrote:
| I would consider error handling in rust my favorite of all
| the lanuages i use by a long margin, im not sure how it would
| be improved. Python I have to try catch, JS i have to try
| catch, C++ i have to try catch, Go I need more boilerplate
| than rust. Swift is close to rust, but without the Map Error
| logic its less ergonomic.
| tensor wrote:
| An incredibly typical scenario is that a function makes
| several calls that each have their own error. E.g. maybe
| initialize the database and a web server.
|
| If you want this function to return an error that
| encompasses any of the sub-error types you are forced to
| write the most incredible amount of boilerplate that I've
| ever seen in a language. You need to define a composite
| error type and then implement all the required traits for
| it.
|
| I'm not sure how some people avoid this situation, but it's
| an incredibly common scenario and the only reasonable
| solution is to use a library like anyhow. Defining your own
| errors is also heavily boilerplate prone giving rise to
| things like thiserror.
|
| Why these third party libraries are not made into first
| class language features is beyond me, but it is an example
| of very poor design and is typical of what I mean. Rust
| implemented a solid error handling core, but then just
| didn't bother with the things that would make the error
| handling actually good. Fancy over pragmatic.
| thinkharderdev wrote:
| I don't understand what you would include in the std lib
| exactly. Defining error types is just defining regular
| types with domain-specific error information. Macros to
| make that less verbose seems like the exact sort of thing
| you would want to be implemented in third-party
| libraries.
| tensor wrote:
| Except as a newcomer to the language, I didn't know about
| anyhow or thiserror and spent an immense amount of time
| figuring out how to solve this problem, and writing all
| the boilerplate. This is not pragmatic and not ok.
|
| I would say a language like Go is well designed in that
| it has an overall design philosophy and you can see that
| throughout the language, including in the limitations
| which are often intentionally chosen. One part of their
| philosophy that I really appreciate is their intention to
| make the language pragmatic in industry settings, and
| making typical things obvious to newcomers is a huge part
| of that.
|
| Even if your solution is macros (which may be ok, but can
| also make the underlying generated code more opaque), it
| should be part of the standard library and part of the
| language manual. Making things the "officially approved
| way of handling a problem" has benefits beyond newcomers
| too, it gives the entire language more consistency
| resulting in a far more cohesive design.
| thinkharderdev wrote:
| "The" solution is not macros. The solution is what is
| provided by the language. If you are calling a bunch of
| different functions that return different error types
| then you either have to map them manually (using map_err)
| or you can create your own error type and implement
| From<XYZError> to automatically lift into your own error
| type. I agree that it is a lot of boilerplate which is
| why there are good libraries for generating the
| boilerplate with macros. But the underlying mechanism is
| not really obscure. It is covered quite well in the rust
| book (https://doc.rust-lang.org/book/ch09-02-recoverable-
| errors-wi...).
| brabel wrote:
| I don't agree. Before Rust 1.0 the community was really, really
| tiny. I don't have numbers but it was really a few people
| discussing the design issues... it was not like now where you
| literally have thousands of people with different incentives
| trying to force their views on things, which makes development
| go much slower than in the pre-1.0 days.
|
| I think the lesson is that, as with any software, having an
| open discussion between people who are on the same page about
| things and have similar incentives (with room for
| disagreements, as the initial author of Rust had plenty of) is
| greatly beneficial, without a doubt... but once you start
| getting people with polar opposite views on things, each backed
| by a substantial faction behind them, things start getting
| messy. I'm not saying this has happened in Rust, just that it's
| not a case the more people, the merrier.
| lagniappe wrote:
| I'm sure it'd be different for everyone reading this, but for
| me this story reinforces my hunch that the BDFL model moves
| faster and satisfies more people, in the sense that its hard
| to design by committee and harder to satisfy everyone.
| scoopr wrote:
| I would like to link Graydon's post on how the language turned
| out in his view, as it seems relevant:
| https://graydon2.dreamwidth.org/307291.html
| frankreyes wrote:
| Rust just took C++ std::unique and std::shared ptr and made those
| integrated directly in the language, and the only option for
| allocation. Which is awesome.
|
| It would be nice to see if we can have a sub set of C++ that
| forces us to only use std::make_unique or std::make_shared calls.
| UncleMeat wrote:
| You can already write a very very very simple linter that bans
| use of the "new" keyword. Rust did quite a bit more to make
| this ergonomic than just force you to use smart pointers.
| proto_lambda wrote:
| > Rust just took C++ std::unique and std::shared ptr and made
| those integrated directly in the language, and the only option
| for allocation
|
| Not really. Both Box and Rc/Arc are first and foremost library
| features implemented using the equivalent of malloc() and
| free(). Box is a bit special due to its deref semantics, but
| other than that, there's nothing stopping you from implementing
| them or something else yourself.
| frankreyes wrote:
| Thanks, never wrote Rust so I'm just guessing. What else
| there is, besides static type checks? Is there a runtime side
| too?
| proto_lambda wrote:
| There is no runtime (other than init/exit handling). The
| main thing that provides memory safety without a GC is the
| borrow checker, which is a language feature and independent
| of the smart pointer types in the standard library.
| yencabulator wrote:
| > _remove garbage collection from the core language and relegate
| it to the standard library_ , with a minimal set of language
| hooks in place to allow for flexible, pluggable automatic memory
| management.
|
| They did part one, but not part two...
| qalmakka wrote:
| The fact they've managed to bring safety to the masses without a
| GC is arguably the #1 reason for Rust's success. This has been a
| good move.
| dgb23 wrote:
| This claim is a bit too broad based on my limited
| understanding. Please correct me if I'm wrong:
|
| First, Rust is not actually a memory safe language. It makes
| writing memory safe code easy and nudges you towards writing
| "unsafe" code in specific places. This is a good thing and many
| get away with not requiring programmer asserted safety. But
| it's also important to note that ultimately it relies on it.
|
| Second, The borrow checker seems to primarily check that you're
| doing RAII properly. In a sense it answers the question of:
|
| "What if we get the predictable nature of stack allocated
| memory for the heap?"
|
| Clever, but that leads to a proliferation of lifetime
| annotations (which are part of your types) and makes code very
| brittle and rigid if spelled out as is. Because every lifetime
| that is encoded that way, has to fully cover the scope that
| encloses all of its usage. And if that weren't enough, it also
| infects almost every data structure that is some way composed
| of references.
|
| There seems to be multiple ways of dealing with this issue that
| I've come across when learning the language:
|
| 1. Avoiding pointer references whenever possible and using
| self-managed vector/slice/hashmap references.
|
| 2. Introducing GC via reference counting.
|
| 3. Cloning.
|
| 4. Macro or trait abstraction.
|
| When we're doing 1, we don't gain much utility from the borrow
| checker.
|
| When doing 2/3 we would be better served if we used a language
| with a battle hardened and optimized GC runtime.
|
| I've seen 4 in some cases, but never looked like it's "bringing
| safety to the masses". The whole API around traits and macros
| is very rich, very sophisticated and very subtle.
|
| I would rather phrase it as: "Rust brings ML/functional
| concepts to C++ programmers and it explores a new space of
| compiler optimizations based on that."
|
| However the Rust _community_ does bring these concepts to the
| masses. They have written excellent books and recorded multi
| hour long videos explaining and exploring the language and
| making this all accessible.
| steveklabnik wrote:
| By the standard of your "first," no language is a memory safe
| language. They all must rely on unsafety in the
| implementation of their runtimes in the same sense that Rust
| builds safe abstractions on top of unsafe code, and many even
| offer FFI, which is conceptually similar to calling an unsafe
| function.
| titzer wrote:
| Most memory safe languages lack enough power to implement
| their own runtime systems (except in an extremely
| inefficient way), because so many languages ultimately rely
| on an implementation in another unsafe language that has
| access to machine capabilities. Implementing the runtime
| system in the language itself is a black art that requires
| an unsafe dialect or extension. E.g. all the Java-in-Java
| VMs sneak dialect features in through intrinsic classes
| like "Pointer" and such that are recognized by their own
| extended compilers.
| gwbas1c wrote:
| > The fact they've managed to bring safety to the masses
| without a GC is arguably the #1 reason for Rust's success. This
| has been a good move.
|
| That's an _understatement._ Rust doesn 't require a framework /
| runtime. Unlike NodeJS, Python, Java, C#, ect; when you ship a
| Rust program, it has no external requirements.
|
| More important: Because Rust can create libraries that adhere
| to the C calling convention, you can create libraries that you
| can call from NodeJS, Python, Java, C#, ect. The fact that
| those systems have a heavyweight runtime (with GC) makes it
| very hard to create a library in one environment and call it
| from the other.
|
| I do think there's room for an "R2" that is semantically
| identical, but designed to be easier to understand. Probably
| the biggest room for improvement is using generics for things
| like RC, ARC, Mutex. I'd rather use something like keywords;
| and leave generics for user-defined types. This way, when
| trying to understand what something "is," the "how the memory
| is tracked" is semantically different from "what the struct
| is." (Even though under the hood they are the same thing.)
| thinkharderdev wrote:
| > using generics for things like RC, ARC
|
| There is an unstable feature for "box syntax" which both
| allows you to construct a `Box<T>` as let foo = box a
| (instead of let foo = Box::new(a)) but also to use box as a
| keyword in pattern matches to deref the box automatically. So
| struct Foo { value: Box<Bar> };
|
| let Foo { value: box Bar } (in which case value is bound to a
| &Bar) which was really nice.
|
| It got subsumed into a more generic "deref patterns" project
| which doesn't seem to be going anywhere but I hope it does
| because it makes the language much more ergonomic IMO.
| SkiFire13 wrote:
| The `box` syntax to create `Box`es has been removed because
| it was broken. The initial idea was to have it initialize
| the value directly in the heap, but that never worked for
| function calls. The `box` pattern instead remained as it
| was still useful to have (really unfortunate for the perma-
| unstable status though).
| thinkharderdev wrote:
| Ah didn't realized it had been removed. I gave up on it
| after it became clear it was never going to make it into
| stable rust. I do hope deref patterns gets some traction
| though as it could really be a nice feature.
| TylerE wrote:
| I push back, strongly, on the notion that Rust in it's current
| state is "for the masses".
| unsolved73 wrote:
| One of the biggest mistake, in my opinion.
|
| Especially with async which complicates lifetimes a lot.
| api wrote:
| Rust is a language for writing the GC, runtime, OS kernel, etc.
| pjmlp wrote:
| And?
|
| Plenty of GC enabled system programming languages have
| achieved similar feats, with bigger outcome than Rust has
| managed to on the desktop space, e.g. Xerox Workstations,
| across Smalltalk, Intelisp-D and Mesa/Cedar.
|
| Redox is still not as feature rich as Mesa/Cedar was on the
| Dorado in 1981.
|
| https://www.youtube.com/watch?v=z_dt7NG38V4
|
| By the way Go, D, Eiffel, Nim, Common Lisp, Scheme, some JVM
| implementations are bootstrapped, meta-circular, with their
| own GC implemented on them.
| qalmakka wrote:
| A GC would have made Rust yet another Dlang, which is pointless
| because Dlang has existed for a way longer time.
| silon42 wrote:
| Or more realistically, another JVM language.
|
| I actually wouldn't mind a subset of Rust that targets the
| JVM.
| raverbashing wrote:
| So why Dlang has not gotten as popular as rust or go?
| nickpp wrote:
| Lack of a big corporate backer is my best guess.
| qalmakka wrote:
| a. it never had a real corporate backer, until it was too
| late (and Rust already stole all of its mindshare) b. It
| had a garbage collector, which meant that realistically it
| could not replace C++. Or rather, it could but you
| basically had two different Dlangs - one with GC and
| classes, and one without. It was arguably not necessary,
| because people already had GC languages that were fast
| enough and worked for their needs, and those who needed
| speed were better served by C++, which arguably isn't that
| bad of a language if you know how to use it.
|
| Rust is arguably the first real contender to C++'s reign
| because it really brings to the table features you'd be a
| fool to pass on. D was nice, but it was not worth the
| switch. Eliminating whole classes of bugs instead is.
| eftychis wrote:
| I (also like sibling comments) respectfully disagree.
|
| Rust is aimed and focused as a safe C or a sane C++ substitute,
| and is meant to intermingle with both. It is not an application
| "high" level language. You can use it as such, which is great.
| For anything you would use C or C++ you can use Rust instead.
| As a cryptographer I find that great.
|
| Regardless of all the lack of latency or other control --
| beyond fine tuning -- a garbage collected language makes
| critical memory choices we want to make instead. There are
| times we want to swap or use our own memory allocator, never
| mind having to add a garbage collector in the mix. (There is a
| good number of languages that scratch that itch, and you can
| likely link and use your C/Rust code with them.)
|
| As far as async: also respectfully disagree. Async is sugar for
| here is a Future<..>. If you want to poll it locally you can.
| You can scope it also. If you want to use a cross thread work
| stealing algorithm you can. But you need like memory management
| to consciously make these design decisions. This is similarly
| why a lot of things are not built in in C.
| dystroy wrote:
| Rust noob here. Can you please explain how removing '~' and '@'
| by moving the GC to a library makes async harder ?
| BenoitP wrote:
| Lifetimes complicated. Having a proof of them at compile time
| is difficult. You can't prove everything for starters. A lot
| of patterns are just a no-go. Why not defer that to the
| runtime, and just observe which variables stick around
| (that's GC)
|
| IO complicated. The cycle of doing code then waiting for IO
| is wasteful (sequentially you waste millions of CPU cycles
| waiting for the network to answer back). To max out usage of
| your hardware resources you could just aggregate IO requests
| with your compiler. Switching back and forth between code
| that uses IO, as IO request come back (that's async)
|
| Problem is: switching back and forth between code that uses
| IO is recklessly hard wrt coming up with a proof of the
| lifetimes of the variables.
|
| And a language/runtime _needs_ to have the trifecta of
| compiler/GC-or-memory-management/memory-model coherent and
| within the runtime.
|
| compiler/GC-or-memory-management: the GC-or-memory-management
| needs to know who writes, who owns, who reads
|
| GC-or-memory-management/memory-model: you need to know when
| and how you can read your writes, what are the rules
|
| memory-model/compiler: you'll be managing memory barriers so
| that you can cram together sequences of writes that are
| compatible, for maximal performance
|
| This trifecta dependence is foundation to a language/runtime,
| and a change to one affects the others quite deeply. Changing
| the compiler (bringing async here) affects the other ends and
| you can't do that when GC-or-memory-management is all over
| the place (as a lib, or god forbid in user hands)
|
| I'm afraid async is just something unaffordable for a
| language that wants to be that close to the metal. And even
| then, async is just a bandaid for a costly threading IO
| model.
|
| ----
|
| Come over to the dark side of Erlang, Go, and Java. We have
| small threads now. You can just block like there is no
| tomorrow, and the runtime will have a cheap back and forth.
| You can just forget about the lifetimes, as the GC will sweep
| after you (and concurrently, outside of the critical
| allocation path). Forget it all my friend. Java is love, Java
| is life.
| baq wrote:
| it's not the things that were removed, it's the things you
| have to add to the source now to make it work within the
| limits of the borrow checker.
|
| in the simplest case, you'd add Arc<..> everywhere @ used to
| be.
| arghwhat wrote:
| Rather than those operators and how GC is done, I think the
| key aspect is how much easier async is in other languages
| that have easy automatic memory manage without ownership
| through garbage collection. Go and JavaScript/Dart are good
| examples.
|
| But such models would also take away everything that makes
| Rust... Rust.
| yccs27 wrote:
| I suspect GP only read the title and not the article. A Java-
| style GC might make async code easier, but that never existed
| in Rust.
| nu11ptr wrote:
| They wanted a systems language. A GC would be very appropriate
| for much of the code I write, but would not have been for an OS
| kernel. Green threads removed as well. I admit the GC version
| with green threads I would have preferred, but again, not
| appropriate for the domain they wanted. I still want my halfway
| between Go and Rust language.
| tikkabhuna wrote:
| I do wonder why there hasn't been more exploration in a
| series of languages that have a very similar style and
| toolchain but have different audiences. It would be great to
| have Rust, Rust with a GC, and then some sort of interpreted
| language that has a similar syntax. When jumping between
| languages I feel like half the battle is overcoming muscle
| memory.
| 0cf8612b2e1e wrote:
| This is my dream. An interpreted language, implemented in
| Rust which essentially boxes all data and can do message
| passing to the host language. This would then give a
| somewhat plausible upgrade path to port the interpreted
| code into Rust bit by bit as required.
|
| The interpreted language even be slower than Python, so
| long as the escape hatch to Rust was simple and safe enough
| to implement the interfaces.
| ynik wrote:
| A GC-free systems language isn't just for OS kernels. Rust's
| main use case was to replace C++ components in firefox. This
| wouldn't have been possible with a language that brings along
| its own GC, as Firefox already has a JS GC. Multiple garbage
| collectors in the same process are a quick way to madness,
| especially if you have reference cycles stretching across
| multiple GC heaps. This also comes up when writing Python
| extension modules, base libraries that are meant to be usable
| across languages, etc...
|
| This is part of why Rust is so successful -- it's the first
| real alternative for this space since C++ came along. For
| most application development, it's better to use a garbage
| collected language. But in the application space there is a
| much bigger choice of languages already available, Rust
| wouldn't have been a big deal over there.
| j1elo wrote:
| I heard this brilliant summary in a video from ThePrimeagen:
|
| _In Rust, lifetimes color your types, like async colors your
| functions._
|
| It is a great condensed summary of what makes lifetimes a great
| difficulty of async Rust. It's a language that has the function
| coloring that is typically introduced by _async_ (likewise in
| JavaScript), and _on top of that_ the typesystem itself gets
| colored by lifetime annotations. You can have a well written
| and working program... then due to some new need or
| refactoring, wanting to add a _' static_ somewhere will cause
| it all to break down.
|
| It's part of the language, nothing bad with that. But it _is_
| an extra layer of difficulty that needs to be mastered. To me
| it shows that Rust might only be the initial step towards
| future programming languages where this kind of issue doesn 't
| lean so much on developer knowledge.
| rcarr wrote:
| Can you post a link to this video? Sounds interesting
| j1elo wrote:
| Wasn't sure, but found it!
|
| https://www.youtube.com/watch?v=p-tb1ZfkwgQ&t=340s
|
| (in case it doesn't work: on the 5:40 mark)
| justincredible wrote:
| [dead]
| nercury wrote:
| With GC rust would have been just another slightly different
| language.
| Shorel wrote:
| Then you can use D lang.
| chrismorgan wrote:
| It's worth noting, given the aspirational conclusion, that @T
| managed pointers were actually equivalent to Rc<T>, since they
| were only ever implemented with reference counting without cycle
| collection. (This would have changed had they stayed in the
| language, but winds changed instead.) The standard library
| doesn't ship tracing GC, and it's still not altogether clear how
| it would be best to do it.
| https://manishearth.github.io/blog/2021/04/05/a-tour-of-safe...
| is useful further reading. I think it's fair to say that the
| "ship tracing GC as part of the standard library" ship has
| sailed, but power and convenience _could_ still warrant language
| /standard library features to support it, though I doubt anything
| will ever happen, or at least in the next decade. The focus of
| the language shifted and was clarified quite a lot between the
| time of this article and even Rust 1.0 just under two years
| later.
| jxf wrote:
| Early Rust was really incomprehensible to me, and it's
| unquestionably better than it once was from an ergonomics and
| ecosystem perspective. The ~ and @ sigils everywhere were verging
| on Perl!
| cmrdporcupine wrote:
| I actually think removing ~ was a mistake.
|
| a) A lot of code ends up littered with Box anyways, which
| frankly isn't any more readable since "Box" doesn't tell you
| anything until you already know what it is and once you do it's
| just verbosity.
|
| b) As I understand it, Box gets "special treatment" by the
| compiler/type system, so pretending it's just a pure standard
| library component is a bit obfuscatory
|
| c) Heap allocation and heap pointers are first class citizens
| in other languages, why wouldn't they be so in Rust?
| veber-alex wrote:
| > b) As I understand it, Box gets "special treatment" by the
| compiler/type system, so pretending it's just a pure standard
| library component is a bit obfuscatory
|
| There is a strong desire to stop that and have Box be a
| normal std type, the main thing that is blocking this at the
| moment is that Box has special deref magic that is not
| possible to implement with surface level rust syntax (even
| with nightly features).
| dingi wrote:
| I'm pretty sure that Rust is a great language for low level
| stuff, the domain of C/C++. Try to shoehorn it to do more higher
| level stuff (i.e: vast majority of software developed today), And
| you'll be fighting battles with the language at each step. For
| those use cases, Java, JavaScript, Python et al will reign
| supreme for many years to come. Most of the time, GC stalls are
| the least of your problems.
| dang wrote:
| Discussed at the time:
|
| _Removing garbage collection from the Rust language_ -
| https://news.ycombinator.com/item?id=5811854 - June 2013 (130
| comments)
| orf wrote:
| Rust has had an interesting and probably pretty unique story,
| moving from a GC'd language with a runtime + green threads[1] to
| what it is today.
|
| They are treading a line that is uniquely difficult to tread, and
| I think it's mostly working. Async is still a bit of a mess but
| it seems that's because it's inherent to the constraints they had
| to impose on themselves.
|
| It's kind of cool that I can use Async stuff in embedded
| devices[1] and a web app, even if I do get frustrated with mind-
| numbing Async issues from time to time.
|
| 1. https://github.com/rust-lang/rfcs/pull/230
|
| 2. https://embassy.dev/
| masklinn wrote:
| > moving from a GC'd language
|
| Rust was never actually a GC'd language. It had a smart pointer
| called Gc (and before that @), but that was only ever
| implemented as refcounting (according to the changelogs some
| preliminary work had been done towards a precise GC but AFAIK
| there was no followup).
|
| This is in large part why it was dropped: it was technically
| redundant with Arc, and could give users the wrong impression,
| and could always be added back later if that made sense.
| pornel wrote:
| The original design by Graydon envisioned a real GC, but one
| per task/actor instead of global, with limited message
| passing between threads.
|
| http://venge.net/graydon/talks/intro-talk-2.pdf
| masklinn wrote:
| Maybe but that was never actually a thing. It was not even
| a thing which got moved away from like type states or
| internat iterators, it never was.
| aidenn0 wrote:
| If that's what had been created, I'd have been all over it
| like butter on toast.
| kaba0 wrote:
| There is pony or erlang that does something similar.
| aidenn0 wrote:
| Pony is already on my list of things to look at; you just
| bumped it up a couple of spaces.
| pjmlp wrote:
| Technically from CS point of view, refcounting is a GC
| implementation algorithm.
| masklinn wrote:
| I fail to see what that has to do with anything, unless
| you're asserting that current rust is GC'd on the back of
| arc existing.
| Zambyte wrote:
| The point of a garbage connector is to simulate infinite
| memory[0], which is the point of Arc. So current Rust has
| GC :)
|
| [0] https://octodon.social/@cwebber/110815313802832972
| bmacho wrote:
| Rust as a language is not GC'd, but has a refcounted
| (that is, GC'd) pointer type if you want/need it.
| pjmlp wrote:
| It has to do everything with the wording on your comment
| that kind of makes the usual lay man distinction between
| GC and refcounting, nothing to do with Arc, as using it
| is manual work anyway.
| ryukoposting wrote:
| Yes, but does that make circa-2013 Rust a "garbage
| collected language?" It seems to me that when we talk about
| GC'ed languages, we're talking about languages where the
| heap is managed implicitly.
|
| If I understand it right, @ is explicitly invoked by the
| user, but the implementation is embedded within the
| language. With Arc/Rc, there are Deref/Drop implementations
| somewhere in the stdlib that do the reference counting.
| flohofwoe wrote:
| Refcounting is also just (dumb and slow) garbage collection
| though.
| masklinn wrote:
| The main point is that rust was never a ref counted
| langage, it had a purportedly GC'd opt-in pointer type.
|
| And while refcounting has lower throughput than more
| advanced forms of garbage collection, it has a much higher
| reactivity / lower memory overhead, and it integrates much
| better with other methods of resource management.
| sbt567 wrote:
| Is there any other languages that let you use async on
| embedded/bare metal? I think Rust could learn from them.
| Especially on the ergonomics side. Otherwise, what Rust might
| currently do is blazing a trail through a forest.
| SeenNotHeard wrote:
| Vala offers async/yield and compiles down to native code
| (after being transpiled to C): https://wiki.gnome.org/Project
| s/Vala/Tutorial#Asynchronous_M...
|
| More info: https://vala.dev/
| jamesmunns wrote:
| From a net-effect standpoint, Rust's implementation of
| async/await is fairly similar to protothreads, which were
| used even on smaller AVR and MSP430 targets where rtos
| threads weren't practical.
|
| That being said, protothreads were implemented using _wildly
| cursed_ C macros, and offered none of the safety guardrails
| that Rust has, nor the ergonomics, and had some insane
| constraints, like "you can't use local variables at all,
| only statics" because like rust, they were stackless
| coroutines that meant you would have no control of the stack
| across await points.
|
| If you were VERY CAREFUL, you could get the same lightweight
| concurrency with almost the same fundamental model. Woe be on
| you if you ever had to debug though.
|
| edit - here's a look at how protothreads were expanded:
| https://dunkels.com/adam/pt/expansion.html
| FpUser wrote:
| >"Is there any other languages that let you use async on
| embedded/bare metal?"
|
| So you do not know if there are and what kind and other
| important details.
|
| >"I think Rust could learn from them."
|
| Yet you think
| marcosdumay wrote:
| There is no other language that does what Rust is trying to
| do. But that is not "using async on embedded/bare metal",
| this one is easy, jut use a garbage collector.
| brabel wrote:
| Hm, I think Pony did try doing what Rust is doing, with an
| even more advanced system for "borrows" (there's several
| types of borrow, not just two): https://www.ponylang.io/
|
| Also, Vale is on the way: https://vale.dev/
|
| And D is also working on it:
| https://dlang.org/blog/2019/07/15/ownership-and-borrowing-
| in...
| kaba0 wrote:
| What should it learn? If you combine async with Rust's nested
| lifetimes, you get quite a bit of complexity from the
| borrowchecker, period. There is not much else to it.
| ekidd wrote:
| > _even if I do get frustrated with mind-numbing Async issues
| from time to time._
|
| A noticeable portion of async issues come from the fact that a
| lot of people use Tokio's _multithreaded_ async runtime. Tokio
| allows you to mix async and native theads, which is both a
| virtuoso technical accomplishment and also a bit ridiculous.
|
| If you use Tokio's single-threaded runtime, things get simpler.
|
| The remaining async challenges are mostly the usual "Rust tax",
| turned up to 11. Rust wants you to be painfully aware that
| memory allocations are expensive _and_ that sharing memory in a
| complex concurrent system is risky.
|
| In sync Rust, the usual advice is "Don't get too tricky with
| lifetimes. Use `clone` when you need to."
|
| In async Rust _without_ native threads, the rules are something
| like:
|
| 1. Boxing your futures only costs you a heap allocation, and it
| vastly simplifies many things.
|
| 2. If you want a future to remain around while you do other
| stuff, have it take ownership of all its parameters.
|
| Where people get in the most trouble is when they say, "I want
| to mix green threads and OS threads willy-nilly, _and_ I want
| to go to heroic lengths to never call `malloc`. " Rust makes
| that approach look _far_ too tempting. And worse, it requires
| you to _understand and decide_ whether you 're taking that
| approach.
|
| But if you remember "Box more futures, own more parameters, and
| consider using a single-threaded runtime" then async Rust
| offers some pretty unique features in exchange for a pretty
| manageable amount of pain.
|
| Also, seriously, more people should consider Kotlin. It has
| many Rust-like features, but it has a GC. And you don't need to
| be constantly aware of the tradeoffs between allocation and
| sharing, if that's not a thing you actually care about.
| foooorsyth wrote:
| If you're counting on people to drop down into a single-
| threaded runtime when using async constructs, you've really
| lost the mark. Parallelism is the user's goal most of the
| time, right? Perhaps in web context to avoid runaway thread
| spawning and slow loris attacks it's not (the main selling
| point of node.js), but otherwise people want to go fast.
|
| >Also, seriously, more people should consider Kotlin.
|
| I like Kotlin, but I find its coroutine machinations to be
| far more confusing that just plain threads (over which Java
| already had/has some nice quality of life abstractions). And
| debugging broken Kotlin coroutine code is hell. You will not
| get a normal-looking stack trace when things go wrong.
| proto_lambda wrote:
| The main goal of using async for me is to not have to
| handle all the IO wait state machines myself. It does a
| pretty good job of that, and as long as my program consists
| of a bunch of tasks concurrently waiting for IO to finish,
| a single thread is perfectly fine.
| biorach wrote:
| > Parallelism is the user's goal most of the time, right?
|
| I'm not sure you're right about that.
| foooorsyth wrote:
| In the general sense? Probably not. In the context of
| Rust users? It certainly begs the question: why use Rust
| if you aren't going for speed? Just write it in
| Node/Go/JVM-lang if performance doesn't matter and you
| just want an event loop that looks like threads.
| seabrookmx wrote:
| > consider Kotlin
|
| Or C#. C# has async/await (it originated there) and does have
| a multithreaded event loop, but due to having GC is just a
| lot easier to work with than Rust+Tokio.
|
| If you're in the "don't colour my function" camp, GoLang is
| also worth throwing in the mix.
| CharlieDigital wrote:
| C# is probably what most teams want but don't know it.
| - Language is very, very similar to TypeScript. If you're
| already doing TS on the backend with Node (or even JS),
| it's a very small lift to C# - Very rich standard
| libraries and first party libraries; reduces the need to
| import a bunch of third party code - .NET minimal
| web APIs are very similar to Express now and perhaps even
| easier since you don't need to import anything to get a
| microservice up and running - .NET AOT with .NET 8
| will dramatically improve the cold-start for use cases like
| serverless functions (I find the cold start already pretty
| good with .NET 7 on Google Cloud Run with the CPU Boost
| feature turned on). - C# has a lot of functional
| features as a result of F# - Compiles fast and has
| hot reload via `dotnet watch` - Provides access to
| low level primitives where extra performance is needed
| o11c wrote:
| Don't forget: - unlike Java, C#
| supports value types so you can avoid gratuitous overhead
| or ugly code when all you need is a simple wrapper
|
| It's still painful when you need strict ownership though,
| except for the limited case of an object allocated
| directly on the stack.
| seabrookmx wrote:
| 100%.
|
| > NET AOT with .NET 8 will dramatically improve the cold-
| start
|
| Don't get your hopes up on AOT. It still has lots of
| limitations, reflection being a big one. ASP.NET
| initialization relies heavily on reflection so it will be
| a while before we can have AOT compiled, HTTP
| microservices.
|
| > I find the cold start already pretty good with .NET 7
| on Google Cloud Run
|
| Thanks for the tip! We're mostly using GKE but have a few
| services on Cloud Run that might benefit. Do you use the
| "always allocate CPU" option? We're seeing some memory
| creep we suspect would be solved by giving the GC cycles
| when a request isn't in flight.
| aeturnum wrote:
| > _If you use Tokio 's single-threaded runtime, things get
| simpler._
|
| I don't do a lot of low level work, but coming out of Elixir
| / Erlang I would expect a greenlet system to manage the # of
| threads based on the hardware its running on. I.e. not single
| threaded or "you manage the threads too" but "a standard
| piece of code spreads your greenlet processes between N
| managed hardware threads where N is determined by hardware +
| settings." Is that not a thing that the Rust async libraries
| support?
| watermelon0 wrote:
| By default, Tokio uses multi-threaded runtime, but you can
| force it to use the single-threaded one:
| https://docs.rs/tokio/latest/tokio/attr.main.html#current-
| th...
| ReactiveJelly wrote:
| Maybe I should try smol instead of Tokio. I'm guessing a lot
| of libraries are made to fit with Tokio, but I imagine for a
| typical desktop app, a multi-threaded runtime is total
| overkill. Plus I could just run multiple runtimes if I want.
|
| Like I suppose if I needed a blocking task that was also
| async, I could send that to a thread pool and then internally
| spawn a worker async runtime. It's a thinker.
| swsieber wrote:
| A single threaded Tokio runtime still has a separate thread
| pool for heavy tasks. Maybe block_on? I can't remember
| where I read that tho.
|
| Edit: not block_on, but spawn_blocking. See https://www.red
| dit.com/r/rust/comments/16ebdi1/comment/jzy7f...
| eska wrote:
| No need to switch. Just change the tokio flags
| Klonoar wrote:
| Or, you know, that single threaded runtime that OP just
| mentioned. ;P
| timcavel wrote:
| [dead]
| masklinn wrote:
| > If you use Tokio's single-threaded runtime, things get
| simpler.
|
| A major issue when dealing with the tokio runtime is Send
| bounds requirements, I don't think the single-threaded
| runtime changes anything because these are API-level issues.
| You can use spawn_local to avoid migrations but you can do
| that on the multithreaded runtime just as well.
|
| And then a lot of tools and libraries which get layered over
| tokio (or assume a tokio environment) will require send
| futures anyway.
| veber-alex wrote:
| What's the problem with Send bounds? So you use Arc instead
| of Rc.
| masklinn wrote:
| The main problem is that for a future to be send,
| _everything held across an await point has to be Send_.
| Which is quite constraining and annoying, especially
| because this often does not play well with trait objects
| or `impl Trait` as people will commonly not think to add
| trait extra trait bounds.
|
| And of course it's an accumulation of trait bounds on
| everything, which makes for a downgrade in readability.
| phamilton wrote:
| LocalSet does not require Send. https://docs.rs/tokio/lates
| t/tokio/task/struct.LocalSet.html
|
| EDIT: Sorry to join in the multiple responses. Clarity for
| others: "current_thread" by itself does not relax Send, and
| many libraries aren't configurable to make everything use
| LocalSet.
| [deleted]
| masklinn wrote:
| I mentioned spawn_local, which is basically the same
| feature as localset (you can only spawn_local within the
| scope of a localset).
| necubi wrote:
| That's incorrect, the Send bounds are only there in the
| multithreaded runtime (because it might need to Send a task
| across threads). The single threaded runtime will never do
| that, so its futures don't need to be Send.
|
| Somewhat relatedly, a major (early) stumbling block for me
| with the multithreaded runtime was trying to keep
| references across await points. While boxing them is always
| an option, things also got much easier when I realized that
| while &T is only Send if T is sync, _&mut T is send if T is
| send_.
| masklinn wrote:
| > That's incorrect
|
| Is it?
|
| > the Send bounds are only there in the multithreaded
| runtime (because it might need to Send a task across
| threads).
|
| Tokio is interacted with using free functions which
| dynamically look up the current runtime, they could not
| have a different signature even if tokio had different
| runtime types, which it does not.
|
| > The single threaded runtime will never do that, so its
| futures don't need to be Send.
|
| Please do point to the ?Send spawn (not spawn_local)
| which supposedly exists for the current_thread runtime.
| You can't even spawn_local at the toplevel of the
| current_thread runtime.
| bryanlarsen wrote:
| Unless I'm mistaken, switching to a single threaded tokio
| runtime made Send requirements go away for me.
| veber-alex wrote:
| Changing the type of runtime doesn't change the API at
| all.
|
| Even in a single threaded runtime spawn requires Send and
| you need to use LocalSet + spawn_local for !Send futures.
| bryanlarsen wrote:
| I wasn't using spawn. It's an I/O bound program with a
| big select! loop at it's core. Rust spit out errors about
| Send and I made them go away by switching to flavor =
| "current_thread".
| masklinn wrote:
| Would be nice if you could find it again, because from
| what I know of tokio I'd assume you made other unrelated
| changes which fixed the issue: tokio only has one runtime
| type, the flavour's effects are internal, the top-level
| future (run by Runtime::block_on) is always !Send, but
| from that you can only run Send futures (via spawn and
| spawn_blocking), unless you create a LocalSet.
|
| current_thread doesn't even create an implicit LocalSet,
| if you try to `spawn_local` from the top-level of a
| current_thread runtime you get a panic, exactly like a
| multi_thread runtime.
| bluejekyll wrote:
| > Tokio allows you to mix async and native theads, which is
| both a virtuoso technical accomplishment and also a bit
| ridiculous.
|
| Can you describe what issues you're referring to? I use the
| multi-threaded Tokio runtime, I've not noticed any overhead
| with that in terms of development vs. single threaded. Also,
| multi-threaded async runtimes are generally what you want to
| make sure you don't have any single tasks blocking others
| that could make progress in parallel.
| danenania wrote:
| "Also, multi-threaded async runtimes are generally what you
| want to make sure you don't have any single tasks blocking
| others that could make progress in parallel."
|
| Is there an advantage to multi-threaded async if you're IO-
| bound? If you want a bunch of concurrent system calls or
| network requests it seems like single-threaded async can
| handle that pretty nicely ala Node.js.
| toast0 wrote:
| Depends on your load, but many I/O bound loads are bigger
| than what you can do in a single thread.
| rcarr wrote:
| For anyone interested there was a big discussion on async in
| rust a few days ago. Rust is on todo list to learn but I found
| reading some of these comments quite interesting:
|
| https://news.ycombinator.com/item?id=37435515
| mplanchard wrote:
| Note also this rebuttal:
| https://news.ycombinator.com/item?id=37448460
|
| As someone who works with async rust professionally, I
| wouldn't let takes like the one you posted dissuade you. My
| personal opinion is that async rust is easier to work with
| than in most other languages because of the extra concurrency
| guarantees that rust gives you. There are some rough edges,
| but not nearly as many as the link you posted suggests.
| littlestymaar wrote:
| This exactly.
|
| Sure Async Rust requires you to learn a few new things, but
| the blog post is mostly a rant from someone not very
| familiar with the topic (like what you'd expect from
| something called "<X> is a bad language" TBH).
| mjw1007 wrote:
| Here are some notes on the later history of GC in Rust:
|
| RFC 256, 2014-09. https://rust-lang.github.io/rfcs/0256-remove-
| refcounting-gc-...
|
| Includes << I (and I think the majority of the Rust core team)
| still believe that there are use cases that would be well handled
| by a proper tracing garbage collector. >>
|
| https://news.ycombinator.com/item?id=8312327 A core developer
| says
|
| << I wouldn't be so quick to give up on GC support yet! "We have
| a plan!" But I don't think we'll have it in for Rust 1.0. And
| it's true that, even if we never do get it to work in a
| satisfactory way, the language works just fine without it. >>
|
| By 2015-04 ("Fearless Concurrency"), "Memory safety without
| garbage collection." is a "key value proposition" (this isn't
| quite the same as saying "we never want Garbage Collection", of
| course).
|
| 2016-08 https://manishearth.github.io/blog/2016/08/18/gc-support-
| in-... << Recently we (Felix, Niko, and I) have been working on
| getting compiler-level GC support for Rust. >>
|
| 2018-10 withoutboats has a research garbage collector as a
| library: https://boats.gitlab.io/blog/post/shifgrethor-i/ The
| intro post includes << I do not expect we will ever "add GC to
| Rust" >>.
|
| 2021 summary of options:
| https://manishearth.github.io/blog/2021/04/05/a-tour-of-safe...
| Alifatisk wrote:
| Would adding the GC back make the syntax easier to read and
| remove all those special characters like the way lifetime is
| handled?
| steveklabnik wrote:
| Even if in theory it would, Rust's commitment to backwards
| compatibility would mean that those things could not be
| removed.
|
| You're asking for a different language.
| gabereiser wrote:
| >"The sigils make the code unfamiliar before the concepts are
| learned. Unlike the rest of the punctuation in Rust, ~ and @ are
| not part of the standard repertoire of punctuation in C-like
| languages, and as a result the language can seem intimidating."
|
| It's still intimidating with lifetimes and _Arc <Box<Rc>>_ like
| idioms. Still, it's blazingly fast (tm). I wonder what Rust would
| be like if the GC was kept in like in Go.
| kaba0 wrote:
| > I wonder what Rust would be like if the GC was kept in like
| in Go.
|
| It would be one of the litany of managed languages that doesn't
| significantly differ in anything from each other, and we would
| have no reason to be hyped about.
| gabereiser wrote:
| Hype is subjective but I do welcome a safer C++.
| bunderbunder wrote:
| What I increasingly want to see is a language where a garbage
| collector is optional in the main executable, but not (directly)
| available to library code.
|
| Because I want to have a language that I can use to write lean
| libraries that are available to any language with a C FFI, and
| that can be linked directly by AOT-compiled languages, without
| getting into a horrible quagmire of dueling heaps and copying.
| But I also want to have proper functional and asynchronous
| programming, and generally just to not have to manually fuss with
| memory in the higher-level code.
|
| Python and C/C++/Rust/Cython/etc extensions kindasorta achieves
| this, and it's a huge factor in the language's ascendance in
| scientific computing applications. Game development has a long
| history of achieving a similar effect by embedding Lua or Lisp.
| But I think that it might be more pleasant to have it formalized
| and baked into a single language.
| AndrewDucker wrote:
| Possibly Rust needs a GC<> wrapper type, which makes everything
| it contains garbage collectable. And then you could be bare
| metal, except when you want to not be.
| zozbot234 wrote:
| There are a number of efforts along these lines, the most
| interesting is probably Samsara
| https://github.com/chc4/samsara
| https://redvice.org/2023/samsara-garbage-collector/ which
| implements a concurrent, thread-safe GC with no global "stop
| the world" phase.
| [deleted]
| yjftsjthsd-h wrote:
| I'm reminded of wuffs, which _only_ allows writing libraries
| and explicitly doesn 't support allocateing memory, requiring
| the calling program to provide memory. It seemed like a nice
| separation of concerns in my non-expert view. (And then wuffs
| compiles to C, which is nice for interoperability)
| binary132 wrote:
| call me crazy, but I found old-style Rust, runtime and all, much
| more appealing.
| timeon wrote:
| Rust has other nice features but I started with Rust not in
| spite of borrow checker but because of it. Without borrow
| checker why would one choose Rust? I would choose from large
| pool of GC-languages.
| jedisct1 wrote:
| You're not alone.
| fleventynine wrote:
| Then choose from one of the countless garbage-collected
| languages with sum types and a runtime; Rust is successful
| because it's offering us systems programmers something unique.
___________________________________________________________________
(page generated 2023-09-11 22:01 UTC)