rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.97k stars 1.57k forks source link

Event indicators: Extended parametric labeled scope exit points #1380

Open SlugFiller opened 9 years ago

SlugFiller commented 9 years ago

In his article "Structured Programming with go to Statements" Donald Knuth describes a syntactic pattern called "event indicators", which allow breaking out of a loop, while maintaining a value. They are similar in principles to exceptions, in roughly the same way that coroutines (Also suggested by Knuth) are similar to threads. From a modern perspective, there is also a parallel: A syntactic construct that creates a limited variant, while eliminating the need for run-time support.

The structure I suggest for this is as follows:

match {
  for (index, value) in haystack.iter().enumerate() {
    if (value == needle) {
      break 'found(index);
    }
  }
  println!("Not here");
} {
  'found(index:isize) => println!("Found at {}", index),
}

The syntax is clearly distinguishable from a regular match due to the use of a label as a pattern.

While the above can also be implemented using a closure with enumeration return type, things become less trivial when dealing with cascaded indicators:

match {
  //...
  match {
    if (something) {
      break 'something;
    }
    //...
    break 'somethingelse
  } {
    'something => {},//...
  }
  //...
} {
  'somethingelse => {},//...
}

The above cannot be cleanly wrapped into closures, since you can't "return twice" in a single statement.

I also suggest that duplicate labels are allowed in the cascaded case, with the innermost matching label applying. In this case "matching" can also include overloaded parameters:

match {
  for i in 1..10 {
    match {
      match {
        match i {
          1 => break 'common(i),
          2 => break 'common(" "),
          3 => break 'common,
          _ => {},
        }
        println!("!");
      } {
        'common(x:isize) => {println!("Hello"); continue},
        'common => break 'common("world"),
      }
      break 'common;
    } {
      'common(s:string) => println!("{}", s),
    }
  }
} {
  'common => 0,
}

The above should print Hello world!.

Note that unlike a regular match, a catch-all pattern (_) is not necessary, since every label must be defined by the match patterns block.

It could also be used to simulate exception handling:

macro_rules! tryb {
  ($expr:expr) => (match $expr {
    $crate::result::Result::Ok(val) => val,
    $crate::result::Result::Err(err) => {
      break 'catch($crate::convert::From::from(err))
    }
  })
}

//...

match {
  tryb!(foo());
  tryb!(bar());
} {
  'catch(e:io::Error) => println!("IO error happened!"),
  'catch(e:MyError) => println!("MyError happened!"),
  'catch(e:Error) => println!("Unknown Error happened!"),
}

In the above, the most appropriate 'catch block is chosen based on the support of the From trait in each case. Only compile-time information is used, however, to determine the type of error returned from each method.

It differs from regular exceptions in that it is method-local, so there's no unwinding of the call stack.

glaebhoerl commented 9 years ago

I wonder if either of these are similar to what you're thinking of:

https://github.com/rust-lang/rfcs/issues/961 https://github.com/rust-lang/rfcs/pull/243

oli-obk commented 9 years ago

Since you can already do this in regular Rust, all that you need is some macro or a compiler-plugin to make it look nicer.

    let haystack = vec![5, 6, 7];
    let needle = 5;
    if let Some(index) = (|| {
      for (index, &value) in haystack.iter().enumerate() {
        if value == needle {
          return Some(index);
        }
      }
      None
    })() {
        println!("Found at {}", index);
    } else {
        println!("Not here");
    }
SlugFiller commented 9 years ago

@glaebhoerl It's different from #961 because that one is limited to loops. #243 is actually a whole bunch of things, since it links to a rather long RFC, but the "Early exit from any block", with labelled returns, is similar. It's less expressive, though, since you need to clarify the block from which you want to exit, so overloading is not possible. For exception simulation, the implication is that you have to specify the catch-like-block that's supposed to capture the specific exception in the try macro.

@oli-obk As I've mentioned in the post, doing this using closures is only possible if you have only one layer. If you have a closure inside a closure, you can't force a break of the outer one from within the inner one. It also doesn't cover overloading. And you need to explicitly define an enumeration for each set of options with which you want to break.