Last active
December 23, 2023 22:56
-
-
Save bvssvni/9674632 to your computer and use it in GitHub Desktop.
A Rust Chain Macro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! Chain macro in Rust | |
//! | |
//! A typical usage is to avoid nested match expressions. | |
//! | |
//! Supports: | |
//! - Pattern matching | |
//! - Or expressions (A | B => ...) | |
//! - Guarded statements (x if <cond> => ...) | |
//! - Implicit else (requires all arms to return same type) | |
#[feature(macro_rules)]; | |
// The chain macro is syntactically similar to a 'match' statement. | |
// Each arm pattern matches against the result of the previous arm. | |
// If any pattern match fails, the expression after the 'else' keyword is executed. | |
// In the case the 'else' part is omitted, all arms must return same type (usually Result). | |
// | |
// #Syntax (v4) | |
// ``` | |
// let <name>: <type> = chain!( | |
// do <start> { | |
// <pattern0> => <step1>, | |
// <pattern1> => <step2>, | |
// <pattern2> => <step3>, | |
// ... => <result> | |
// } else <err> | |
// ); | |
// ``` | |
// | |
// No colon is allowed after the last arm. | |
// The macro is calling itself recursively. | |
macro_rules! chain( | |
// Base case (2 arms). | |
( | |
do $start: expr { | |
$($a: pat)|+ $(if $c: expr)* => $b: expr | |
} else $err: expr | |
) => { | |
match $start { | |
$($a)|+ $(if $c)* => $b, | |
_ => $err | |
} | |
}; | |
// Case for more than 2 arms. | |
( | |
do $start: expr { | |
$($a_head: pat)|+ $(if $c_head: expr)* => | |
$b_head: expr $(, $($a: pat)|+ $(if $c: expr)* => $b: expr)+ | |
} else $err: expr | |
) => { | |
match $start { | |
$($a_head)|+ $(if $c_head)* => chain!( | |
do $b_head { | |
$($($a)|+ $(if $c)* => $b),+ | |
} else $err | |
), | |
_ => $err | |
} | |
}; | |
// Base case implicit else | |
(do $start: expr { | |
$($a: pat)|+ $(if $c: expr)* => $b: expr | |
}) => { | |
match $start { | |
$($a)|+ $(if $c)* => $b, | |
x => x | |
} | |
}; | |
// Multi-case implicit else | |
(do $start: expr { | |
$($a_head: pat)|+ $(if $c_head: expr)* => | |
$b_head: expr $(, $($a: pat)|+ $(if $c: expr)* => $b: expr)+ | |
}) => { | |
match $start { | |
$($a_head)|+ $(if $c_head)* => chain!( | |
do $b_head { | |
$($($a)|+ $(if $c)* => $b),+ | |
} | |
), | |
x => x | |
} | |
} | |
) | |
#[allow(dead_code)] | |
fn main() { | |
use std::path::Path; | |
use std::io::fs::File; | |
use std::io::BufferedReader; | |
use std::vec_ng::Vec; | |
// Creates a list of numbers from 1 to n where n is the first line in 'n.txt'. | |
let numbers: Vec<uint> = chain!( | |
do File::open(&Path::new("n.txt")) { | |
Ok(file) => BufferedReader::new(file).lines().map(|line| line.unwrap()).next(), | |
Some(first_line) => from_str::<uint>(first_line.trim()), | |
Some(n) => range(0, n).map(|x| x + 1).collect() | |
} else Vec::new() | |
); | |
for i in numbers.iter() { | |
println!("{}", i); | |
} | |
} | |
#[test] | |
pub fn test_1() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(x) => Some(x + 1) | |
} else None | |
); | |
assert_eq!(x, Some(6)); | |
} | |
#[test] | |
pub fn test_2_pre() { | |
let x: Option<int> = match Some(5) { | |
Some(4) | Some(5) => Some(5), | |
_ => None | |
}; | |
assert_eq!(x, Some(5)); | |
} | |
#[test] | |
pub fn test_2() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(4) | Some(5) => Some(4) | |
} else None | |
); | |
assert_eq!(x, Some(4)); | |
} | |
#[test] | |
pub fn test_3() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(4) | Some(5) => Some(4), | |
Some(4) | Some(5) => Some(6) | |
} else None | |
); | |
assert_eq!(x, Some(6)); | |
} | |
#[test] | |
pub fn test_4() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(x) if x == 5 => Some(x+1) | |
} else None | |
); | |
assert_eq!(x, Some(6)); | |
} | |
#[test] | |
pub fn test_5() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(x) if x == 5 => Some(x+1), | |
Some(x) if x == 6 => Some(x+1) | |
} else None | |
); | |
assert_eq!(x, Some(7)); | |
} | |
#[test] | |
pub fn test_6() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(x) if x == 5 => Some(x+1), | |
Some(6) | Some(7) => Some(7) | |
} else None | |
); | |
assert_eq!(x, Some(7)); | |
} | |
#[test] | |
pub fn test_7_pre() { | |
let x: Option<int> = match Some(5) { | |
Some(0..10) => Some(6), | |
_ => None, | |
}; | |
assert_eq!(x, Some(6)) | |
} | |
#[test] | |
pub fn test_7() { | |
let x: Option<int> = chain!( | |
do Some(5) { | |
Some(0..10) => Some(6) | |
} else None | |
); | |
assert_eq!(x, Some(6)); | |
} | |
#[test] | |
pub fn test_8() { | |
let x: Option<int> = chain!( | |
do 5 { | |
4 | 5 => Some(5) | |
} else None | |
); | |
assert_eq!(x, Some(5)); | |
} | |
#[test] | |
pub fn test_9() { | |
let x: Option<int> = chain!( | |
do 5 { | |
x @ 0..10 => Some(x) | |
} else None | |
); | |
assert_eq!(x, Some(5)); | |
} | |
#[test] | |
pub fn test_10() { | |
let x = Some(1); | |
let y = Some(2); | |
let z = chain!( | |
do x { | |
Some(x) => y, | |
Some(y) => Some(x+y) | |
} else None | |
); | |
assert_eq!(z, Some(3)); | |
} | |
#[test] | |
pub fn test_implicit_else() { | |
let res = chain!( | |
do Ok(1) { | |
Ok(x) => Ok(2), | |
Ok(y) => Err('z'), | |
Ok(z) => Ok(x+y+z) | |
} | |
); | |
assert_eq!(res, Err('z')); | |
} | |
#[test] | |
pub fn test_block() { | |
let x = Some(1); | |
let y = Some(2); | |
let z = chain!( | |
do x { | |
Some(x) => { | |
println!("in a block!"); | |
y | |
}, | |
Some(y) => Some(x+y) | |
} | |
); | |
assert_eq!(z, Some(3)); | |
} | |
#[test] | |
pub fn test_inner_chain() { | |
let x = Some(1); | |
let y = Some(2); | |
let z = chain!( | |
do x { | |
Some(x) => chain!( | |
do x { | |
1 => { | |
println!("in a block!"); | |
y | |
} | |
} else None | |
), | |
Some(y) => Some(x+y) | |
} else None | |
); | |
assert_eq!(z, Some(3)); | |
} |
The first syntax proposal:
let <name>: <type> = monad!(
<err> {
_ => <start>
<pattern0> => <step1>,
<pattern1> => <step2>,
<pattern2> => <step3>,
... => <result>
}
);
The second syntax proposal:
let <name>: <type> = monad!(
do <start> {
<pattern0> => <step1>,
<pattern1> => <step2>,
<pattern2> => <step3>,
... => <result>
} else <err>
);
Some people might not like 'do' because it was used for 'spawn!' earlier in Rust, but it puts it a bit closer to the 'do' monad in Haskell. Alternative is using another keyword or no keyword at all. I put the at the end because people got confused by having it in front of the bracket.
The 'monad' name is a bit confusing, so it was suggested "chain!" is a better name. Renamed!
Added implicit else written by @jfager
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The goal is to have a monad that works similar to a match statement.
Supports:
Feedback/bug fixes are welcome!