Skip to content

Instantly share code, notes, and snippets.

@bvssvni
Last active December 23, 2023 22:56
Show Gist options
  • Save bvssvni/9674632 to your computer and use it in GitHub Desktop.
Save bvssvni/9674632 to your computer and use it in GitHub Desktop.
A Rust Chain Macro
//! 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));
}
@bvssvni
Copy link
Author

bvssvni commented Mar 21, 2014

The goal is to have a monad that works similar to a match statement.

Supports:

  1. Or expressions
  2. Guarded statements

Feedback/bug fixes are welcome!

@bvssvni
Copy link
Author

bvssvni commented Mar 21, 2014

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.

@bvssvni
Copy link
Author

bvssvni commented Mar 21, 2014

The 'monad' name is a bit confusing, so it was suggested "chain!" is a better name. Renamed!

@bvssvni
Copy link
Author

bvssvni commented Mar 22, 2014

Added implicit else written by @jfager

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment