Как реализовать экземпляр Read с уже завершенным фактическим синтаксическим анализом в Haskell?

В этом ответе можно найти заявление о том, что было бы несложно реализовать экземпляр Read для типа данных Tree с уже завершенным фактическим синтаксическим анализом.

Однако мне трудно понять, как работает такая функция, как read: AFAIK, я должен реализовать функцию readsPrec вместо read, и эта readsPrec должна выполнять чтение для строк, состоящих только из одного символа. Верно ли это?
Если это так, то как реализовать Read экземпляр Tree, когда синтаксический анализ выполняется через ParsecT? Можем ли мы разбить синтаксический анализ слово за словом, или в этом есть необходимость?

Я не думал о read как о сложной функции в Haskell, но теперь я нахожу ее довольно озадаченной и запутанной, и я теряюсь в поисках Hoogle всех таких незнакомых вещей, как readP_to_S, readS и т. д.

Любая помощь или ссылка будет принята с благодарностью.


person awllower    schedule 07.08.2016    source источник
comment
Тип readsPrec — это просто Int -> String -> [(a, String)]. Вы можете вернуть пустой список, чтобы обозначить ошибку чтения, или одиночный элемент [(x, "")], где x — результат синтаксического анализа, чтобы обозначить успех. Если у вас уже есть анализатор Parsec для вашего типа или любой другой анализатор, просто запустите этот анализатор и преобразуйте его вывод в соответствующую форму.   -  person user2407038    schedule 07.08.2016
comment
Но этот "" выглядит неправильно: он не вернет остальную часть строки, которая не проанализирована, верно?   -  person awllower    schedule 08.08.2016


Ответы (1)


Я давно задавался этим вопросом, и ваш вопрос побудил меня изучить его.

Вывод: Самый простой способ сделать это вручную:

instance Read Tree where
    readsPrec _ str = [(parsecRead str,"")]

Но deriving более безопасный вариант; вышеприведенное не работает с [Tree] и другими типами данных. Насколько я понимаю, Show и Read не предназначены для ручной реализации; они должны быть производными и работать с синтаксически правильными выражениями Haskell.


Похоже, причина того, что Read не так проста, как

class Read a where
    read :: String -> a

заключается в том, что существует система комбинаторов синтаксических анализаторов, похожая на Parsec, но отличная от нее, разработанная как модульная, рекурсивная и так далее. Но поскольку мы уже используем другую библиотеку комбинаторов синтаксических анализаторов, Parsec, я думаю, что лучше возиться с другой системой как можно меньше.

В документации Prelude сказано, что минимальной полной реализацией Read является readsPrec или readPrec. Последнее описывается как «Предлагаемая замена readsPrec с использованием синтаксических анализаторов нового стиля (только GHC)». Для меня это пахнет неприятностями, так что давайте приступим к реализации readsPrec.

Тип

readsPrec :: Read a => Int -> ReadS a
type ReadS a = String -> [(a,String)]

а документация для ReadS гласит: «Синтаксический анализатор для типа a, представленный в виде функции, которая принимает String и возвращает список возможных анализов в виде пар (a,String)». Для меня не совсем очевидно, что такое «разбор», но взгляните на исходный код для read в Text.Read раскрывает:

read :: Read a => String -> a
read s = either errorWithoutStackTrace id (readEither s)

readEither :: Read a => String -> Either String a
readEither s =
    -- minPrec is defined as 0 in Text.ParserCombinators.ReadPrec
    case [ x | (x,"") <- readPrec_to_S read' minPrec s ] of
      [x] -> Right x
      []  -> Left "Prelude.read: no parse"
      _   -> Left "Prelude.read: ambiguous parse"
  where
   read' = -- read' :: P.ReadPrec a
     do x <- readPrec
        lift P.skipSpaces -- P is Text.ParserCombinators.ReadP
        return x

Я попытался расширить определения readPrec_to_S и т. д., но понял, что это того не стоит. Я думаю, определение ясно дает понять, что мы должны вернуть [(x,"")] как успешный синтаксический анализ.

Целочисленный аргумент readsPrec кажется «контекстом приоритета». Я предполагаю, что его безопасно игнорировать, если мы просто хотим анализировать одно дерево за раз, но игнорирование этого вызовет проблемы позже, если мы попытаемся, например, проанализировать экземпляры [Tree]. Я проигнорирую это, потому что я не думаю, что это стоит проблем.

Короче говоря, если у нас есть parsecRead :: String -> Tree, как определено в посте, на который вы ссылаетесь (автор назвал его read')

instance Read Tree where
    readsPrec _ str = [(parsecRead str,"")]

Если мы проверим, как это работает в программе (используя экземпляр Show, предоставленный исходным запросчиком):

main = do
    print (read "ABC(DE)F" == example)
    print ([read "ABC(DE)F", read "ABC(DE)F"] :: [Tree])
    print (read "[ABC(DE)F,ABC(DE)F]" :: [Tree])

мы получаем

True
[ABC(DE)F,ABC(DE)F]
Test.hs: Prelude.read: no parse

Сложность и отсутствие документации здесь на самом деле заставляет меня думать, что deriving (Read) на самом деле единственный безопасный вариант, если только вы не хотите углубляться в детали уровней приоритета. Кажется, я где-то читал, что Show и Read на самом деле главным образом предназначены для производных и что строки должны быть синтаксически правильными выражениями Haskell (пожалуйста, поправьте меня, если Я не прав). Для более общего синтаксического анализа такие библиотеки, как Parsec, вероятно, являются лучшим вариантом.

Если у вас есть энергия, чтобы самостоятельно изучить исходный код, соответствующие модули кажутся

person Elias Riedel Gårding    schedule 07.08.2016
comment
readsPrec _ str = [(parsecRead str, "")] кажется плохой идеей. Он полностью игнорирует ошибки синтаксического анализа и плохо сочетается с функциями, которые ожидают, что readsPrec вернет не проанализированные биты строки. И более того, даже не обязательно быть плохим в этих отношениях: parsec прекрасно умеет сообщать об ошибках синтаксического анализа и вполне способен возвращать не проанализированную часть входной строки. - person Daniel Wagner; 07.08.2016
comment
@DanielWagner Похоже, это было бы улучшением! Не могли бы вы привести пример? Я не очень хорошо знаком с парсеком и не могу найти очевидный способ вернуть не проанализированную часть в документации. - person Elias Riedel Gårding; 07.08.2016
comment
Вот доказательство концепции. В ghci parsecToReads (char 'a' <|> char 'b') "abc" производит [('a',"bc")]. - person Daniel Wagner; 08.08.2016