Follow

I checked out the programming language by writing some code in it for the first time. I'm deeply disappointed. If you open almost any project written in Rust you'll see a lot of ".unwrap()"`s (very often for "Option<T>"), that's how most of the people write in Rust. But this is a partial function for many of the types, compiler will give you no warnings about that. This is just a new era of "NullPointerException". Rust is trying to sell the idea of "safety" but it fails to deliver it.

· · Web · 3 · 0 · 3

@unclechu Full disclosure: I believe Rust does improve over C and C++ in most areas.

But this is a partial function for many of the types, compiler will give you no warnings about that.

I believe it's partial for all types, because it always panics on None.

Trivial warnings like this are always the first one to get disabled, so I don't see how it would improve the situation. It's hard to imagine someone who writes in Rust yet doesn't know that unwrap() and similar functions can panic.

Also, not all calls to unwrap() are actually dangerous. I bet you've ran into situations where the language is simply not expressive enough to encode all the invariants that you want to uphold. That's where all these "_ -> error "can't happen"` come in.

This is just a new era of "NullPointerException".

Perhaps, but it's slightly better this time around: we can grep for all the places where panic may occur. With NULL, this stuff was very hard to trace, because grep NULL gave you a list of places where an error could originate, not a list of places where it could manifest.

Same with pointer arithmetic: by wrapping that stuff in unsafe blocks, Rust makes it far easier to audit than any other language.

I mean, it's still a problem, and a whole program's guarantees can be broken by a single buggy unsafe block, but it's still way better than what we had before.

Rust is trying to sell the idea of "safety" ...

It sells memory- and thread-safety specifically, not safety from partial functions. C'mon, I know you, you're better than this; read some actual docs, not overviews that generalize everything to the point of uselessness.

... but it fails to deliver it.

Just like with any other language, Rust's strong points can be subverted by the programmer. Are you aware of Turing-complete languages that can't be misused? I don't believe such a thing can ever exist.

@minoru @kornel

For instance in (in the "base" library) there are non-total "fromJust", "head", etc. But:

1. Those are historical decisions made in the past century ( is a modern one, it shouldn't have that historical luggage), nowadays maintainers wouldn't implement it without wrapping result into "Maybe".

2. There are libraries such as "safe" which provide total equivalents.

3. No practicing haskeller would use them. The opposite tendency takes place in the community.

@minoru @kornel I also can share the same concerns about . I recently checked it out after few years of interruption. I always say is a complete shit, the same I used to say about TS too. But to this day the type system of TypeScript has significantly improved and I really like it. But no way I'm going to participate in a project written in it, just because most of the people who use it (its community) doesn't understand and value its benefits.

@unclechu You can ban .unwrap() in your codebase if you want, with clippy:

rust-lang.github.io/rust-clipp

In practice I don't see unwrap happening nearly as often as NPE. nulls are implicit, and NPE's tend to come as a surprise. Unwrap is intentional and explicit.

Rust has robust tools to avoid using unwrap, so if you find it in production-quality code it's usually because the author believes it can't possibly fail, so it's used more like `assert()`.

@kornel @minoru I have to clarify my point. My main concern is that it's how most of the developers do it, that's the most horrible part. Some bad ways of doing something can be banned by a code style guide, by code review procedure or whatever. But the main tendency is ruining it. And the fact that adds ".unwrap()" without a caution and without any discouraging of using it nowadays is a design failure.

@unclechu You may have wrong impression about `unwrap()` being common and not discouraged.

Maybe it's because it appears frequently in 1-liner code examples that don't want to add noise of real error handling?

unwrap is actively discouraged in real codebases that aren't lazy toy examples. Real Rust code uses `if let`, `?`, etc.

@kornel Actually no, I said "almost any project". Let's check the codebase of few projects I personally use.

find . -name '*.rs' | xargs cat | rg -F '.unwrap()' | wc -l

1. skim - 63
2. neovim-gtk - 144
3. ripgrep - 510 (sic!)

These are not 1-liners, useful applications for daily use. "Real code".

@unclechu See how easy it was? How many man-years would you need to come up with a similar analysis of, say, GNU grep? :)

