thedeemon: (Default)
[personal profile] thedeemon
Некоторое время назад меня пытались убедить, что Окамл недостаточно хорош для парсер-комбинаторов, потому что стоит попытаться добавить в парсеры состояние, как тут же возникают трудности, связанные с нехваткой полиморфизма. Проблема там вот в чем. Мой вариант простых парсеров не содержал состояния, и парсером там была функция типа char list -> 'a parse_result, где тип результата выглядел так:

type 'a parse_result = Parsed of 'a * char list | Failed

В определенном смысле какое-то состояние в таких парсерах есть - это текущая позиция в разбираемом тексте, определяемая неразобранным остатком текста, возвращаемым в случае успеха. Для разбора контекстно-зависимых грамматик иногда хочется, чтобы парсер имел какое-то дополнительное состояние. Например, при разборе текста на С++ хорошо бы помнить имена описанных ранее классов, чтобы понимать, что означает some_name(42). Расширим наше определение парсера и добавим передачу произвольного состояния:

type ('res, 'state) parse_result =  
  Parsed of 'res * ('state * char list) | Failed

Парсер будет принимать на вход не просто список символов, а пару из состояния и списка символов. На выходе будет только что описанный parse_result, возможно с новым состоянием.

Тогда базовый парсер, разбирающий один символ, удовлетворяющий заданному предикату, будет выглядеть так:

let p_pred p (st, lst) =     
  match lst with 
  | [] -> Failed 
  | h::t -> if p h then Parsed(h, (st, t)) else Failed

Теперь попробуем его использовать для описания парсера, разбирающего одну цифру:

let isdigit c = c>='0' && c<='9' 
let p_digit = p_pred isdigit

При попытке это скомпилировать окамл ругается:
The type of this expression, '_a * char list -> (char, '_a) parse_result, contains type variables that cannot be generalized.
Фишка в том, что мы использовали карринг, опустив последний параметр p_pred, но этот параметр должен быть полиморфным - он содержит состояние, тип которого еще не определен. Алгоритм вывода типов в окамле умеет выводить полиморфные типы для имен, заданных в конструкции let, но не для тех, что были опущены при карринге. Простейшим решением проблемы было бы дописать недостающий параметр:

let p_digit s = p_pred isdigit s

Такой вариант компилируется нормально, но это некрасиво, т.к. придется везде писать эти лишние параметры, код раздуется, потеряется DSL-ность. Однако выход есть, и очень простой!

Вынесем типовый параметр состояния на уровень выше - в параметр модуля.
Заключим нашу библиотеку комбинаторов в функтор:

module Parsers(S : sig type t end) = struct 
  type 'res parse_result = Parsed of 'res * (S.t * char list) | Failed 
  ...(* здесь все базовые комбинаторы *) 
end

Теперь
let p_digit = p_pred isdigit
прекрасно компилируется, т.к. тип опущенного параметра известен, он содержит тип состояния S.t, который будет передан функтору при его использовании.

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

let p_many p =  
  let rec loop s =  
    match p s with   
    | Parsed(_, s') -> loop s'  
    | Failed -> Parsed((), s) in   
  loop 
 
let (>>>) p1 p2 s = 
  match p1 s with 
  | Parsed(_, s') -> p2 s' 
  | Failed -> Failed 
 
let p_many1 p = p >>> p_many p 
 
let (>>=) p1 f s = 
  match p1 s with 
  | Parsed(x, s') -> f x s' 
  | Failed -> Failed 
 
let return x s = Parsed(x, s)

let updateState f (st, lst) = Parsed((), (f st, lst))


А вот так выглядит их использование. Сделаем парсер, принимающий непустую последовательность маленьких латинских букв и подсчитывающий количество вхождений буквы 'e', причем это количество будет храниться в состоянии парсера, т.е. в данном случае тип состояния - int:

let isalpha c = c>='a' && c<='z' 
 
module P = Parsers(struct type t = int end) 
open P 
 
let myletter =  
  p_pred isalpha >>= fun c -> 
  if c='e' then updateState ((+) 1) else return () 
   
let parser = p_many1 myletter

Используем его для подсчета вхождений буквы 'e' в слове 'engineering':

let lst = String.explode "engineering" in 
match parser (0, lst) with 
| Parsed(_, (n, _)) -> print_int n 
| Failed -> print_string "failed"

Запускаем, получаем ответ - 3. Парсеры с состоянием готовы, все базовые комбинаторы от типа состояния абстрагированы, использование их требует лишь пары дополнительных строк: передать в функтор тип состояния и открыть получившийся модуль.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

Profile

thedeemon: (Default)
Dmitry Popov

May 2025

S M T W T F S
    123
45678910
11 121314151617
18192021222324
25262728293031

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 9th, 2025 01:34 am
Powered by Dreamwidth Studios