Expand description
Uniplate helps you write simple, boilerplate-free operations on tree shaped data types.
- A port of Haskell’s Uniplate library in Rust.
§Getting Started
Adapted from (Mitchell and Runciman 2007)
Consider the abstract syntax tree for a simple calculator language:
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
}
Say we want to list all the used variable names inside a given expression:
fn var_names(expr: &Expr) -> Vec<String>{
match expr {
Add(a,b) => {
[var_names(a),var_names(b)].concat()
},
Sub(a,b) => {
[var_names(a),var_names(b)].concat()
},
Mul(a,b) => {
[var_names(a),var_names(b)].concat()
},
Div(a,b) => {
[var_names(a),var_names(b)].concat()
},
Val(a) => {
Vec::new()
},
Var(a) => {
vec![a.clone()]
},
Neg(a) =>{
var_names(a)
}
}
}
Functions like these are annoying to write: the first 4 constructors are basically identical, adding a new expression type requires a new line to be added to all match statement, and this code cannot be shared with similar functions (e.g. one that change all the variable names).
With Uniplate, this boilerplate can be eliminated:
use std::collections::VecDeque;
use uniplate::Biplate;
fn var_names(expr: &Expr) -> Vec<String>{
let names: VecDeque<String> = expr.universe_bi();
names.into()
}
The functionality of Uniplate comes from two main traits: Uniplate
and
Biplate<T>
.
- The
Uniplate
ofExpr
operates over all nestedExpr
s. - The
Biplate<T>
ofExpr
operates over all nested values of typeT
in the expression tree.
These traits provide traversal operations (e.g. children
) as well as
functional programming constructs such as map
(transform
, descend
) and fold
(cata
.
See the trait documentation for the full list of operations provided.
The easiest way to use Uniplate is with the derive macro.
§Derive Macro
To derive Uniplate instances, use the #[uniplate]
attribute:
use uniplate::derive::Uniplate;
#[derive(Clone,PartialEq,Eq,Debug,Uniplate)]
#[uniplate()]
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
}
To derive Biplate instances, use the #[biplate]
attribute:
use uniplate::derive::Uniplate;
#[derive(Clone,PartialEq,Eq,Debug,Uniplate)]
#[biplate(to=String)]
#[biplate(to=i32)]
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
}
§Multi-type traversals
Uniplate also supports trees with multiple recursive types. Lets extend our calculator language to include statements as well as expressions:
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
}
enum Stmt {
Assign(String, Expr),
Sequence(Vec<Stmt>),
If(Expr, Box<Stmt>, Box<Stmt>),
While(Expr, Box<Stmt>),
}
When looking for variable names in a given statement, we want to identify not only the variable names directly used inside the statement, but also any variable names used by child expressions:
use uniplate::derive::Uniplate;
use uniplate::{Uniplate,Biplate};
use std::collections::VecDeque;
#[derive(Clone,PartialEq,Eq,Debug,Uniplate)]
// look for strings inside expressions as well as statements
#[biplate(to=String,walk_into=[Expr])]
#[biplate(to=Expr)]
#[uniplate()]
enum Stmt {
Assign(String, Expr),
Sequence(Vec<Stmt>),
If(Expr, Box<Stmt>, Box<Stmt>),
While(Expr, Box<Stmt>),
}
#[derive(Clone,PartialEq,Eq,Debug,Uniplate)]
#[biplate(to=String)]
#[uniplate()]
enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
}
fn vars_names(stmt: &Stmt) -> Vec<String>{
let names: VecDeque<String> = stmt.universe_bi();
names.into()
}
The types given to the walk_into
argument are recursed through by uniplate.
Both #[uniplate]
and #[biplate]
support the walk_into
parameter.
§Bibliography
The techniques implemented in this crate originate from the following:
-
Neil Mitchell and Colin Runciman. 2007. Uniform boilerplate and list processing. In Proceedings of the ACM SIGPLAN workshop on Haskell workshop (Haskell ’07). Association for Computing Machinery, New York, NY, USA, 49–60. https://doi.org/10.1145/1291201.1291208 (free copy)
-
Huet G. The Zipper. Journal of Functional Programming. 1997;7(5):549–54. https://doi.org/10.1017/S0956796897002864 (free copy)
Modules§
Macros§
- derive_
iter - Generates
Biplate
andUniplate
instances for a collection using itsIterator
implementation. - derive_
unplateable - Generates
Biplate
andUniplate
instances for an unplateable type.