The count for skim is low enough that you can look through all of them and decide if they're really dangerous. I'm not familiar with that project, but I'd bet that 95% of those are actually "can never happen" type, likely with comments that explain the reasoning.

@kornel

@minoru @kornel I personally hate when people make assumptions (like "it's okay to 'unwrap' it here, I have checked it above") instead of relying on software, on the type system. The fact it's easier to debug doesn't make it much better to me. If such debugging would be automatized by static analysis then this would be better.

@unclechu But how do you express in the type system that `Regex::new("[a-z]")` can't fail?

@kornel Very easily, a regular expression syntax is just a eDSL for writing parsers. See attoparsec library for instance: hackage.haskell.org/package/at

In Haskell you can for instance write a quasi-quoter or just a template function that would transform some regex string into a parser in compile-time (and fail if something is wrong with it).

@minoru @kornel Consider how it's implemented in (if you tell me a year ago I'd use TypeScript as an example of something good I would laugh) if you have "const x: SomeType | null = smth" and you check runtime value like this "if (x !== null)" then inside this "if"-block or after it (if you interrupt somehow inside that block) you have implicit cast to "SomeType". I wish would make sure it's okay to unwrap if you have ".is_some()" check before an ".unwrap()".

@unclechu Rust has that too:

if let Some(x) = x {
// `x` here is already unwrapped
}

@kornel

@minoru @kornel Perfect, I only wish the community would never use ".unwrap()" but this "if let" or "match", whatever. And I wish compiler would discourage you from using ".unwrap()". I wish it would be possible only in an "unsafe" block or something similar like "partial" (e.g. "partial { x.unwrap() }").

@kornel That's all nice, I got it. But it's not the point that it's possible to avoid it. The point is that it's not discouraged by the compiler and that is how many developers do it. If a new release of compiler would scream on you for using ".unwrap()" and recommend you to replace it with more idiomatic solution I'll take my words back and will change my mind.

@unclechu I'm looking at ripgrep, and I don't see any case where it's misused.

It unwrap() in tests, where it's meant to fail loudly.

It uses it in examples for things that are not the point of the example.

It uses for `mutex.lock().unwrap()` which was a bad API design. If your other threads have already crashed, how do you even handle this other than by aborting?

It uses in places where it obviously can't fail, like `regex::new("[a-z]").unwrap()`

@kornel
> It uses in places where it obviously can't fail, like `regex::new("[a-z]").unwrap()`

I repeat one more time that I hate when people make assumptions that they're right when they can rely on software (type system, tests, whatever). "It obviously can't fail" is one of such assumptions. A human can make a typo very easily. A typo which is hard to notice at first sight.

@unclechu This is not an assumption, but an assertion.

@kornel An assertion made in a human mind (not in the code) I call an "assumption". There's no assertion in the code which proves that ".unwrap" is okay to the type checker in .

@unclechu It's an assertion in the sense that if it's wrong and the author is mistaken, the code will panic.

Unwrap doesn't violate type safety. Unwrap doesn't allow skipping of any checks.

It's merely a shorthand for:

match x {
Some(x) => x,
None => panic!(),
}

@unclechu or
if let Some(x) = x {…} else { panic!() }

or in TypeScript:

if x != null {…} else { throw "panic" }

Haskell has runtime errors too.

@kornel
> Haskell has runtime errors too.

Yes, it was me who mentioned it from the beginning. But the use of such techniques are discouraged and are rather historical luggage which is avoided by a practicing haskeller. The community isn't ruining it.

> or in TypeScript:

In if you check for null after that line of code nullability is eliminated from your type and you can work with it as it's a non-nullable type. is really missing that feature of static type checking.

