Реализуем Type Inference на Rust — Часть #2: Простой Lambda Calculus

от автора

В прошлой статье мы реализовывали алгоритм унификации (unification) на Rust-е. Теперь давайте применим его на реальном примере — простое lambda исчисление.

Синтаксис

Синтаксис выражений у нашего простого lambda calculus-а будет выглядеть так:

<expr> ::= <identifier>         |  \ <identifier> . <expr>         | ( <expr> <expr> )

В данном случае \ <identifier> . <expr> это синтаксис для лямбд:

\x.x  ^ параметр лямбды    ^ "возвращаемое" значение \x.\y.x \x.\y.y \a.((\x.\y.y) a)

А <expr> <expr>, для применений:

\x.\y.(x y)        ^ лямбда          ^ аргумент

Теперь когда с синтаксисом мы ознакомились, мы можем приступить к самому сладкому: реализации!

Токенизация (сканирование, или лексический анализ, или …)

Первое, что нам нужно сделать с исходником на нашем простеньком языке программирования это превратить его в список токенов, то есть токенизировать:

Токен — здесь можно рассматривать как грамматическую единицу языка: разные виды пунктуации, название (идентификатор), иногда пробелы, ключевые слова и т.д.

Разметим структуру токена в нашем языке:

#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum Token {     Lambda, // `\`     OpenParent, // `(`     CloseParent, // `)`     Dot, // `.`     Name(String), // identifier }

Реализуем токенизацию!

