1. 31
    1. 35

      My prefered way to spell the following

      let mut xs: Vec<u32> = Vec::new();
      xs.push(1);
      xs.push(2);
      let xs = xs; // no longer can be changed!
      

      would be

      let longer_name = {
        let mut xs = Vec::new();
        xs.push(1);
        xs.push(2);
        xs
      };
      

      as it makes initialization bit more visible, and also hides away any extraneous temporaries should you need them.

      As for the general thoughts on shadowing, I don’t know… I had bugs both ways:

      • if I don’t shadow, I make a bug because I have too many similarly named variables in the scope and use the wrong one.
      • if I do shadow, I confuse the semantics of the few variables I have

      I guess the only reasonable choice is to ban locals (and parameters, while we are at it), as they are too error prone!

      1. 7

        I also often use blocks for initialization of a local variable (especially HashMaps).

        But, I don’t use the block approach in order to have an “immutable” binding at the end. I only use it for the reasons you cite: it clearly separates the initialization of a local from the rest of the logic of the function, and it keeps temporaries from polluting the outer-local scope.

        I would never write the first version (let mut xs = Vec::new(); [...] let xs = xs;) because I find that to be extra line noise for very little gain. Sure, it might give a slight hint to future readers that xs isn’t mutated anymore past that line, but it’s truly only a slight hint because you can easily write let mut xs = xs; later in the function and go back to mutating it. The important thing in Rust is not that you have little-to-no mutation, it’s that you have concurrency-safe mutation, which you have whether xs is a mut binding or not.

        As for shadowing, I love it, but I only use it for one scenario that I can think of: when you immediately transform an input argument and don’t want to ever refer to the “raw” value again. This happens especially frequently in Rust with things like Into<>. If you have a function like fn do_stuff<S: Into<String>>(text: S), it’s really nice to have the first line of the function be let text = text.into(). Similar for things like trimming whitespace from input params, etc.

    2. 5

      I really like TypeScript’s alternative to this: testing a variable automatically narrows its type, so inside if (typeof(X) === ‘string’) {…} the type of x is automatically string. This is a real godsend especially with null tests!

      1. 2

        I think you can manage the same thing with Rust’s if-let-else syntax, though I’m not sure whether it’s more clunky than TypeScript because you need a return or if it’s less clunky than TypeScript’s (JavaScript’s) stringly typed objects.

        1. 2

          What you’re liking to is let-else, not if-let-else. The former must have a divergent branch, the latter is a normal-ish conditional so depends on the types involved.

          For “null tests” you might also be able to just use ? depending on the context and semantics you need (hopefully one joyous day try {} blocks will land).

      2. 1

        It’s not really an alternative though? Rather an ad-hoc replacement for a very limited subset of the features e.g. you can’t handle the conversion case with this, if X is a string but you want an integer the it does nothing helpful.

        1. 1

          It’s different. I wouldn’t say ad hoc; it works with many different kinds of type checks, e.g. if you test if(x) then the compiler knows x is not null or undefined, which affects its type. You can also define your own functions that do type refinement this way.

    3. 4

      Redeclaring local variables is something I always use in any language that lets me, including Rust, mainly for the same purposes as described in the article, namely wrapping / unwrapping / transforming data while still keeping consistent naming.

      There are however problems with this approach, namely:

      • in lexically scoped languages, one can forget that the new variable doesn’t outlive the current scope, i.e. {let x = 5; if true { let x = 10 }; assert_eq! (x, 10); (instead of just using assignment); (this happens especially during refactoring when you move code inside if but forget to remove the let);
      • in languages that don’t actually have “declaration” constructs (i.e. let), like for example Python, one can’t rely on the fact that a redeclared variable is only visible inside the “logical” scope (i.e. the for body), and the value escapes;
      • sometimes it can be confusing what type a variable is, or what properties it currently has;

      One more item, sometimes when I have a long strench of code and I want to be sure that I can’t use a particular variable, I just let x = (); using drop (x) doesn’t work if the type of x is Copy

      On the other hand, one language where this doesn’t work at al is Erlang, thus (keeping in-line with my overall coding style), I end-up using variables like Something_1, Something_2, etc.

    4. 2

      I remember reading somewhere a while ago an observation about lexical scoping. In general, code can be easier to understand if you structure it in a way that makes certain things impossible. E.g. the article uses let xs = xs; to make further mutation impossible. When deciding where to define a variable or function:

      • With a narrow scope, you immediately know it can’t be used outside that scope.
      • With a broad scope, you immediately know it can’t rely on anything in narrower scopes.

      In other words, if you imagine a directed graph of definitions, you can reduce the mental burden by ruling out incoming edges at the expense of outgoing edges, or vice versa.

      Does anyone know what I’m talking about/where I might have seen this?