@unclechu You've been told twice already that Rust does the same thing with `if let`, but better. TypeScript's null checks, or type checks in general, aren't even sound.
Rust's type system is sound. `null` is not used (except in `unsafe {}` for C interop), and `if let` is a stronger and more explicit check than TypeScript's.

@unclechu And `if is_some() { unwrap() }` is an officially discouraged antipattern, and there is a warning against doing this.

@unclechu Again, `if let Some() {}` is the "typescript" thing for Rust. If you don't know it, you haven't even finished reading the first few chapters of the Rust book.

@kornel
> You've been told twice already that Rust does the same thing with `if let`, but better.

And you've been told much more many times that it's not the point that it's possible to avoid ".unwrap()".

> TypeScript's null checks, or type checks in general, aren't even sound.

Maybe aren't sound but works perfectly in the sense of protection from human-factor. Works in many ways, including partial checking for union types, with `switch` cases, exceptions handling, etc.

@unclechu Yeah, I don't think we'll come into agreement when you think TypeScript's heuristics work perfectly for human-factor, but sound algebraic data types don't, and that halting problem is a legacy issue of haskell.

@kornel
> Rust's type system is sound.

I don't know, I'm not sure if it can provide zero-cost row-polymorphism provides.

> and `if let` is a stronger and more explicit check than TypeScript's.

How is it in any way stronger please tell me? More explicit? Maybe. But on the other hand TypeScript's checks are more polymorphic, can be applied to more types which has the same form/properties/etc.

@unclechu Rust uses nominal typing, except tuples which are structurally typed. It has zero-cost type-based generics via compile-time monomorphisation. Plus ADT, roughly similar to TS union types, but without holes.

Keep in mind that TS is not sound, and still relies on JS to catch unsoudness, so its polymorphism can't be entirely zero cost at runtime. Rust's worst case (trait objects) is roughly as costly as the best case of hidden class optimization in JS VMs.

@unclechu a/type-based/trait-based.

Anyway, it's all in The Rust Book.

@kornel
> still relies on JS to catch unsoudness, so its polymorphism can't be entirely zero cost at runtime

Sure, I only meant that it's zero-cost in sense it doesn't add anything in runtime on top of JS. Of course JS is definitely not a zero-cost environment, and JS as-is (without layer) is still an essence of crap.

@unclechu "essence of crap"? You're judging quite harshly. There's no need to shit on other's work like that.

@kornel I think there's actually a need. Many people think it's a solution for everything, it's definitely not. It historically fails to do anything right and safe just because it supposed to be used as few-liner additions to the web pages. Not to build big, complex and sensitive to human-factor desktop applications. And it still carries all the historical luggage of the tool developed in a couple of weeks in a rush just to work somehow. I think our responsibility is to discourage that.

@kornel Okay, let me be more specific. It's *runtime assertion*, not compile-time one, it's a completely different case. It's absolutely possible to statically check it, that's what does for instance.

@unclechu Can you give example of something that TypeScript can check at compile time, for which Rust requires unwrap()?

@kornel NP

In :
if (x.is_none()) panic!()
x.unwrap()

There's no evidence for the type checker that ".unwrap()" is safe, even there's a check right above it.

In :
const x: whatever | null = undeterminedValue
if (x === null) return
const y: whatever = x

Is okay, "|null" is eliminated from "x" after check (or even if a value is determined as not null in compile-time), if you remove that assertion you get a compile-time error (remember to use --strict option).

@unclechu @kornel this is how you do it in Rust, if you want to do something when x has a value:

if let Some(x) = maybe_x {
// ... use x
}

which is a shorthand for an explicit match:

match maybe_x {
Some(x) => // ... use x,
None => { },
}

if you want to *panic* in case it's None, you can write

let x = maybe_x.unwrap();

which is a shorthand for

match maybe_x {
Some(x) => // ... use x,
None => panic!(...),
}

Doing an explicit check, panicking, and *then* using unwrap() again is indeed wrong.

@kornel
> Unwrap doesn't violate type safety. Unwrap doesn't allow skipping of any checks.

