9 ms·
Rust's Ugly Syntax (2023)
- sedatk 2y agoI think the article makes a good point, but the actual example isn’t Rust’s worst, not even close. It gets really hard to follow code when multiple generic types are combined with lifetime markers. Then it truly becomes a mess.
- hckr1292 2y agoAgree about the example! I can't tell if this article is tongue-in-cheek or earnest. I'm unclear on the point the author is trying to make.
- tcfhgj 2y agoThe author explains it in the first sentence, i.e. not the syntax of lifetimes may be your problem, but the feature itself
- zarzavat 2y agoMy reading is: people use Rust because it’s fast but then they complain about the semantics that make it fast. In other words, be careful what you wish for. Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. However, once you deviate from the goal of “as fast as possible”, then you have to choose which parts you want to sacrifice speed for productivity. Like Excel, everybody agrees that Rust is too complicated but nobody can agree on which 10% to remove.
- d_tr 2y ago> Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. D maybe? D and Rust are the two languages which come to mind when I think about "possible C++ replacements".
- cdogl 2y agoWhen GP said “most”, I interpreted it more broadly. Most applications simply do not require the guarantees of a non-GC language. When you expand that horizon, list of contenders becomes considerably larger - even when restricted to statically typed languages.
- zarzavat 2y agoYes for example many Python users switched to Go, a native code GC language, and are satisfied with the performance. There’s also the middle ground of Swift’s memory management which uses compiler-elided refcounting - i.e. the compiler detects when a count goes up then down again and removes those operations.
- dwattttt 2y ago> There’s also the middle ground of Swift’s memory management which uses compiler-elided refcounting - i.e. the compiler detects when a count goes up then down again and removes those operations. In the face of threading that's not a safe optimisation; if another thread decrements the refcount inbetween those two removed operations, boom. The compiler will have to track every variable that crosses threads or something. EDIT: spelling
- sk11001 2y ago> people use Rust because it’s fast but then they complain about the semantics that make it fast. I don't think most people use Rust because it's fast - fast is nice but Rust is being thrown at a bunch of use cases (e.g. backend services and APIs) for which it replaces "slower" garbage collected languages (the language being faster doesn't always make the overall product/service faster but that's a separate question). What Rust gives you is a viable potential alternative to C and C++ in places where you absolutely can't have a GC language, and that's a huge deal, the problems and confusion start when people try to use Rust for everything. > everybody agrees that Rust is too complicated I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion. > Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. True, and such languages already exist and are widely used, Rust doesn't need to fit that use case.
- zerodensity 2y ago> I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion. Personally I feel it's not complicated enough. Where is my function overloading, variadic templates and usable compile time reflection? (Sure you can sometimes use macros but ew macros)
- swiftcoder 2y ago> Personally I feel it's not complicated enough. Where is my function overloading, variadic templates and usable compile time reflection? (Sure you can sometimes use macros but ew macros) Indeed. Rust is really crying out for a real CTFE implementation + richer macros to replace the mess that is procmacros (I really don't want to have to run an arbitrary external binary with full system access just to manipulate the AST...)
- zarzavat 2y ago> I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion With any language there’s an active part of the community and then there’s the “dark matter” of people who use the language but are not actively involved in shaping its direction, forums or subreddits, etc. Of course the people who are actively involved are likely to be of the opinion that all the complexity is necessary, but I doubt that applies to the broader Rust userbase.
- LoganDark 2y agoI always, always forget what `'a: 'b` means, because I remember it always being the opposite of what I think it is, but memorizing that obviously doesn't work because then it will just flip again the next time. It's so annoying.
- nrabulinski 2y agoI always describe it to myself this way - T: Foo means T is a superset of Foo (because it at least implements Foo but most likely more) thus 'a: 'b means 'a is at least as wide as 'b, and possibly wider
- delifue 2y agoIn lifetime, subtype means longer lifetime (it's unintuitive). 'a : 'b means 'a is a subtype of 'b, which contains 'b and can be longer. Rust can improve this by introducing syntax like `'a contains 'b`
- runiq 2y agoIf you think of it as 'a implements b', it makes sense for both lifetimes and (other) subtypes: If lifetime `a` implements `b`, it is obviously valid for `b` (and maybe longer).
- LoganDark 2y agoI always do `<T, U extends T, V extends U>` etc for generics, but for lifetimes it's `<'a: 'b, 'b: 'c, 'c>` which always trips me up...
- lifthrasiir 2y agoIt was a good signal to me that you are overthinking into the architecture if that is really required. Rust makes something pretty much impossible in C/C++ possible, but not necessarily easy, and that would be one such example.
- LoganDark 2y ago> It was a good signal to me that you are overthinking into the architecture if that is really required. Sure, maybe I don't need to statically guarantee the correct execution of code that could easily just throw at runtime instead, but it sure is a fun hobby.
- namjh 2y agoIMHO the mentioned examples of complexity like multiple type variables and lifetimes with bounds are for who "really" wants compile-time contracts. These are mostly opt-in so higher level use cases(like writing backend business logics) should not care about that, just wrapping everything with Boxes and Arcs. Of course Rust is not perfect; there is some 'leakages' of low level aspects to high level like async caveats(recursion, pinning, etc.). I'm not sure how these can be avoided. Maybe just trial-and-errors for all..?
- Hamuko 2y agoI do remember the compiler constantly suggesting lifetimes to me as a newcomer to the language, so it didn't really feel that opt-in. Quite a lot of the suggestions also started to look like someone poured alphabet soup all over the code.
- scotty79 2y agoThat's mostly because borrows are a curiosity of Rust that newcommers are quickly introduced to while they are mostly just a perfomance gimmick. If you come to rust from high level language you can just do everything with Rc and cloning. It's still hard because Rust, in opposition to every pipular language, is a value oriented language. But at least you won't have much contact with alphabet soup.
- namjh 2y agoThat's because the code triggering compilation error is using reference. If you use Rc or Arc (which pays runtime cost) there should be no lifetime at all. Albeit I admit there somewhat exists a community sentiment like "if you use Rust, you should maximize its zero cost abstraction feature so lifetime is good and generics good", and my (minor) opinion is that, it's not always true to all users of Rust. And the clumsy Arc<Mutex<Something<TheOtherThing>>> makes users feel bad about using runtime cost paid types. Maybe we should introduce easy Rust dialect which transpiles into Rc/Clone everywhere but I doubt it's trivial to transpile.
- dmurray 2y ago
- yulaow 2y agothrow in some async too and I really lose myself most of the times
- WhyNotHugo 2y agoHere's a nice example of a Trait that has async functions: fn list_items<'life0, 'life1, 'async_trait>( &'life0 self, collection_href: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, Rendered docs: https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorage/base/trait.Storage.html#tymethod.list_items https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorag... Source: https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta0/item/vstorage/src/base.rs#L79 https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta...
- the_mitsuhiko 2y agoIt's worth pointing out here for people not familiar with Rust, that this is the result of code generation by a third party crate to enable async methods on traits.
- Filligree 2y agoWhich isn't even needed anymore; now the compiler accepts it without any macros.
- WhyNotHugo 2y agoThe compiler only supports these for static dispatch, the above use case relies on dynamic dispatch.
- Dagonfly 2y agoWell, you can't use the `async` keyword version if you need the `Send` bound. Imo, Rust should introduce some syntax like `async(dyn+Send)` that desugars to `Box<dyn Future<Output = bla> + Send>`. This solves most of the async wards if you don't care about heap allocation and perfect performance.
- ahahahahah 2y agoThat's partially not true. You can use a non-async form in the trait definition to require the Send bound and then the trait impls can use the async form. See https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=d55f089dc3de8df08cf0fe5b2c8d0e2f https://play.rust-lang.org/?version=nightly&mode=debug&editi...
- nazka 2y agoThat's why I am a huge fan of Rust but at the same time at the end of the day all I want is the features of the language that Rust has minus the memory management and a GC. That would be my dream language. If only ReasonML/Rescript were more popular... Or I guess Elixir
- sedatk 2y agocheck out Google’s Carbon :)
- coryfklein 2y agoScala
- neonsunset 2y agoIt's not necessarily identical to what you are looking for per se but a mix of C# and F# will be the closest overall to Rust in terms of performance, access to systems programming features, language expressiveness and tooling experience. Cons are: - C# has OOP (you don't have to use it heavily) - No Hindler-Milner type inference in C#, nested generic arguments may need to be specified by hand - Smaller amount of supported targets by CoreCLR: x86, x86_64, arm, arm64 for ISAs and Linux, Windows, macOS and FreeBSD for OSes. NativeAOT-based support is experimentally available on iOS, and is undergoing further work. As you can imagine, LLVM targets absolutely everything under the sun and above it too. no-std story in Rust is first-class. C# has bflat and zerosharp but they are niche. - In C#, type unions will only be available in one of the coming versions. F# to the rescue - Error handling is a combination of exceptions and e.g. int.TryParse patterns, there is no implicit returns with ? like in Rust - Lack of associated types and the types own their interface implementations unlike traits which you can introduce without control over the source. This results in having to implement wrapper types (even if they are structs) if you want to modify their interface implementations - You only control shallow immutability - readonly struct will not prohibit modification of the contents of a Dictionary<K, V> that it holds - async/await is more expensive - Big popular libraries often have features or implementation incompatible or not guaranteed to work with native compilation via NativeAOT - Object reference nullability (e.g. 'T?') happens at the level of static analysis i.e. does not participate in type system the same way Option<T> does in Rust Pros are: - Has FP features like in Rust: high order functions, pattern matching (match val -> val switch { , also 'is'), records, tuples, deconstruction - Fast to compile and run, AOT not so fast to compile but tolerable - If you know how to use Cargo, you know how to use .NET CLI: cargo init -> dotnet new {console, classlib, etc.}, cargo run -> dotnet run, cargo build -> dotnet build/publish, cargo add {package} -> dotnet add package {package} - Monomorphized structs generics with the same zero-cost abstraction assurance like in Rust, &mut T -> ref T, &T -> ref readonly T, sometimes in T but with caveats - Box/Arc<T> -> class or record, Arc<Mutex<T>> -> class + lock (instance) { ... } - Easy to use async/await but without ever having to deal with borrow checker and misuse-resistant auto-scaling threadpool. Task<T>s are hot started. Simply call two task-returning network calls and await each one when you need to. They will run in background in parallel while you do so. While they are more expensive than in Rust, you can still do massive concurrency and spawn 1M of them if you want to - Built-in Rayon - Parallel.For and PLINQ, there is Channel<T> too, you can e.g. 'await foreach (var msg in chReader) { ... }' - Iterator expressions -> LINQ, seq.filter(...).map(...).collect() -> seq.Where(...).Select(...).ToArray(), unfortunately come with fixed cost but improve in each version - Rust slice that wraps arbitrary memory -> Span<T>, e.g. can write the same fast idiomatic text parsing on top of them quite easily - Stupid fast span routines like .IndexOf, .Count, .Fill, .CopyTo which use up to AVX512 - Compiler can devirtualize what in Rust is Box<dyn Trait> - Can make small native or relatively small JIT single-file executables that don't require users to install runtime - Rich and fast FFI in both directions, can statically link into Rust, can statically link Rust components into itself (relatively new and advanced) - Great tooling, Rider is very good, VSCode + base C# extension about as good as rust-analyzer - Controversial but powerful runtime reflection and type introspection capability, can be used in a very dynamic way with JIT and compile additional code on the fly - A bit easier to contribute to, depending on area owner and project (runtime, roslyn, aspnetcore, ...) - CoreLib has full-blown portable SIMD API that is years ahead of portable-simd initiative in Rust Because I occasionally use Rust and prefer its formatting choices, I carry this .editorconfig around: https://gist.github.com/neon-sunset/c78174b0ba933d61fb66b54d123de00d https://gist.github.com/neon-sunset/c78174b0ba933d61fb66b54d... to make formatting terser and more similar. Try it out if K&R and `I` prefix on interfaces annoy you.
- AxelLuktarGott 2y agoIs it really better to remove the error case information from the type signature? Aren't we losing vital information here?
- treyd 2y agoThe std::io error type is defined roughly as: type Result<T> = std::result::Result<T, io::Error>; So it's actually fine, since we're specifying it's an IO result. This is a fairly common pattern.
- eterps 2y agoJust give me Rattlesnake or CrabML and I'll stop complaining :-)
- awesomebytes 2y ago+1
- stavros 2y agoWhat's Rattlesnake? I can't find anything at all online.
- frankie_t 2y agoWanted to say the same. He straight up conjured a good looking code in CrabML as an example of similar level of "ugliness", while it has about three times less syntax noise.
- hyperpape 2y agoIt ends in ";;". If that's not a typo, and it's semantically significant that there are two semicolons instead of one, that sounds quite finicky.
- Joker_vD 2y agoThe answer to the question “When do I need the ;; within OCaml source code?” is never. It's not a part of the language and is only used by the interpreter as an end of input mark. Historical note: In CAML Light, the predecessor of OCaml, double semicolons were mandatory. For this reason they are quite common in old code originally written in CAML Light or written in the early days of OCaml. These days they are considered a bad style. from https://baturin.org/docs/ocaml-faq/#the-double-semicolon https://baturin.org/docs/ocaml-faq/#the-double-semicolon
- oguz-ismail 2y agoThe final version is still ugly. Why `pub fn'? Why is public not the default and why do you have to specify that it's a function? Why `: type' and `-> type', why can't type go before the identifier? Why do you need `File::' and `Bytes::'? What is that question mark? Why does the last statement not need a semicolon? It's like the opposite of everything people are used to.
- atoav 2y agoAs someone who doesn't think it is pretty, but knows Rust I went through all your points and let me assure you except for the one where you wonder why the syntax can't be more like C/C++ where it comes down to taste, all of your questions have an answer that really makes sense if you understand the language. E.g. making pub default is precisely the decision a language would make that values concise code over what the code actually does.
- janalsncm 2y agoDefinitely agree, “pub” was one of the design decisions I loved learning Rust. If you forget to add it, you’ll get a compiler error. But if pub was default, I’d be exposing code unnecessarily. And no need for a separate private keyword, the absence of pub is sufficient. The same reasoning works for “mut” as well. That said, I don’t like Rust’s syntax. Especially once you get to lambdas, things get hard to read.
- pta2002 2y agoShort answer for the type ordering and `fn`: because C/C++/Java tried that type of syntax and the result was an ambiguous grammar that is way too hard to parse, not to mention C's overly complicated pointer syntax.
- uasi 2y agoYour points have nothing to do with ugliness. > Why `pub fn'? Why is public not the default and why do you have to specify that it's a function? If public were the default, you'd end up having to make other functions `priv fn` instead. > Why `: type' and `-> type', why can't type go before the identifier? It's easier to parse, and most major typed languages other than C/C++/C#/Java put the type after the identifier. > Why do you need `File::' and `Bytes::'? Seriously? > What is that question mark? The final version doesn't use a question mark. > Why does the last statement not need a semicolon? This is a legitimate question. In Rust, the last statement without a semicolon becomes the return value.
- mjburgess 2y agoKinda disingenuous, you don't reskin one language in another to make an argument about syntax -- you develop a clear syntax for a given semantics. That's what rust did not do -- it copied c++/java-ish, and that style did not support the weight. When type signatures are so complex it makes vastly more sense to separate them out, Consider, read :: AsRef(Path) -> IO.Result(Vec(U8)) pub fn read(path): inner :: &Path -> IO.Result(Vec(U8)) fn inner(path): bytes := Vec.new() return? file := File.open(path) return? file.read_to_end(&! bytes) return OK(bytes) inner(path.as_ref())
- demurgos 2y agoPeople may disagree on specifics, but you're definitely right that being able to separate the function signature from its definition would be very helpful in complex cases.
- scotty79 2y agoWhy? You can easily find parameter names in the signature if you just put them on separate lines. For me there's very little reason of putting them all together on separate line after the signature. And then when you look for a type of a parameter you know the name of, it gets difficult.
- tcfhgj 2y agoTo me this example is not more clear than normal Rust
- remcob 2y agoWhy stop there and not go all the way to pub fn read(path: Path) -> Bytes { File::open(path).read_to_end() }
- oneshtein 2y agoHow to return an error in your example?
- tcfhgj 2y agoThrow an exception proving the point of the article even further
- gary_0 2y agopub fn read(path: Path) -> Result<Bytes> { File::open(path)?.read_to_end() } isn't so bad either.
- remcob 2y agoExactly, and this is in my experience what most Rust code ends up looking like. It compromises a bit on generality and (potential) performance to achieve better readability and succinctness. Often a worthwhile trade-off, but not something the standard library can always do.
- MetricExpansion 2y agoIf I understood all the semantic properties, including the separate compilation requirements, correctly, here’s how I think it would be done in Swift with the proposed nonescapable types features (needed to safely express the AsRef concept here). (Note that this doesn’t quite compile today and the syntax for nonescaping types is still a proposal.) @usableFromInline func _read(pathView: PathView) throws(IOError) -> [UInt8] { var file = try File(pathView) var bytes: [UInt8] = [] try file.readToEnd(into: &bytes) return bytes } @inlinable public func read<Path>(path: borrowing Path) throws(IOError) -> [UInt8] where Path: PathViewable, Path: ~Copyable { try _read(pathView: path.view()) } // Definitions... public enum IOError: Error {} public protocol PathViewable: ~Copyable { func view() -> PathView } public struct PathView: ~Escapable {} public struct File: ~Copyable { public init(_ pathView: borrowing PathView) throws(IOError) { fatalError("unimplemented") } public mutating func readToEnd(into buffer: inout [UInt8]) throws(IOError) { fatalError("unimplemented") } }
- wiz21c 2y agoFor my own situation, the articles present the right way to express all possible performance/error handling (which is expected in a standard lib) and then goes on to show how I actually code it in my own softawre where I don't really need the level of detail/finetuning of the standard lib. Interestingly, my life starts at the end of the article, with the simple verison of the code, and as my understanding of rust widens, I go up to the beginning of the article and better define my function...
- awesomebytes 2y agoI've only learned a tiny bit of Rust, and I feel the same. Going from the bottom up, makes it all make so much sense. (Albeit I still like the Rattlesnake syntax haha)
- macmac 2y agoMy hot take is that Rust should have been a Lisp. Then it could also have had readable macros.
- kzrdude 2y agoWhat if Rust was an OCaml-ish.
- deleted 2y ago[deleted]
- jiwangcdi 2y ago> The next noisy element is the <P: AsRef<Path>> constraint. It is needed because Rust loves exposing physical layout of bytes in memory as an interface, specifically for cases where that brings performance. In particular, the meaning of Path is not that it is some abstract representation of a file path, but that it is just literally a bunch of contiguous bytes in memory. I can't understand this. Isn't this for polymorphism like what we do this: ```rust fn some_function(a: impl ToString) -> String { a.to_string(); } ``` What to do with memory layout? Thanks for any explanation.
- K0nserv 2y agoRust needs to know the exact size, layout, and alignment of every argument passed to a function to determine how it gets passed(register(s) or spilled to stack) and used. For example PathBuf and String can both be turned into a reference to a Path, and while they have the same size their layout and implementation of `as_ref` differ. As for `impl`, fn foo(a: impl ToString) is syntactic sugar for fn foo<S: ToString>(a: S) The reason the standard library doesn't use this is because the code predates the introduction of `impl` in argument position. The reason the function takes `AsRef<Path>` instead of `&Path` is callsite ergonomics. If it took `&Path` all callsites need to be turned into `read(path.as_ref())` or equivalent. With `AsRef<Path>` it transparently works with any type that can be turned into a `&Path` including `&Path` itself.
- jiwangcdi 2y agoThen if Path is not about abstraction, why not use a raw byte slice like &[u8]
- K0nserv 2y agoThat's orthogonal. If the type was `&[u8]` instead of `Path` the type signature would be: pub fn read<P: AsRef<[u8]>>(path: P) -> Result<Vec<u8>> The reasons for it to be generic and us `AsRef` remain. The reason for Path over &[u8] is, AFAIK, because not all byte slices are valid paths on all OSs, but also because a dedicated type lets the standard library add methods such as `Path::join`
- anonymous2024 2y agoI wonder. How does Rust syntax compares with https://www.hylo-lang.org/ https://www.hylo-lang.org/ syntax? That also is memory safe, typesafe, and data-race-free.
- pieresqi 2y ago[dead]
- mgaunard 2y agoThere are several problems with the C++ variant, which could have been easily avoided by just following the original Rust more closely.
- apatheticonion 2y agoSomeone needs to tell them about async Rust. Big yikes.
- carlmr 2y agoI'm a big Rust fan, but async Rust is an abomination.
- iknowstuff 2y agoI love async Rust. Its implementation is marvelous. Sueper plasant to write now that * async closures * async trait fns, * `impl Trait` everywhere are in place.
- apatheticonion 2y agoWait up, are async closures in stable now? Would certainly help with the wild return and type signatures I've had to write involving combinations of `Pin` `Box` & `Future<Output = Result<...>>`. I think Rust futures are really awesome as they expose the lower level details of how async behavior works, giving the developer the flexibility to adapt it to their use case - but the standard library really dropped the ball on standardized usage/types. For instance, can we please just have `AsyncRead` and `AsyncWrite` traits? Can they also be easy to implement (are just `async read()`)? Right now you have to use adapters between Tokio types and the Futures crate and they both offer their own non-interoperable read/write traits.
- iknowstuff 2y agoIt’ll be a nice day when AsyncRead/AsyncWrite lands in stable but I think we should appreciate that it didn’t happen hastily. Now that Linux has uring, Windows has IOCP etc we need to pass ownership of buffers to the kernel instead of passing by reference. Edit: we use nightly Rust at work so I’m able to use async closures no problem but they’re not quite in stable just yet.
- carlmr 2y ago
- Woshiwuja 2y agoSo you just end up with python at the end?
- qalmakka 2y agoPeople that complain about Rust's syntax never have never seen C++ at its worst
- olologin 2y agoC++ is relatively easy to read. The only troubles I had is reading STL's sources because of all _ and __ prefixes, and understanding template errors from compiler, but that will soon be fixed with concepts.
- qalmakka 2y ago> C++ is relatively easy to read Only if you suffer from a very high level of stockholm syndrome, that is. Rust's syntax is vastly clearer than C++ in basically all circumstances.
- tmtvl 2y agoAw, no Rasp variant? Let's brainstorm it up... (defun read (path) (declare (generic P (AsRef Path)) (type P path) (returns (io:Result (Vector U8)))) (flet ((inner (path) (declare (type (Ref Path) p) (returns (io:Result (Vector U8)))) (try-let ((file (File:open path)) (bytes (vector))) (declare (mutable file bytes)) (try (read-to-end file bytes) (Ok bytes))))) (inner (as-ref path))))
- tevelee 2y agoThe article just turned Rust into Swift. Nicer syntax, same semantics
- singularity2001 2y ago"I think that most of the time when people think they have an issue with Rust’s syntax, they actually object to Rust’s semantics." You think wrong. Rust syntax is horrible because it is verbose and full of sigils
- librasteve 2y agoHere's the cleaned up version of Rust from the OP: pub fn read(path: Path) -> Bytes { let file = File::open(path); let bytes = Bytes::new(); file.read_to_end(bytes); bytes } Here is is in raku (https://raku.org https://raku.org): sub read(Str:D $path --> Buf:D) { $path.IO.slurp: :bin } [the `--> Buf:D` is the raku alternative to monads]
- sedatk 2y agoThen it’s just this with C#: public byte[] Read(string path) => File.ReadAllBytes(path); I think the article’s trying to explain a concept using an arbitrary piece of code from stdlib, not necessarily that specific scenario (opening and reading all bytes from a file).
- neonsunset 2y agoThis. In general, standard library is a poor example as it has to serve very wide range of scenarios, be robust to many environmental conditions when IO is involved and perform optimally. If it's terse enough while doing so, it's good enough already.
- librasteve 2y agowell no need for the sub wrapper really say $path.IO.slurp: :bin