pub fn tokenize(source: &str) -> Vec<Token> {     let mut chars = source.chars().peekable();     let mut tokens = Vec::new();      while let Some(c) = chars.next() {         match c {             '(' => tokens.push(Token::OpenParent),             ')' => tokens.push(Token::CloseParent),             '\\' => tokens.push(Token::Lambda),             '.' => tokens.push(Token::Dot),             _ => {

Здесь мы обрабатываем токены состоящие из единичных символов — пунктуацию. Теперь самое интересное — идентификаторы:

                if c.is_whitespace() {                     // ignore                 } else if c.is_alphabetic() || c == '_' {                     ...                 } else {                     panic!("unexpected character `{}`", c);                 }             }         }     }      tokens }

Здесь мы делаем несколько вещей. Во-первых, мы пропускаем пробелы в нашей программе (никакого значения даже с точки зрения синтаксиса они здесь не имеют). Во-вторых, если мы обнаруживаем невалидный символ, мы выводим ошибку (пока-что делаем панику, что позже исправим). В-третьих обрабатываем идентификаторы:

                    let mut name = String::new();                      name.push(c);                      while let Some(&c) = chars.peek() {                         if c.is_alphanumeric() || c == '_' {                             name.push(c);                             chars.next();                         } else {                             break;                         }                     }                      tokens.push(Token::Name(name));

Пока следующий символ это либо число, либо буква, либо '_', мы продолжаем идти дальше и добавлять символы в нашу строку name, иначе мы прерываем цикл и идем сканировать следующие токены.

Парсер

Теперь же, когда мы имеем список токенов (с которым работать проще, нежели чем с исходным кодом), мы должны провести синтаксический анализ:

Парсер берет список токенов и преобразует его в абстрактное синтаксическое дерево или AST.

Первое, что мы должны сделать — разметить AST для выражений:

#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum Expression {     Lambda {         parameter: String,         value: Box<Self>,     },     Apply {         callee: Box<Self>,         argument: Box<Self>,     },     Variable {         name: String,     }, } 

Я надеюсь, здесь вопросов не должно быть.

Теперь мы должны создать структуру, которая будет хранить итератор по токенам, для того, чтобы по ним проходиться:

struct ParseState<I> where     I: Iterator<Item = Token>, {     tokens: Peekable<I>, }  impl<I> ParseState<I> where     I: Iterator<Item = Token>, {     fn new(tokens: I) -> Self {         Self {             tokens: tokens.peekable(),         }     }

Теперь можно приступить к самому интересному! Начнем с выражений завернутых в скобки:

    fn parse_parenthesized_expression(&mut self) -> Expression {         let inner_expression = self.parse_expression();          if !matches!(self.tokens.next(), Some(Token::CloseParent)) {             panic!("expected `)`");         }          inner_expression     }

Здесь мы не проверяем на открывающую скобку, потому-что предполагается, что при вызове функции она уже обработана.

Теперь обработка лямбд:

    fn parse_lambda(&mut self) -> Expression {         let parameter = match self.tokens.next() {             Some(Token::Name(name)) => name,             _ => panic!("expected identifier"),         };          if !matches!(self.tokens.next(), Some(Token::Dot)) {             panic!("expected `.`");         }          let value = self.parse_primary_expression();          Expression::Lambda {             parameter,             value: Box::new(value),         }     }

Здесь тоже предполагается, что \ уже обработан.

В данном случае мы можем заметить метод parse_primary_expression. Это по сути парсинг всех выражений кроме применений (<expr> <expr>), потому-что например здесь:

\x.x a

Мы парсим не x a цельно, а берем толькоx. И поэтому получаем совершенно другой порядок действий, так как \x.x и a это два раздельных выражения.

Реализуем этот самый метод!

    fn parse_primary_expression(&mut self) -> Expression {         match self.tokens.next() {             Some(Token::Lambda) => self.parse_lambda(),             Some(Token::OpenParent) => self.parse_parenthesized_expression(),             Some(Token::Name(name)) => Expression::Variable { name },             _ => panic!("expected expression"),         }     }

Здесь мы вызываем все функции, которые мы написали до этого и еще обрабатываем простые переменные.

Теперь же нам нужно обрабатывать применения (<expr> <expr>):

    fn parse_expression(&mut self) -> Expression {         let mut left = self.parse_primary_expression();          while !matches!(               self.tokens.peek(),                Some(Token::CloseParent) | None) {             let argument = self.parse_primary_expression();              left = Expression::Apply {                 callee: Box::new(left),                 argument: Box::new(argument),             };         }          left     } }

Здесь мы продолжаем анализировать выражения пока не встретим закрывающую скобку или пока не уткнемся в конец входных данных:

          while !matches!(               self.tokens.peek(),                Some(Token::CloseParent) | None) { ... }

Ну и самое логичное, мы анализируем аргумент при применении и сохраняем это все в AST:

            let argument = self.parse_primary_expression();              left = Expression::Apply {                 callee: Box::new(left),                 argument: Box::new(argument),             };

Таким образом мы реализовали простой парсер за ~130 строк кода для lambda исчисления. А теперь то чего мы так долго ждали:

Type inference

Помните в прошлой статье, я говорил про type constraint-ы? Так-вот пока для простоты у нас будет только 1 вид constraint-ов — равенство:

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] enum Constraint {     Eq { left: Arc<Type>, right: Arc<Type> }, }

То есть как раньше пояснялось, constraint требует, чтобы тип были равны. Соответственно unification будет как раньше принимать левый и правый тип и давать нам новые замены (substitutions).

Теперь, так как мы уже начинаем работать со scope-ами, нам нужно где-то хранить информацию о типах. Мы пока-что сделаем это самым очевидным способом:

#[derive(Debug, Clone, PartialEq, Eq, Default)] struct Environment {     variables: HashMap<String, Arc<Type>>, }  impl Environment {     fn new() -> Self {         Self::default()     } }

Теперь создадим контекст для нашего type inference-а:

#[derive(Debug, PartialEq, Eq)] struct InferenceContext<'env> {     constraints: Vec<Constraint>,     environment: &'env mut Environment,     last_type_variable_index: usize, }  impl<'env> InferenceContext<'env> {     fn new(environment: &'env mut Environment) -> Self {         Self {             constraints: Vec::new(),             environment,             last_type_variable_index: 0,         }     }

Как видим, здесь есть интересное поле — last_type_variable_index. Нужно оно для генерации новых уникальных переменных типа. Помните?

Напишем функции, которые будут генерировать новые переменные типов и сами типы заполнители:

    fn fresh_type_variable(&mut self) -> TypeVariable {         self.last_type_variable_index += 1;         TypeVariable(self.last_type_variable_index)     }      fn type_placeholder(&mut self) -> Arc<Type> {         Arc::new(Type::Variable(self.fresh_type_variable()))     }

А теперь делаем завершающие штрихи. Напишем функцию, которую назовем infer_type. Она будет возвращать тип данного выражения:

    fn infer_type(&mut self, expression: &Expression) -> Arc<Type> {         match expression {             Expression::Variable { name } => self                 .environment                 .variables                 .get(name)                 .unwrap_or_else(|| panic!("unbound variable: {}", name))                 .clone(),

Первое, что мы должны обработать это самый простой случай. Если выражение — переменная, берем информацию о типе из scope-а.

            Expression::Lambda {                 parameter,                 value,             } => {                 let parameter_type = self.type_placeholder();                  self.environment                     .variables                     .insert(parameter.clone(), parameter_type.clone());                  let value_type = self.infer_type(value);                  Arc::new(Type::Constructor(TypeConstructor {                     name: "Function".to_owned(),                     generics: vec![parameter_type, value_type],                 }))             }

С лямбдами все поинтереснее. Во-первых, мы не знаем тип параметра лямбды. Единственное, что мы можем узнать это ее возвращаемое значение, что мы и делаем:

                let value_type = self.infer_type(value);

Соответственно, для типа параметра, мы пока оставим переменную типа (которую после можно будет убрать с помощью замен) и конечно же добавляем в текущий scope, чтобы можно было использовать параметр, при анализации возвращаемого значения:

                let parameter_type = self.type_placeholder();                  self.environment                     .variables                     .insert(parameter.clone(), parameter_type.clone());

Заметим, что мы возвращаем type constructor, с названием Function:

                Arc::new(Type::Constructor(TypeConstructor {                     name: "Function".to_owned(),                     generics: vec![parameter_type, value_type],                 }))

В данном случае, первый generic аргумент это тип параметра лямбды, а второй это тип возвращаемого значения.

Теперь приступим к анализации применений:

            Expression::Apply { callee, argument } => {                 let callee_type = self.infer_type(callee);                 let argument_type = self.infer_type(argument);                  let return_type = self.type_placeholder();                  self.constraints.push(Constraint::Eq {                     left: callee_type.clone(),                     right: Arc::new(Type::Constructor(TypeConstructor {                         name: "Function".to_owned(),                         generics: vec![argument_type.clone(), return_type.clone()],                     })),                 });                  return_type             }         }     }

Во-первых, мы анализируем типы самой лямбды и аргумента. Возвращаемое значение, мы бы в теории могли бы узнать если бы не типовые переменные. Вместо такого наивного подхода, мы можем пока пометить возвращаемое значение как переменную типа и использовать type constraint:

                self.constraints.push(Constraint::Eq {                     left: callee_type.clone(),                     right: Arc::new(Type::Constructor(TypeConstructor {                         name: "Function".to_owned(),                         generics: vec![argument_type.clone(), return_type.clone()],                     })),                 });

А исходя из информации, что Function<A, ?t> = Function<A, B> мы и так узнаем тип возвращаемого значения (?t = B), а еще и во-вторых проверим типы на type mismatch.

Поздравляю, мы почти закончили!

Теперь, нам нужна функция, которая будет проходиться по type constraint-ам и применять нашу написанную в прошлой статье функцию unify, чтобы получать замены для типовых переменных:

    fn solve(self) -> HashMap<TypeVariable, Arc<Type>> {         let mut substitutions = HashMap::new();          for constraint in self.constraints {             match constraint {                 Constraint::Eq { left, right } => {                     unify(left, right, &mut substitutions);                 }             }         }          substitutions     } }

А теперь самое сладкое, проверим работает ли наш алгоритм:

fn main() {     let mut environment = Environment::new();      environment.variables.insert(         "add".to_owned(),         tconst!(             "Function",             tconst!("int"),             tconst!("Function", tconst!("int"), tconst!("int"))         ),     );      let expression = ParseState::new(tokenize("\\a.(add a a)").into_iter()).parse_expression();      let mut context = InferenceContext::new(&mut environment);     let ty = context.infer_type(&expression);     let substitutions = context.solve();      println!("{:?}", ty.substitute(&substitutions)); }

Как видим алгоритм понял, что тип выражения \a.(add a a) если add это Function<int, Function<int, int>> это Function<int, int>, что звучит логично:

Constructor(TypeConstructor { name: "Function", generics: [Constructor(TypeConstructor { name: "int", generics: [] }), Constructor(TypeConstructor { name: "int", generics: [] })] })

Github

Весь исходный код, который я предоставляю в статьях теперь можно найти здесь.

Окончание

Спасибо большое за внимание! Я следующих статьях, мы будем говорить о более продвинутых способах реализации type inference, интересных фишках и много другого!


ссылка на оригинал статьи https://habr.com/ru/articles/753180/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *