6 ms·
A guide to closures in Rust
- bjarneh 3y ago> I'll explain what the move keyword does later in the article. For now just trust me that it is needed for the code to compile. That does not take a lot of trust, after a few rounds with that compiler. At least error messages are good with the Rust compiler. I've got very limited experience with Rust, but it does seem like a language with a massive threshold for beginners.
- gdcbe 3y agoIt has a massive threshold for beginners for sure. Personally I have a background in C/C++ and moved towards C#, Go, JS etc. With an interest in many more, but the above were my main languages for a decennia. 9 years ago I picked up Go, and have used it the most as my main language since then. 7 Years ago I picked up Rust, but only since 6 months ago really intensive (including publishing the guide that came out of my experience at https://rust-lang.guide/ https://rust-lang.guide/). Over those 7 years I've only done a production project around the start of that journey and now since 6 months ago I'm writing production code in it again and haven't stopped since. But I've learned the language like 3 different times in that journey. It's not an easy one to get into. A lot of things to wrap your head around, especially if your goal is to really master it, rather then just "woohoo I can compile my program". I still not master it, but I do feel now fluent in it and can express my ideas well. There are also no more fights with the language or its tooling. Compared to that, Go(lang) is super easy to get into. Ridiculously easy. Perhaps too easy. You can give it to a programmer of any level, and they probably can ship their first feature that same day. The difference is however, that. First of all with Golang it's super easy to ship code with nil pointer exceptions in it or data races. I've seen it in the best code bases. And sure, plenty of people will tell if you do it "right" you'll have no issues. I've hear similar comments from ex-colleagues still in the C++ world as well. Secondly, Golang is very opinionated and if you derive in ideas or needs a bit from what they want you to do, you are in a bit of a problem. This has been slightly improved, but it's still very limited and still a lot of magic that only built-in features can perform. And thirdly, the language is very minmal (Golang). So codebases look very verbose very quickly. Also harder to express ideas (as error support is limited, and sum types are not a thing, neither are other things like pattern matchings). It's also a lot less "functional". This is not to compare Golang vs Rust too much, even though I did. My point was more, and I probably butcher my own comment not making that point well, is that I think it's fair to have a massive threshold for beginners to get into a programming language. As long as in return it means you get a very powerful tool in rerturn to do some of your work. And Rust is def. such a tool. Many people boast about how peformant it is. And while that is true, it is for me for most projects more of an extra benefit rather than the things I really like it for. What I like with Rust is that it gives me really high confidence in my code (comparable to projects I've written in Elm and Haskell in the past). When my project compiles I really am pretty confident it will not give me runtime surprises beyond mistakes elsewhere (e.g. infrastructure choices). It's a very expressive language and pleasant to use in that way. And then there is of course the fact that it is truly FOSS, has a very nice community, great documentation and a lot of excellent learning resources. I am not a fanboy of much, neither of Rust, but I do really appreciate its existence and I am happy to use it where it suits. Yes it is a massive threshold, but one worth to pay.
- bheadmaster 3y ago> And sure, plenty of people will tell if you do it "right" you'll have no issues. Go is inspired a lot by CSP, and allows you to apply CSP calculus when using goroutines and channels. It doesn't make it impossible to make mistakes, but it does give you tools needed to reason about your concurrent program, which is more than can be said of most other languages' concurrency support.
- dontlaugh 3y agoAnd yet goroutines share the same address space, don’t have a good way for the parent to wait for them and even panic silently. There’s a reason many projects ban the go keyword in favour of a wrapper that fixes some of this. Go requires constant vigilance, sadly.
- bheadmaster 3y agoYes, as I said, Go doesn't make it impossible to make mistakes. It doesn't hold your hand and slap you when you go off-road. But saying it "requires constant vigilance" is an overstatement. As long as you put a little bit of thought in the concurrent code you're writing, it's very easy to do things right. Data races are easy to avoid if you only transfer ownership by sending pointers through channels.
- dontlaugh 3y agoIt would’ve been trivial to make the language disallow nil, separate goroutine address spaces, crash the process on unhandled panic in any goroutine, etc. The reason Go annoys people is the unforced errors. Sure, it gets a lot right. But what it gets wrong had solutions long before Go existed and those solutions were wilfully ignored.
- bheadmaster 3y ago> It would’ve been trivial to make the language disallow nil Which would interfere with Go's philosophy that zero should be a valid (usable) state for types. What would be the default value of a reference type without nil? > separate goroutine address spaces This is not trivial to implement. The only way I can think of is to start each goroutine in a new process, which brings along other downsides - everything must be copied, cannot pass file/socket handlers between goroutines, etc. Doesn't seem to be worth it. > crash the process on unhandled panic in any goroutine Go does crash the process on unhandled panic in any goroutine: # main.go package main func main() { go func() { panic("hehe") }() for { } } # go run main.go panic: hehe goroutine 5 [running]: main.main.func1() /[...]/main.go:5 +0x27 created by main.main /[...]/main.go:4 +0x25 exit status 2 > The reason Go annoys people is the unforced errors. This is the criticism I've also had, and raised an issue on GitHub. Unfortunately, it's impossible to fix this without breaking backwards compatibility. > But what it gets wrong had solutions long before Go existed and those solutions were wilfully ignored. They were wilfully ignored, but only because the tradeoffs didn't seem worth it at the moment. You make it sound bad, as if Go devs were simply high on crack and knew what they doing was wrong, but did it anyways, when in reality it's more of a case of those "solutions" not being compatible with the goals of the language, or simply nobody coming up with an elegant way to incorporate it.
- laeri 3y agoYou will probably get a lot of varied responses and also some people who just defend the language of their choice. Let me give you my opinion who just started learning 3 weeks ago. And I needed around 1 week until I could write a simple parser by hand and around 2 more until I am now being quite productive in this new language. It is actually very simple go get into, depending on your experience. What do mean by 'threshold for beginners', what are beginners for you. Do you mean someone learning their first programming language or someone already proficient with one or multiple trying to learn Rust now? Because a lot of the features of Rust are present in other language and your mental models will be similar enough for them in Rust accelerating your learning curve. Of course someone new to programming will have problems as the features that are unique to rust (borrow, move) in addition to everything else leads to a lot of overhead sure. My experience is with Java, Python and Javascript/Typescript as I know them sufficiently well to create programs and I also dabbled with many other languages just trying out how they feel (Scheme, Racket, Nim, Go, Haskell, C, C++, Ruby, Smalltalk). When starting with Rust it took very little time to get comfortable. The language feels very well designed and Option, Result type are very logical to use for return types. Many languages have these so depending with what you are familiar with this might be new to you but it is surely not more complicated than explaining try/catch blocks. Of course dealing with Option/Result is a bit uncomfortable in the beginning but If you just browse a bit through the standard library and learn about the `?` operator you see many patterns how to deal with them in an efficient manner (map, map_or_else, or, or_else, flatten, unwrap, unwrap_or,...). Rust has closures but now you find them in almost any language so it is not really surprising (maybe only nuances that come up with the borrow checker). Most of the standard traits and enums are quite straightforward such as `Default`, `From`, `FromStr`, `Iterator`, `Clone`, `PartialEq`, all the operator traits. Also the borrow checker and some lifetime compiler errors are for sure a thing that can lead to thresholds and for my time spent with the language I would say there are many things I will need to learn to fully be able to grok the language. However, most of the time you can circumvent these problems by just not using lifetimes in your own code and by cloning variables. So for people that want to write really efficient code you can do this without cloning variables if you have a better understanding of them but as a beginner you have a simple escape hatch most of the time and allows you to gradually gain a better model of lifetimes/borrowing rules. I think the only really threshold is the trait object problem. If you want to build any kind of nested structure or if you want some kind of "polymorphism" which is a natural reaction depending which programming language you have used. You will need to deal with understanding them and this might take some time.
- DonaldPShimoda 3y ago> it does seem like a language with a massive threshold for beginners. Programming with manual memory management should be (comparatively) hard. It's not where beginners should start. C makes it far too easy to write code with objectively bad effects on the system, as evidenced by the countless vulnerabilities discovered in critical real-world systems over the last half-century. > after a few rounds with that compiler. I find this phrasing curious. Do you feel that you are fighting the compiler?
- freeopinion 3y agoI know I keep repeating this, but... Rust does not have a massive threshold. Memory management has a massive threshold. Rust just has a lot of safety rails you might keep bumping into if you struggle with memory management. The fact that other languages let you drive off the cliff doesn't mean they have a smaller threshold.
- davidkunz 3y agoI had a hard time with closures when doing async programming. Is that something one should avoid?
- techn00 3y agoI'd love to see a side by side comparison on how someone would use goroutines in Go vs tokio in Rust. Including with closures
- gdcbe 3y agoAsync programming in Rust can still be a bit awkward. They're working hard on it though. https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizin... We'll have Async traits in stable Rust after the summer (normally) and in 2024 they'll start working on supporting async closures :) So if all is well, you're experience should start to become more pleasant next year. For now your closures will either have to return a future (without use of `async` keyword) or you better avoid it for now. Then again, hadn't really had the need for async closures, but I did had a big need for Async traits, as implementing Traits with manual futures was so far a big pain in the ass, and sometimes still not optimal. So I for one, welcome async traits with open arms.
- masklinn 3y agoThe lack of asynchronous closures does make it awkward, as you often need to put an `async move` block inside the closure for things to work correctly, which means the closure becomes an FnOnce and you have to work around that using clones. It’s one of the worst corners of the ecosystem currently, and I’d very much recommend avoiding it.
- z3t4 3y agoAsync code is hard to reason about. Async closures is what I like most about JavaScript (JS) though. Because JS pass all variables by value they get frozen in time when passed to a function. This make async code easier to reason about, as all variables are immutable.
- 3y ago
- KolmogorovComp 3y agoThe content is interesting but the use of ligature really put me off guard at first, and I am very familiar with Rust. I cannot imagine it being good for beginners to grok its syntax. This has reminded me of https://news.ycombinator.com/item?id=35925821 https://news.ycombinator.com/item?id=35925821 recently reposted, which boils down to: > if you’re preparing your code for others to read—whether on screen or on paper—skip the ligatures.
- xmcqdpt2 3y agoYeah I think the ligature debate is a bit tiring frankly but one place where putting ligatures is very clearly a bad idea is in code examples. A blog is not my IDE, I can't turn them off and even worse I don't even know they are on! Can you use a unicode arrow for Rust closures? Is that what this is? I don't know!
- cryptonector 3y agoDo you mean the arrows? As long as they are fixed-width -single or double- it's ok and even pretty, but if variable-width fonts in code is a no-no for me.
- cat-whisperer 3y agoThis reminds me of the time I functional programmed the shit out of `async` (featuring closures). https://github.com/juspay/hyperswitch/blob/16cd32513bc6528e064058907a8c3c848fdba132/crates/common_utils/src/ext_traits.rs#L314-L332 https://github.com/juspay/hyperswitch/blob/16cd32513bc6528e0...
- amelius 3y agoThere's a lot to be said in favor of Rust's approach to memory management but closures in Rust suck compared to garbage collected languages.
- returningfory2 3y agoTrue, but isn't the complexity of Rust closures essential rather than accidental? Fundamentally closures are easy in e.g. Go because you don't have to think about lifetimes, at all. As soon as you capture a variable, the GC guarantees it won't be dropped from underneath your feet. With non-GC'd languages that responsibility moves from the GC to the programmer. The trickyness of using closures in Rust seems largely to be the trickyness of managing the lifetimes of captured variables.
- verdagon 3y agoNot quite, there are non-GC ways to guarantee memory safety without borrow checking, see languages like Vale, HVM (more a runtime), and Inko. With those in mind, Rust's complexity does indeed look accidental here. It has its other benefits, but it does make closures a bit more difficult.
- EnergyAmy 3y agoInko uses automatic reference counting, which you can argue about definitions, but I would consider to be GC. At any rate, it's not relevant to whether Rust's complexity is accidental or not, because Rust specifically doesn't do automatic reference counting, and instead uses the borrow checker at compile time.
- cryptonector 3y agoThat's because Rust wants you to use the stack almost exclusively for memory allocation because that's the obvious way to do automatic memory management w/o a GC. So the moment you want to return closures you have to allocate them on the heap (Box them). Rust is a modern, functional programming language where the programmer works in a straightjacket, and this is most painfully evident the moment you want to use closures like you're programming in Haskell. In Haskell you don't have to think about stack vs. heap allocation. But w/o a way to make it easier for the compiler to choose stack allocation where that would be safe, Haskell ends up being heap-heavy. Rust takes the opposite tack and is stack-heavy, but unlike Haskell Rust forces you to be explicit about the alternative. It's not like Rust doesn't have a GC. Arc is a GC after all. It's just that Rust makes it hard to write natural code using the heap and GC.
- FridgeSeal 3y ago> let add_closure = |a, b| a + b; let sixty_six = add_closure(42, 24); Woah woah, how did it not occur to me that I could just…construct a closure that directly in Rust? This feels like one of those “incredibly obvious and reasonable in hindsight” things. I don’t know why I was labouring under the assumption they could only be invoked in very special and specific places, but it’s good to know I was wrong.
- throwaway894345 3y agoIt becomes more painful when the closure is actually closing over something (especially if that thing is mutable), since the borrow checker gets involved.
- bsder 3y agoThis is a key thing to know, actually. Closures in Rust are different from named functions and that can trip you up. Closures in Rust also close over lifetime information in ways that named functions cannot do. This confused me for quite a while.
- vaylian 3y agoWelcome to functional programming, where functions are values.
- duped 3y agoIf you want to do anything with the closure it gets a little tricker, for example if you did something naive like fn fn_that_takes_closure (f: fn (i32, 3i2) -> i32) { ... } let x = |a, b| a + b; fn_that_takes_closure(x); That would not compile, because `x` does not have the type `fn (i32, i32) -> i32` (that's reserved for "normal" functions). Every closure in Rust has a unique anonymous type, which means if you want to do anything with them besides calling them, you need to understand `Fn`, `FnOnce`, and `FnMut` traits and/or `dyn` trait objects. To write that above you'd need to do something like this: fn fn_that_takes_closure<F> (f: F) where F: Fn(i32, i32) -> i32
- 3y ago
- karmakurtisaani 3y agoThanks, very informative article! Definitely clarifies things like the move keyword, Fn and related traits and how closures are different from function pointers.
- Koshkin 3y agoJust to remind, closures are poor man's objects; objects are poor man's closures
- runevault 3y agoThis is the first time I've understood what the compiler is meaning with FnOnce (that it is moving the captured item back out of the closure, whether by Drop or other means). Glad I read the article now.
- DonaldPShimoda 3y ago> A closure is like an anonymous function. I like the article overall, but I have a bit of a pedantic note about this opening line. Using "like" suggests that there is some similarity that can be used to understand one thing from the other, but in truth these are two distinct (though related) technical terms, and I think phrasing this relationship as a simile may actually obstruct understanding rather than enable it. An anonymous function is just a function without a name. There's nothing particularly exciting about that, except that they only really make sense as a concept within the context of a language with first-class functions (i.e., a language where functions are values that can be passed around as arguments and such). A closure is a function paired with an environment. Technically (meaning "in the academic literature"), this can be any function at all --- named functions can be used to create closures. But in Rust this is not the case, since they only use the term in connection with anonymous functions. This is actually addressed in the first couple of lines of the Rust Book's entry on closures [1]. Saying "a closure is like an anonymous function" would be like me saying "a car is like a set of four wheels". While it is true that the four wheels are integral to the car's identity, it is not the case that these things are directly similar in the way that the word "like" suggests they are. [1] https://doc.rust-lang.org/book/ch13-01-closures.html https://doc.rust-lang.org/book/ch13-01-closures.html
- jerf 3y agoI would further elaborate on your point by saying that as someone who likes to hang out on programming fora and help people as I was helped when I was young, this terminology confusion causes real confusion in the field as well. Many programmers-in-training find it confusing as to why whether or not a function has a name seems to be tied to whether or not it has an environment (not that they phrase it that way but that's what it boils down to), and my usual answer is to explain something much like your post; the resolution is, there is no such connection. I have fielded this question in one form or another many times. "Anonymous function" should not be used as a synonym for "closure". Generally I will follow along with the dominant language terminology if I am clearly in the context of a very particular language, but may include why I don't like the language community's terminology if it helps my point.
- 3y ago
- kazinator 3y agoWhy do I need to see machine-specific types like i32 in a lecture about closures?
- proto_lambda 3y agoWhat is machine-specific about i32? It's a signed 32-bit integer, regardless of the machine you're on.
- kazinator 3y agoWhy not use i36 then?
- proto_lambda 3y agoBecause that's not a type in Rust. I don't understand where you're going with this.
- kazinator 3y agoWhy isn't that a type in Rust? It's a signed 36 bit integer, regardless of the machine you are on.
- tialaramex 3y agoThere are several crates which provide this type if you want it, such as ux and num_x
- deleted 3y ago[deleted]
- deleted 3y ago[deleted]
- winrid 3y agoBlocks with return values are super useful too!