Now I see the deeper problem. In everything is "IO" in terms of . In Haskell it's okay to fail in "IO", this is the right place for an intended runtime failure, on the most top layer of a functional composition. But it's definitely not okay in Haskell to do everything inside "IO". Actually the opposite, "use IO as less as possible" is the way. Side effects are allowed everywhere in Rust. 🤔

@unclechu @kornel it's not a "problem", it's just a different language design decision, one most mainstream languages follow. Rust is a systems programming language, after all.

Bonus point: async functions / Future's are very much like IO in Haskell.

@bugaevc @unclechu @kornel Unrestricted effects *is* a problem.

Just a different design" is how languages try to deal with it.

Erlang folks had to forbid custom functions in conditionals because of that - a severe solution for a severe problem.

@dpwiz @unclechu @kornel care to elaborate? How is it a problem that any function can do, say, debug logging?

Of course it's not great if a supposedly pure function goes and does some HTTP requests, but there are far more effective ways of preventing that (common sense) than encoding that into the type system, which is what FP purists advocate for.

Compare: it's not easy to track whether everything a function touches is thread-safe, which is why Rust has Sync & Send.

@bugaevc @unclechu @kornel debug logging is an escape hatch, that "FP purists" use sparingly too. Better think about putting SQL or something like that into IFs, then tracing nasty heisenbugs.

@dpwiz @bugaevc @kornel I don't know what "FP purists" you're referring to but there are plenty of good abstractions for logging without any "escape hatches", MonadLogger in for instance.

@unclechu @dpwiz @kornel that's what I'm saying, I'm not seeing which problem MonadReader (or any such "solution" for logging) solves: it should be fine for any function to do debug logging, and there's no need to have such functions specially marked in the type system. Right?

@bugaevc @dpwiz @kornel I don't see how MonadReader would solve logging problem (only if you read some "channel" entity for logging from something). You probably meant MonadLogger. And no, not right, it shouldn't be fine, definitely not. MonadLogger is an abstraction it doesn't really "log" anything, it only provides an interface for do some "logging". How do you log depends on an implementation (of a type) you're applying a function on which depends on MonadLogger.

@unclechu @dpwiz @kornel yes, I meant MonadLogger of course, but apparently my keyboard's autocomplete only knew MonadReader.

I know what MonadLogger is; could you expand why is it that you think that it's not fine to just do the damn logging, and that having any otherwise pure function that you want to insert some logging into have its result wrapped into a monad type is better?

@bugaevc @dpwiz @kornel Because you may not want to write to stdout directly at some point? Because you may want to read those logs in tests? Becuase it involves side effects into your pure functions which makes it harder to debug, or hard to parallelise it (imagine hundreds of threads which pollute stdout), what if you want to suppress the logging in some use case? It's better to wrap it in a monad type (IO itself is also just a monad type) because you may avoid all the IO, limit side-effects.

Show more

@bugaevc @dpwiz @kornel What problem MonadLogger may solve? Plenty! For instance you can log into some stdout real IO, or collect log messages into a pure list, or send a log messages to a cross-thread variable so the log is delivered somewhere from some single thread. Your function stays the same in all of this cases, it depends on what explicit type you're applying your function on. And it extremely useful for tests, pure ones, without involving any IO.

@bugaevc @dpwiz @kornel In a test you can analyse pure log messages, collected into a list as a payload of some monadic type. Or if you have some cross-thread conflicts/racing of writing to the log you can easily avoid it by adding a centralized log output thread and change single MonadLogger implementation for your monadic type which would start to send log messages there via cross-thread MVar or whatever STM instead of just stdout-ing it. Your functions stay absolutely untouched.

@unclechu @dpwiz @kornel any serious logging framework provides all this functionality without any monads. You just configure where you want your logs to go. For example, we were talking about Rust, so take a look at the log crate: docs.rs/log

Show more
Sign in to participate in the conversation
Mastodon

Server run by the main developers of the project 🐘 It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!