[HN Gopher] Rust's Golden Rule
       ___________________________________________________________________
        
       Rust's Golden Rule
        
       Author : steveklabnik
       Score  : 140 points
       Date   : 2023-03-27 16:52 UTC (6 hours ago)
        
 (HTM) web link (steveklabnik.com)
 (TXT) w3m dump (steveklabnik.com)
        
       | nu11ptr wrote:
       | Wouldn't this be true of all statically typed languages that
       | don't allow type inference in function signatures?
        
         | steveklabnik wrote:
         | I do not claim this property is unique to Rust, it's true.
         | 
         | I think it's important to Rust conceptually because the type
         | system is one of the ways in which Rust achieves its most
         | important goals, but that doesn't mean that it's exclusive to
         | Rust or unimportant elsewhere.
         | 
         | I do find that lifetimes cause issues here that don't happen
         | in, say, GC'd yet statically typed languages. Rust puts things
         | in the signature that aren't there in many other places. I
         | think the accessor/mutator example is so common when talking
         | about this problem precisely because it's a pattern that's
         | pretty normal in other statically typed languages, but doesn't
         | cause any issues there, thanks to GC and/or lack of explicit
         | lifetime tracking.
        
           | jerf wrote:
           | "Rust puts things in the signature that aren't there in many
           | other places."
           | 
           | I expect this sort of thing scales up as you are putting more
           | in the type signature. Simple manifestly-typed languages like
           | C or Go can't say much in their type signature compared to
           | Rust, so it is harder to notice as a principle, though I
           | think it's still present if you examine it hard enough. (A
           | good example of how learning another language can be the best
           | way to advance in your "core" language.)
           | 
           | Haskell is an interesting supporting case for your core
           | thesis; it can put a lot more in its type signature than most
           | languages, but it also has inference, even pretty good
           | inference considering how strong and complex the type system
           | is. However, community practice is (and has been for a long
           | time) to put type annotations on all your top-level
           | functions, to a large degree for the same reason you are
           | talking about. Haskell can get away without annotations to a
           | large degree, but you'll be chugging along changing this &
           | tweaking that, and suddenly your entire program explodes into
           | a mess of type error messages attributed to all your code
           | everywhere because of some change you made. Explicit
           | annotations contain the blast radius of such changes and keep
           | the compiler messages pointing at something much closer to
           | the vicinity of what you actually changed and somehow broke.
           | At the top expression level, type inference is treated more
           | as a tool for determining the top-level types than a solution
           | on its own terms.
        
             | asib wrote:
             | I always explicitly annotate when writing Haskell, but I
             | often like to remove types on functions (one at a time) to
             | see what Haskell infers because it sometimes gives me a
             | more general type that I prefer. But then I'll manually add
             | in that annotation.
        
             | FullyFunctional wrote:
             | 100%
             | 
             | In fact much of "debugging" when I was writing Haskell was
             | to increasingly hand type-annotate stuff to catch where my
             | assumptions were flawed. Definitely, top-level exported
             | entries almost all get an explicit signature is it's part
             | of the documentation.
             | 
             | Rust's level of type inference feels very natural to me and
             | I don't miss what additional inference Haskell provided.
        
               | dllthomas wrote:
               | > Rust's level of type inference feels very natural to me
               | and I don't miss what additional inference Haskell
               | provided.
               | 
               | The biggest place I miss Haskell's inference is the repl;
               | When futzing with a new library I very much appreciate
               | being able to glue things together in ghci and ask what
               | the type is. I don't know how to get something quite that
               | general quite that easy in rust, although 'let _: () =
               | my(expression)` is often good enough.
        
             | steveklabnik wrote:
             | > I expect this sort of thing scales up as you are putting
             | more in the type signature.
             | 
             | Yep, I would agree with this. On my mental list of "stuff
             | to blog about someday" is a post along these lines. For me,
             | most Rust code fits into a sort of sweet spot, with enough
             | types to make things helpful, but not so many that it
             | causes pain. Other people have different preferences and/or
             | goals when it comes to these sorts of things.
             | 
             | Incidentally, I was porting some Haskell code to Rust the
             | other day, and ran into some of these differences. Oops,
             | there's a lot more to this signature than I originally
             | considered! This caused me to completely change the API of
             | the code, given that what's natural for Haskell isn't
             | always natural for Rust.
        
           | nu11ptr wrote:
           | I do think this is a nice thing in general and one thing I
           | find annoying in Ocaml without an interface file
        
             | c-cube wrote:
             | You can write explicit types in OCaml, it's actually quite
             | smooth. Over the years my style has evolved towards that
             | and now I almost always write at least the return type when
             | I write a function!
        
         | cmrdporcupine wrote:
         | I was confused by this, too. It feels like the target audience
         | for this post must be people coming from dynamic-typed, late-
         | bound languages? Hence the references to e.g. Ruby? Certainly
         | Rust's immediate mainstream "competition" all have the same
         | constraints.
         | 
         | However, languages in the ML language family like StandardML
         | and OCaml and Haskell do get away (sometimes) without explicit
         | type signatures in function definitions. But I believe it's
         | also considered good practice to declare them explicitly much
         | of the time anyways. Because the alternative is, as the article
         | pokes at, confusing error messages.
        
           | steveklabnik wrote:
           | > It feels like the target audience for this post must be
           | people coming from dynamic-typed, late-bound languages?
           | 
           | You're picking up on something, for sure. When I originally
           | put metaphorical pen to metaphorical paper, I intended to
           | talk about static types vs TDD. Then this background got a
           | bit long, so I decided to just make it its own thing, with
           | that topic to follow.
           | 
           | You're right on the "considered good practice" aspect, which
           | I didn't call out specifically because I generally try not to
           | frame things as "Rust good, other language bad," hence the
           | "Rust vs what Rust would be with function-level inference."
        
           | marcosdumay wrote:
           | > But I believe it's also considered good practice to declare
           | them explicitly much of the time anyways.
           | 
           | Much of the time is very different from always.
           | 
           | For example, Haskell would be incredibly restrictive if every
           | time you named a lambda you had to declare its type. People
           | don't even declare most of their functions, just the top
           | level ones.
           | 
           | Always declaring the types would lead to large functions that
           | are less general... what is common in Rust. (But that's not
           | the only reason they are common in Rust.)
        
             | dllthomas wrote:
             | To be fair, you don't need to declare the types of your
             | lambdas in Rust, either.
        
               | marcosdumay wrote:
               | Rust lambdas are mostly equivalent to the stuff you don't
               | name in Haskell.
               | 
               | (But well, you can certainly name them, as I've done
               | that; I don't remember if you need to declare their type;
               | and it is almost never useful.)
        
               | pornel wrote:
               | You _can 't_ even name their actual types, but you need
               | to use generics that fully specify their arguments,
               | return types, mutability and ownership.
        
       | j-pb wrote:
       | > Putting "here's the fields I'm borrowing in the body" into the
       | signature, in order to conform with the Golden Rule, looks very
       | odd.
       | 
       | I think this would be an amazing feature, but it would require
       | something like typescripts partial types, where essentially every
       | object/struct exists as a sum type of atomic fieldname:field-type
       | types.[1]
       | 
       | Rich hickey[2] has a great talk on how existing type systems
       | break in scenarios where you'd want them to be forwards
       | compatible, e.g. when relaxing what you accept as arguments or
       | similarly increasing the guarantees that you provide on returned
       | values. Partial types are one piece to that puzzle imho.
       | 
       | 1: https://www.typescriptlang.org/docs/handbook/utility-
       | types.h...
       | 
       | 2: https://youtu.be/YR5WdGrpoug?t=463
        
         | dgb23 wrote:
         | Rich Hickey also came to mind, but a different talk[0].
         | 
         | There he talks in part about software stability and how spec
         | (the primary way to encode data and function contracts in
         | Clojure), is about communicating what is required and provided
         | by a function or library (among other things).
         | 
         | Rust's decision to make signatures mostly explicit (sans
         | lifetime parameters here and there), is self-documenting and
         | useful, but most importantly it creates an explicit contract
         | that people can rely on.
         | 
         | Rust code adheres to semantic versioning so at least it will
         | also adhere to not breaking callers without bumping the major
         | version. Which the further discussion in the talk do not agree
         | with. But the fundamental decision to make signatures explicit
         | at least enables people to write stable software.
         | 
         | [0] https://www.youtube.com/watch?v=oyLBGkS5ICk
        
         | Georgelemental wrote:
         | See also:
         | https://smallcultfollowing.com/babysteps/blog/2021/11/05/vie...
        
       | roarcher wrote:
       | > Putting "here's the fields I'm borrowing in the body" into the
       | signature, in order to conform with the Golden Rule, looks very
       | odd.
       | 
       | Do you mean something like this (using destructuring in
       | arguments)?                   struct Point {             x: i32,
       | y: i32,          }              impl Point {             pub fn
       | x_mut(self { &mut x, .. }) -> &mut i32 {                 &mut
       | self.x             }                  pub fn y_mut(self { &mut y,
       | .. }) -> &mut i32 {                 &mut self.y             }
       | impl Point {                 pub fn calculate(&mut self) -> i32 {
       | let x = self.x_mut();                     let y = self.y_mut();
       | *x * *y                 }             }         }
       | 
       | I'd agree that it looks odd, but not _very_ odd. Certainly there
       | are odder syntaxes in Rust ;)
        
         | steveklabnik wrote:
         | That syntax is not valid Rust, though you're right it's sorta
         | similar to destructuring. I wasn't thinking of it specifically,
         | but of some of the sorta-proposed syntaxes by various folks in
         | blog posts or on internals. And yeah, it's not insurmountable
         | of an issue.
        
           | roarcher wrote:
           | Oh I know, it's just an idea of a possible syntax. Something
           | like that might look a little funny but it doesn't seem too
           | terrible to me. Anyway, thanks for the work you do on Rust!
           | It's always my first choice of language when I have one.
        
             | steveklabnik wrote:
             | You're welcome, though I don't do any of that any more.
        
         | bobbylarrybobby wrote:
         | You can already kind of do this with `pub fn x_mut(Self { x, ..
         | }: &mut Self) -> &mut i32`, but
         | 
         | 1. Your function is no longer callable like p.x_mut(); you have
         | to go through `Point::x_mut(p)` (seems kind of dumb that the
         | first argument has to literally be called `self` and can't just
         | be of type `Self`)
         | 
         | 2. The compiler doesn't actually use the fact that you're
         | ignoring the unspecified fields of `Self`, although it seems
         | like that analysis should be trivial, given that rust already
         | understands that `calculate(&mut point.x, &mut point.y)` isn't
         | overlapping?
        
       | lalaithion wrote:
       | This is what I find most annoying about rust's async ecosystem;
       | many functions depend on being called within a certain executor,
       | but this is not documented in the type at all. You should not
       | provide, and arguably not be able to provide, functions which
       | look like they can be called in any context but which require
       | shared mutable global state!
        
         | msla wrote:
         | This sounds like a job for monads, to be honest.
         | 
         | "Shared mutable global state" is "IO monad" all over.
         | 
         | (Ia! Ia! Ruskell fhtagn!)
        
         | ahepp wrote:
         | Can you give an example of what you're talking about? I'm
         | pretty new to async and Rust in general but I'm having a hard
         | time thinking of an example (almost certainly a deficiency on
         | my end haha).
        
           | paulgb wrote:
           | Here's an example:
           | https://docs.rs/tokio/latest/tokio/time/fn.sleep.html#panics
           | 
           | > It can also panic whenever a timer is created outside of a
           | Tokio runtime.
           | 
           | Note that nothing in the signature tells you that, just the
           | function documentation.
           | 
           | In practice, this ends up not being a big deal, IMHO. The
           | ecosystem has largely coalesced around tokio as an executor.
           | The only times I've run into it are when running Actix, which
           | has its own global state on top of tokio.
        
             | lalaithion wrote:
             | It's not a big deal because the ecosystem has coalesced
             | around tokio, but it's coalesced around tokio partially
             | because it's a big deal.
        
             | ahepp wrote:
             | Ah, that makes sense. In fact, I was trying to unit test
             | some time based code in tokio a month or two back, and it
             | was really painful. I couldn't figure out how to inject a
             | fake clock into the runtime to test my timers.
        
         | yazaddaruvala wrote:
         | +1
         | 
         | I don't have the time to shepherd this RFC (or something
         | similar), but I really wish that the whole Async ecosystem was
         | built on something less error prone / magic-y than
         | ThreadLocalState.
         | 
         | Its not ideal to ever have any "hidden" functionality like
         | auto-propagation, but it would be way better than the current
         | state of async Rust.
         | 
         | https://github.com/rust-lang/rfcs/issues/2327
        
           | oconnor663 wrote:
           | I could be wrong, but I don't think Rust futures/tasks rely
           | on thread-local storage anymore. Some early iterations of the
           | feature did, but I think that was replaced with an explicit
           | argument to the poll() function before stabilization. That
           | said, the typical async/await syntax makes that invisible, so
           | it definitely does feel a lot like TLS.
        
           | gpderetta wrote:
           | If I understand correctly, Autopropagators are basically
           | implicit parameters , a.k.a typed dynamic scoping, which is
           | exactly what you need to solve this problem.
           | 
           | In fact the equivalent Scala functionality is mentioned in
           | the issue thread. They might be confusing but they are
           | probably a million time better than actual mutable globals.
        
       | hardwaregeek wrote:
       | Having explicit function types prevents Rust from feeling like an
       | algebra problem. Languages like Haskell and OCaml make writing
       | generic functions super simple and elegant, but it comes at the
       | cost of having abstraction be the default. Therefore you end up
       | reading signatures where the mental process is "so I have a
       | parameter x of type a, then a parameter f of type a -> m b, then
       | a return type of m b, hmm now in this case m = List, a = Int, b =
       | Float". You end up having to do a lot of substitution and manual
       | unification in your head. Which, to be fair, some people enjoy
       | and find useful. More power to them!
       | 
       | On the flip side, forcing type signatures means that I tend to
       | shy away from more sophisticated types like fancy closures with
       | lifetimes. Which can be frustrating because the usage is simple,
       | but the type is sometimes not. But I think it's fine for function
       | signatures to be a little harder to write for library authors and
       | a little easier to understand for library users.
        
         | marcosdumay wrote:
         | > You end up having to do a lot of substitution and manual
         | unification in your head.
         | 
         | Hum... I don't think I do that. I did a lot when I was first
         | learning it, but not now.
         | 
         | Now there's some pattern matching (`f :: a -> m b` immediately
         | says something) and a lot of generalization (so I don't think
         | "I have a list, I want to apply a function"; I think "I will
         | apply a function, I want something I can map"). The
         | `Applicative m => m a` comes before the `[a]`.
        
         | brundolf wrote:
         | IDEs close the gap a lot on type inference. Being able to hover
         | a symbol and see the inferred type without mentally inferring
         | it yourself is important
        
       | ChadNauseam wrote:
       | Tangentially related. (I thought of this when thinking about what
       | the OP said about rust forcing you to hardcode your function's
       | types prevents spooky action at a distance.)
       | 
       | In Unison [0], when you change a function's type, it computes the
       | number of call sites of that function (and callers of the
       | callers, etc), and gives you a "todo-list" of functions you'll
       | need to fix. As you go through the todo-list, the list of
       | functions to fix only ever gets smaller. It can do this because
       | the compiler knows the state of the codebase before and after
       | your change. That means it can see the difference between "you
       | changed a function's type, which broke the callers" and "you
       | changed how you're calling a function, and your change didn't
       | typecheck".
       | 
       | [0]: https://www.unison-lang.org/
        
       | pornel wrote:
       | There is one case where Rust breaks this rule for the sake of
       | simpler syntax. Send of Futures:                    async fn
       | send_peekaboo() {             let _r = std::rc::Rc::new(());
       | // async {}.await;         }              fn is_it_send(_: impl
       | Send) {}             is_it_send(send_peekaboo());
       | 
       | If you uncomment the .await line, this crates a possibility of
       | moving thread-unsafe Rc across threads, and the Future stops
       | being thread-safe. Rust catches this problem correctly, but it is
       | a case where function body leaks into the signature.
        
         | steveklabnik wrote:
         | Someone pointed this out to me on Twitter, it's a great point!
        
       | brundolf wrote:
       | I'm gonna politely disagree and say, I don't think "spooky action
       | at a distance" is very scary when it happens at build-time
       | 
       | Much of my background is in TypeScript, which encourages mixing
       | and matching explicit and implicit return-types, and I don't
       | think it's just bias when I say I broadly prefer that. Having the
       | flexibility to decide which part of your code is driving your
       | types in different circumstances is really helpful. And Rust
       | already does this in lots of situations, like in variable
       | declarations and generics; Rust closures even let you have
       | inferred argument and return types, so it feels a little
       | arbitrary that regular functions are one of the _only_ places
       | where explicit types are absolutely required
       | 
       | Caveats:
       | 
       | - Crate exports should definitely require explicit types, since
       | you don't have all the callers available when you build your own
       | code to see that you don't break the API without meaning to
       | 
       | - Rust of course tends to be used for slower-moving and more
       | careful projects than TypeScript, so in general I'm less bothered
       | by having to be explicit in a Rust project than I would be in a
       | TypeScript project
       | 
       | - TypeScript types have no runtime footprint. They in no way
       | affect the underlying data or how it's represented, they only
       | describe data that has its own separate material reality. Rust
       | being a low-level language, that isn't the case here! When the
       | return type of a function changes, that isn't just for checking,
       | it changes the actual runtime footprint of the function in a
       | small way. So I could see this being a reason to keep types
       | explicit. Though, again, Rust already does lots of type inference
       | in other places with potentially much bigger runtime
       | implications, like when .collect() infers the data structure the
       | items are going to be allocated into.
       | 
       | In all: it doesn't matter to me that much, but if it were totally
       | up to me, I'd strongly consider allowing inferred function
       | return-types
        
         | steveklabnik wrote:
         | It's all good! Polite disagreement is how things move forward.
         | I think if you truly twisted my arm, I wouldn't _hate_ the idea
         | of non-pub things being inferred, but no further than that. I
         | 'd still choose Rust's current design, personally, but that is
         | in fact a preference.
        
           | dllthomas wrote:
           | I think there's something to be said for allowing things to
           | be inferred during rapid development that need to be pinned
           | down before publishing, but once you've removed the technical
           | barrier there's a big question of how you make sure the
           | social side of it stays healthy.
        
           | vlovich123 wrote:
           | I think it might be nice to allow for inference at the
           | pub(crate) level and below (or maybe only within a module if
           | you wanted to be extra cautious). Now that I've had a lot of
           | experience with TS, I'd say the probability that you
           | accidentally change the type and the code doesn't fail is
           | minimal, so the productivity gains of being kind to the user
           | outweigh the value of extra verbosity. And ultimately that's
           | all type checking is for - to make sure your types form an
           | internally consistent system that you can reason about and
           | avoid making mistakes on.
        
           | shadowgovt wrote:
           | Agreed, because when I think of signature-inferred-by-body, I
           | don't think of TypeScript; I think of C++ and the nature of
           | the error messages you get when you try to use a novel type
           | in a container template.
        
             | estebank wrote:
             | It's also not an all or nothing rule: `type Alias = impl
             | Trait;` will have in effect the same issues that return and
             | argument type inference could have, and I am convinced that
             | the issues around diagnostics caused by type errors
             | involving it can be made almost as clear as E0308 is
             | already today. I also believe that Rust _can_ afford to
             | have type inference in functions, but only for crate-local
             | functions, I wouldn 't ever want to see a `pub fn foo(x: _)
             | -> _ {}` be allowed. Your type signatures are a contract.
             | You can have a "gentleman's agreement" within your own
             | project, but you want a signed and notarized contract with
             | everyone else :)
             | 
             | An interesting thing is that for return types all you need
             | is evaluating the body (which is why rustc already suggests
             | the right type when writing `-> _`, it was super-easy to
             | implement), while parameter type inference requires global
             | inference (which requires a full-blown change to the type
             | checker's implementation, not as worth it for a "mere
             | suggestion").
        
               | steveklabnik wrote:
               | > I am convinced that the issues around diagnostics
               | caused by type errors involving it can be made almost as
               | clear as E0308 is already today
               | 
               | That's very cool! I'm glad to hear it.
               | 
               | > An interesting thing is that
               | 
               | Ahh, this is an interesting point for sure; I chose
               | return values basically at random, but I can see how
               | that's easier than parameters are. Guess that would have
               | been an ever so slightly more strong example on my part!
        
       | eterps wrote:
       | I believe the first issue could be somewhat mitigated if the
       | editor displayed the inferred types as virtual text.
        
         | bombela wrote:
         | You get this for all well known text/code/ide editors via rust-
         | analyzer.
        
         | ChadNauseam wrote:
         | Rust-analyzer in VSCode does this for local variables, and it's
         | very nice
        
           | Ygg2 wrote:
           | Same as IntelliJ Rust in CLion.
        
           | timeon wrote:
           | Isn't it also what rust-analyzer does in other editors?
        
         | cmrdporcupine wrote:
         | Jetbrain's Rust mode for CLion, etc. will do this all over the
         | place -- display inferred/matching types for variables, pattern
         | matches, function arguments, etc.
         | 
         | But it would be unfortunate to have to rely on a tool in this
         | way for basic language functionality.
        
       ___________________________________________________________________
       (page generated 2023-03-27 23:02 UTC)