1 unstable release
Uses old Rust 2015
0.1.0 | Aug 3, 2017 |
---|
#25 in #pub
Used in scrapmetal
17KB
421 lines
scrapmetal
: Scrap Your Rust Boilerplate
Generic transformations, queries, and mutations for Rust without the boilerplate.
A port of some of the ideas and code from "Scrap Your Boilerplate: A Practical Design Pattern for Generic Programming" by Lämmel and Peyton Jones to Rust.
⚠ Depends on the specialization nightly Rust feature. ⚠
Say we work on some software that models companies, their departments, sub-departments, employees, and salaries. We might have some type definitions similar to this:
pub struct Company(pub Vec<Department>);
pub struct Department(pub Name, pub Manager, pub Vec<SubUnit>);
pub enum SubUnit {
Person(Employee),
Department(Box<Department>),
}
pub struct Employee(pub Person, pub Salary);
pub struct Person(pub Name, pub Address);
pub struct Salary(pub f64);
pub type Manager = Employee;
pub type Name = &'static str;
pub type Address = &'static str;
One of our companies has had a morale problem lately, and we want to transform
it into a new company where everyone is excited to come in every Monday through
Friday morning. But we can't really change the nature of the work, so we figure
we can just give the whole company a 10% raise and call it close enough. This
requires writing a bunch of functions with type signatures like fn(self, k: f64) -> Self
for every type that makes up a Company
, and since we recognize
the pattern, we should be good Rustaceans and formalize it with a trait:
pub trait Increase: Sized {
fn increase(self, k: f64) -> Self;
}
A company with increased employee salaries is made by increasing the salaries of each of its departments' employees:
impl Increase for Company {
fn increase(self, k: f64) -> Company {
Company(
self.0
.into_iter()
.map(|d| d.increase(k))
.collect()
)
}
}
A department with increased employee salaries is made by increasing its manager's salary and the salary of every employee in its sub-units:
impl Increase for Department {
fn increase(self, k: f64) -> Department {
Department(
self.0,
self.1.increase(k),
self.2
.into_iter()
.map(|s| s.increase(k))
.collect(),
)
}
}
A sub-unit is either a single employee or a sub-department, so either increase the employee's salary, or increase the salaries of all the people in the sub-department respectively:
impl Increase for SubUnit {
fn increase(self, k: f64) -> SubUnit {
match self {
SubUnit::Person(e) => {
SubUnit::Person(e.increase(k))
}
SubUnit::Department(d) => {
SubUnit::Department(Box::new(d.increase(k)))
}
}
}
}
An employee with an increased salary, is that same employee with the salary increased:
impl Increase for Employee {
fn increase(self, k: f64) -> Employee {
Employee(self.0, self.1.increase(k))
}
}
And finally, a lone salary can be increased:
impl Increase for Salary {
fn increase(self, k: f64) -> Salary {
Salary(self.0 * (1.0 + k))
}
}
Pretty straightforward.
But at the same time, that's a whole lot of boilerplate. The only interesting
part that has anything to do with actually increasing salaries is the impl Increase for Salary
. The rest of the code is just traversal of the data
structures. If we were to write a function to rename all the employees in a
company, most of this code would remain the same. Surely there's a way to factor
all this boilerplate out so we don't have to manually write it all the time?
Enter scrapmetal
:
// Imports
#[macro_use]
extern crate scrapmetal_derive;
extern crate scrapmetal;
use scrapmetal::{Everywhere, Transformation};
// Add derive(Term) to type definitions
#[derive(Term)]
pub struct Company(pub Vec<Department>);
// Etc...
// Define the `increase` transformation
let increase = |s: Salary| Salary(s.0 * 1.1);
let mut increase = Everywhere::new(Transformation::new(increase));
// Use the `increase` transformation
let new_company = increase.transform(old_company);
Nothing more required!
Dependencies
~2MB
~42K SLoC