Crates.io | moniker |
lib.rs | moniker |
version | 0.5.0 |
source | src |
created_at | 2018-07-22 08:36:52.569953 |
updated_at | 2018-10-13 03:20:44.251856 |
description | Automatically derive variable binding and alpha equivalence for abstract syntax trees |
homepage | https://github.com/brendanzab/moniker |
repository | https://github.com/brendanzab/moniker |
max_upload_size | |
id | 75471 |
size | 140,318 |
Moniker makes it simple to track variables across nested scopes in programming language implementations.
Instead of implementing name-handling code by hand (which is often tedious and error-prone), Moniker provides a number of types and traits for declaratively describing name binding directly in your language's abstract syntax tree. From this we can derive the corresponding name-handling code automatically!
It's interesting to note that the idea of a 'scope' comes up quite often in programming:
Description | Rust Example |
---|---|
case match arms | match expr { x => /* `x` bound here */ } |
let bindings | let x = ...; /* `x` bound here */ |
recursive functions | fn foo() { /* `foo` bound here */ } |
functions parameters | fn foo(x: T) { /* `x` bound here */ } |
recursive types | enum List { Nil, /* `List` bound here */ |
type parameters | struct Point<T> { /* `T` bound here */ } |
For example, let's take a silly example of a Rust function:
type Count = u32;
fn silly<T>((count, data): (Count, T)) -> T {
match count {
0 => data,
count => silly((count - 1, data)),
}
}
There's actually lots of name-binding at play here! Let's connect the binders to their corresponding binders:
// ?
// |
// v
type Count = u32;
// |
// '----------------------.
// .-------------------------|------------------.
// | .--------------------|----*------. |
// | | | | | |
// | | v v v |
fn silly<T>((count, data): (Count, T)) -> T { // |
// | | |
// .--' | |
// | *--------------. |
// v | | |
match count { // | | |
// .------' | |
// | | |
// v | |
0 => data, // | |
// .--------------. | |
// | | | |
// | v v |
count => silly((count - 1, data)), // |
// ^ |
// | |
// '-----------------------------'
}
}
Keeping track of the relationships between these variables can be a pain, and can become especially error-prone when implementing evaluators and type checkers. Moniker aims to support all of these binding structures, with minimal pain!
Here is how we would use Moniker to describe a very small functional language, with variables, anonymous functions, applications, and let bindings:
#[macro_use]
extern crate moniker;
use moniker::{Embed, Binder, Rec, Scope, Var};
use std::rc::Rc;
/// Expressions
///
/// ```text
/// e ::= x variables
/// | \x => e anonymous functions
/// | e₁ e₂ function application
/// ````
#[derive(Debug, Clone, BoundTerm)]
// ^
// |
// The derived `BoundTerm` implementation
// does all of the heavy-lifting!
pub enum Expr {
/// Variables
Var(Var<String>),
/// Anonymous functions (ie. lambda expressions)
Lam(Scope<Binder<String>, RcExpr>),
/// Function applications
App(RcExpr, RcExpr),
}
pub type RcExpr = Rc<Expr>;
We can now construct lambda expressions by doing the following:
let f = FreeVar::fresh(Some("f".to_string()));
let x = FreeVar::fresh(Some("x".to_string()));
// \f => \x => f x
Rc::new(Expr::Lam(Scope::new(
Binder(f.clone()),
Rc::new(Expr::Lam(Scope::new(
Binder(x.clone()),
Rc::new(Expr::App(
Rc::new(Expr::Var(Var::Free(f.clone()))),
Rc::new(Expr::Var(Var::Free(x.clone()))),
)),
)))
)))
More complete examples, including evaluators and type checkers, can be found
under the moniker/examples
directory.
Example Name | Description |
---|---|
lc |
untyped lambda calculus |
lc_let |
untyped lambda calculus with nested let bindings |
lc_letrec |
untyped lambda calculus with mutually recursive bindings |
lc_multi |
untyped lambda calculus with multi-binders |
stlc |
simply typed lambda calculus with literals |
stlc_data |
simply typed lambda calculus with records, variants, literals, and pattern matching |
stlc_data_isorec |
simply typed lambda calculus with records, variants, literals, pattern matching, and iso-recursive types |
Moniker is currently used in the following Rust projects:
We separate data types into terms and patterns:
Terms are data types that implement the BoundTerm
trait.
Var<N>
: A variable that is either free or boundScope<P: BoundPattern<N>, T: BoundTerm<N>>
: bind the term T
using the pattern P
Ignore<T>
: Ignores T
when comparing for alpha equalityImplementations for tuples, strings, numbers, slices, vectors, and mart pointers are also provided for convenience.
Patterns are data types that implement the BoundPattern
trait.
Binder<N>
: Captures a free variables within a term, but is ignored for alpha equalityIgnore<T>
: Ignores T
when comparing for alpha equalityEmbed<T: BoundTerm<N>>
: Embed a term T
in a patternNest<P: BoundPattern<N>>
: Multiple nested binding patternsRec<P: BoundPattern<N>>
: Recursively bind a pattern in itselfImplementations for tuples, strings, numbers, slices, vectors, and mart pointers are also provided for convenience.
Moniker is currently good enough to use for initial language prototypes, but there is more work that we'd like to do to make it a more fully-featured name binding and scope handling toolkit.
Embed
Ignore
Nest
Rec
Scope
BoundTerm
BoundPattern
Subst
String
Here is a list of interesting references and prior art that was helpful when building Moniker. Note that it isn't necessary to read and understand these to use the library, but they might be useful if you would like to contribute!
The API was mainly inspired by the Unbound and
Unbound-Generics libraries for Haskell, with some
differences. The main change that we make is to have two separate traits
(BoundTerm
and BoundPattern
) in place of Unbound's single Alpha
type
class. We've found that this better captures the semantics of the library, and
greatly cuts down on the potential for accidental misuse.
Other auto-binding libraries exist for a number of different languages:
We really want to encourage new contributors to help out! Please come chat with us on our Gitter channel - if you have any questions about the project, or just want to say hi!
This work was done in part with the generous support of YesLogic.