`let .. in` to declare variables with limited scope


#1

Hi all,

With controlling scope so important in Rust, esp for things like MutexGuards, I’m finding the brace syntax somewhat cumbersome.

Other languages like Ocaml have let ... in ... syntax for declaring variables with limited scope, and it seems to me that it would also be useful in Rust. Specifically, something like:

let locked = thing.lock().unwrap() in { locked.use() + locked.otheruse() }

which seems a lot more readable than:

{
    let locked = thing.lock().unwrap();
    locked.use() + locked.otheruse()
}

This form of let could also be an expression which has the value of the in clause, allowing:

let val = let locked = thing.lock().unwrap() in locked.use() + locked.otheruse();

though this might cause ambiguities with if let or while let, depending on how much look ahead the parser has. Or perhaps just require let .. in to be parenthesized to break the ambiguity.

Because let pattern = expr expr is currently a syntax error, the in keyword isn’t strictly needed, but it seems like it would be too easy to accidentally use this form without it:

let x = foo() // forgotten ;
something(x); // OK, x in scope
some_other_thing(x); // error, x not in scope (!?)

Thoughts?

This seems like a pretty obvious extension so I’m sure it has been considered before; if so, I wonder if it was outright rejected, or just put on the backburner.


#2

:-1:: this is “garden path” syntax: it looks like you’re defining a binding… until you look closely at the initialiser when surprise! it’s actually only defined for this one statement!

I don’t see the benefit when all this really does is move the brace right a bit, but makes it much less clear what’s going on.

Also, that example with let val = let locked is a fantastic reason not to allow it. That just hurts to look at.

Not everything needs to be hammerable into an expression. It’s ok; you don’t have to be afraid of semicolons; they won’t hurt you! :slight_smile:


#3

Aren’t you sort of comparing apples and oranges? If you remove the whitespace from your second example it becomes

{let locked = thing.lock().unwrap(); locked.use() + locked.otheruse()}

which is very similar to your

let locked = thing.lock().unwrap() in { locked.use() + locked.otheruse() }

I don’t think there’s much readability difference.


#4

I don’t see a readability difference, if I had to decide, I’d use the standard form. I’m no stranger to languages with syntax similar to this.

First of all, it introduces ambiguity on the left hand side: until I know the scope of locked, I need to read the line to the end.

I also think it hides the scope block. It’s usually very apparent in Rust how long scopes run by just following braces. and some very recognizable statement forms (match =>). This introduces a rather hidden form that is not as easy to parse visually.

I think it’s a special thing without many gains. If scope syntax is the issue, working with closures could be a way of doing things here.


#5

I also think the scoping ambiguity is a problem. Maybe you could add this sugar using something other than let, like a pythonic with, or the existing keyword where.

{ let locked = thing.lock().unwrap(); locked.use() + locked.otheruse() }
where locked = thing.lock().unwrap() { locked.use() + locked.otheruse() }

So that’s actually a little longer, depending on the keyword, but it may be subjectively nicer when formatted.

{
    let locked = thing.lock().unwrap();
    locked.use() + locked.otheruse()
}
where locked = thing.lock().unwrap() {
    locked.use() + locked.otheruse() 
}

The unwrap makes me feel you ought to be using match or if let anyway.


#6

This can currently be done with a match:

match thing.lock.unwrap() { locked => {
   locked.use() + locked.otheruse()
}}

#7

If with had been available, I might have suggested it: with <pattern-bindings> in <expr>.

But Rust’s already numerous ways of expressing things like this, perhaps another form is redundant. @arielb1’s suggestion of a single-armed match is interesting - I don’t know how I feel about that yet.


#8

That’s a detail specific to Mutex::lock - it returns an error if there’s a poisoned lock, but usually the correct response is to panic on error anyway (since that will kill this thread and poison any locks it holds, propagating the error onwards).

In general I reserve unwrap() for test cases.


#9

Maybe you should try that single-armed match in a with! macro. It’s probably pretty close to how this would desugar anyway.


#10

If you’re going to do this with a macro, match just makes things harder. The real problem, from my perspective, is that there’s really no point whatsoever:

macro_rules! with {
    ($($name:ident = $init:expr),+ => $body:expr) => {
        {
            $(let $name = $init;)+
            $body
        }
    }
}

fn main() {
    println!("{}", with!(four = 4, two = four/2 => two*four));
    println!("{}", {let four = 4; let two = four/2; two*four});
}

This is really just fundamentally about having a slightly different way of saying something you can already say, in a way that not really significantly shorter or more readable.

I just don’t see any objective benefits over what we already have in the language.

(Incidentally, if statement macros weren’t borked, you could make a case for a lets! macro that just saves you from having to repeat the let keyword multiple times, but that’s still not a huge improvement.)