1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
//! A [Hindley-Milner polymorphic typing system]. //! //! For brevity, the documentation heavily uses the two provided macros when //! creating types. //! //! A [`TypeSchema`] is a type that may have universally quantified type //! variables. A [`Context`] keeps track of assignments made to type variables //! so that you may manipulate [`Type`]s, which are unquantified and concrete. //! Hence a `TypeSchema` can be instantiated, using [`TypeSchema::instantiate`], //! into a `Context` in order to produce a corresponding `Type`. Two `Type`s //! under a particular `Context` can be unified using [`Context::unify`], which //! may record new type variable assignments in the `Context`. //! //! # Examples //! //! The basics: //! //! ``` //! use polytype::{ptp, tp, Context}; //! //! // filter: ∀α. (α → bool) → [α] → [α] //! let t = ptp!(0; @arrow[ //! tp!(@arrow[tp!(0), tp!(bool)]), //! tp!(list(tp!(0))), //! tp!(list(tp!(0))), //! ]); //! //! // Quantified type schemas provide polymorphic behavior. //! assert_eq!(t.to_string(), "∀t0. (t0 → bool) → list(t0) → list(t0)"); //! //! // We can instantiate type schemas to remove quantifiers //! let mut ctx = Context::default(); //! let t = t.instantiate(&mut ctx); //! assert_eq!(t.to_string(), "(t0 → bool) → list(t0) → list(t0)"); //! //! // We can register a substitution for t0 in the context: //! ctx.extend(0, tp!(int)); //! let t = t.apply(&ctx); //! assert_eq!(t.to_string(), "(int → bool) → list(int) → list(int)"); //! ``` //! //! Extended example: //! //! ``` //! use polytype::{ptp, tp, Context}; //! //! // reduce: ∀α. ∀β. (β → α → β) → β → [α] → β //! // We can represent the type schema of reduce using the included macros: //! let t = ptp!(0, 1; @arrow[ //! tp!(@arrow[tp!(1), tp!(0), tp!(1)]), //! tp!(1), //! tp!(list(tp!(0))), //! tp!(1), //! ]); //! assert_eq!(t.to_string(), "∀t0. ∀t1. (t1 → t0 → t1) → t1 → list(t0) → t1"); //! //! // Let's consider reduce when applied to a function that adds two ints //! //! // First, we create a new typing context to manage typing bookkeeping. //! let mut ctx = Context::default(); //! //! // Let's create a type representing binary addition. //! let tplus = tp!(@arrow[tp!(int), tp!(int), tp!(int)]); //! assert_eq!(tplus.to_string(), "int → int → int"); //! //! // We instantiate the type schema of reduce within our context //! // so new type variables will be distinct //! let t = t.instantiate(&mut ctx); //! assert_eq!(t.to_string(), "(t1 → t0 → t1) → t1 → list(t0) → t1"); //! //! // By unifying, we can ensure function applications obey type requirements. //! let treturn = ctx.new_variable(); //! let targ1 = ctx.new_variable(); //! let targ2 = ctx.new_variable(); //! ctx.unify( //! &t, //! &tp!(@arrow[ //! tplus.clone(), //! targ1.clone(), //! targ2.clone(), //! treturn.clone(), //! ]), //! ).expect("unifies"); //! //! // We can also now infer what arguments are needed and what gets returned //! assert_eq!(targ1.apply(&ctx), tp!(int)); // inferred arg 1: int //! assert_eq!(targ2.apply(&ctx), tp!(list(tp!(int)))); // inferred arg 2: list(int) //! assert_eq!(treturn.apply(&ctx), tp!(int)); // inferred return: int //! //! // Finally, we can see what form reduce takes by applying the new substitution //! let t = t.apply(&ctx); //! assert_eq!(t.to_string(), "(int → int → int) → int → list(int) → int"); //! ``` //! //! [`Context`]: struct.Context.html //! [`Context::unify`]: struct.Context.html#method.unify //! [`Type`]: enum.Type.html //! [`TypeSchema::instantiate`]: enum.TypeSchema.html#method.instantiate //! [`TypeSchema`]: enum.TypeSchema.html //! [Hindley-Milner polymorphic typing system]: https://en.wikipedia.org/wiki/Hindley–Milner_type_system mod context; mod macros; mod parser; mod types; pub use context::{Context, ContextChange, UnificationError}; pub use types::{Type, TypeSchema, Variable}; /// Types require a `Name` for comparison. /// /// We mandate that [`arrow`] be implemented for any such names, and we provide an implementation /// for `&'static str`. /// /// # Examples /// /// Using static strings: /// /// ``` /// # use polytype::Type; /// let t = Type::Constructed("int", vec![]) /// # ; /// ``` /// /// A custom implementation: /// /// ``` /// # use polytype::{Type, Name}; /// #[derive(Clone, PartialEq, Eq)] /// struct N(u8); /// /// impl Name for N { /// fn arrow() -> Self { /// N(0) /// } /// } /// /// let t: Type<N> = Type::Constructed(N(1), vec![]) /// # ; /// ``` /// /// [`arrow`]: #tymethod.arrow pub trait Name: Clone + Eq { /// A specific name representing an arrow must be declared. fn arrow() -> Self; /// A way of displaying the name. fn show(&self) -> String { String::from("<unshowable type>") } /// To go from a particular name's string representation to a `Name`. This should round-trip /// with [`show`]. /// /// [`show`]: #method.show fn parse(_s: &str) -> Result<Self, ()> { Err(()) } fn is_arrow(&self) -> bool { *self == Self::arrow() } } impl Name for &'static str { /// The rightwards arrow in unicode: `→`. #[inline(always)] fn arrow() -> &'static str { "→" } #[inline(always)] fn show(&self) -> String { self.to_string() } /// **LEAKY** because it gives the string a static lifetime. #[inline(always)] fn parse(s: &str) -> Result<&'static str, ()> { Ok(unsafe { &mut *Box::into_raw(s.to_string().into_boxed_str()) }) } /// The rightwards arrow in unicode: `→`. #[inline(always)] fn is_arrow(&self) -> bool { *self == "→" } }