Экземпляр класса типа не используется при выводе, содержащем структуру данных

Я пытался использовать в своем коде больше newtype оберток, чтобы создать более различные типы. Я также часто делаю дешевую сериализацию с использованием Read / Show, особенно в виде простой формы строго типизированного файла конфигурации. Я столкнулся с этим сегодня:

Пример начинается так, и я определяю простой новый тип для обтекания Int вместе с именованным полем для разворачивания:

module Main where

import Debug.Trace ( trace )
import Text.Read ( readEither )


newtype Bar = Bar { unBar :: Int }
   deriving Show

Пользовательский экземпляр для чтения одного из них с помощью простого синтаксиса Int. Идея здесь в том, что было бы здорово иметь возможность поместить "42" в файл конфигурации вместо "Bar {unBar = 42}".

Этот экземпляр также имеет «ведение журнала» трассировки, чтобы мы могли видеть, когда этот экземпляр действительно используется, при наблюдении за проблемой.

instance Read Bar where
   readsPrec _ s = [(Bar i, "")]
      where i = read (trace ("[debug \"" ++ s ++ "\"]") s)

Теперь еще один тип, содержащий Bar. Этот будет просто автоматически извлекать Read.

data Foo = Foo { bar :: Bar }
   deriving (Read, Show)


main :: IO ()
main = do

Десериализация только типа Bar работает нормально и использует экземпляр Read выше.

   print $ ((readEither "42") :: Either String Bar)
   putStrLn ""

Но по какой-то причине Foo, содержащий Bar и автоматически производный от Read, не выполняет детализацию и не выбирает экземпляры Bar! (Обратите внимание, что сообщение об отладке из трассировки также не отображается)

   print $ ((readEither "Foo { bar = 42 }") :: Either String Foo)
   putStrLn ""

Итак, хорошо, а как насчет формы отображения по умолчанию для панели, которая должна совпадать с правом чтения по умолчанию?

   print $ ((readEither "Foo { bar = Bar { unBar = 42 } }") :: Either String Foo)

Нет! Тоже не работает !! Опять же, нет отладочного сообщения.

Вот результат выполнения:

  $ stack exec readbug
  [debug "42"]
  Right (Bar {unBar = 42})

  Left "Prelude.read: no parse"

  Left "Prelude.read: no parse"

Мне это кажется ошибочным, но я хотел бы услышать, что я делаю это неправильно.

Доступен полностью рабочий пример приведенного выше кода. См. Файл src/Main.lhs в тестовом проекте на darcshub


person dino    schedule 16.12.2016    source источник
comment
Это очень хороший вопрос. Мне нравится, насколько легко вы помогли кому-то начать отладку вашего кода. Надеюсь, мой ответ поможет определить конкретную проблему, с которой вы столкнулись. Кроме того, я бы не рекомендовал когда-либо использовать Read для чего-либо, кроме отладки, - а затем убедитесь, что read . show = id. Я бы поместил свою конфигурацию либо в JSON (и использовал aeson для кодирования / декодирования), либо (если вы настаиваете на настраиваемом парсере) использовал что-то вроде attoparsec или megaparsec. Read - феноменально неэффективный синтаксический анализатор, потому что он готов возвращаться куда угодно.   -  person Alec    schedule 16.12.2016
comment
Вы ошибаетесь: производный экземпляр для Foo использует экземпляр Read для Bar, который вы написали! Просто экземпляр Foo терпит неудачу, прежде чем он потрудится принудительно установить значение Bar (следовательно, никогда не заставляет преобразователь с trace в нем), потому что Bar неверно сообщает, что он потребил весь оставшийся ввод, и поэтому читатель Foo не видит } это необходимо для успеха.   -  person Daniel Wagner    schedule 16.12.2016
comment
@Alec Я не рассматривал возможность использования JSON для конфигов. Сохраняет типизацию и иерархическую структуру. И затем вы получаете файл конфигурации, который можно использовать в других языках / системах. Я сейчас немного исследую это с новыми типами. Спасибо!   -  person dino    schedule 16.12.2016


Ответы (1)


Проблема в Read. readsPrec необходимо рассмотреть возможность того, что он может увидеть что-то еще после Bar. Цитата из Prelude:

readsPrec d s пытается проанализировать значение с начала строки, возвращая список (<parsed value>, <remaining string>) пар. Если нет успешного синтаксического анализа, возвращенный список пуст.

В вашем случае вы хотите:

instance Read Bar where
   readsPrec d s = [ (Bar i, s') | (i, s') <- readsPrec d tracedS ]
      where tracedS = trace ("[debug \"" ++ s ++ "\"]") s

Затем работает следующее:

ghci> print $ ((readEither "Foo { bar = 42 }") :: Either String Foo)
[debug " 42 }"]
Right (Foo {bar = Bar {unBar = 42}})

Другая ваша проблема, а именно:

Итак, хорошо, а как насчет формы отображения по умолчанию для панели, которая должна совпадать с правом чтения по умолчанию?

 print $ ((readEither "Foo { bar = Bar { unBar = 42 } }") :: Either String Foo)

это ваша вина: вы определили экземпляр Read для Bar, так что read . show не является операцией идентификации. Когда Foo наследует Read, он использует экземпляр Bars Read (он не пытается регенерировать код, который Bar сгенерировал бы, если бы вы производили Read на нем).

person Alec    schedule 16.12.2016
comment
Я знаю, что это своего рода касательная, но не могу не предположить: instance Read Bar where readsPrec = coerce (readsPrec @Int) - person Daniel Wagner; 16.12.2016
comment
@ Алек А! Я не осознавал этой разницы между read и readsPrec, но гораздо разумнее использовать одну и ту же функцию. Спасибо. - person dino; 16.12.2016
comment
@DanielWagner Ох, мне нравится лаконичность этого. Я не знал о Data.Coerce. Интересно, эффективнее ли это. Также требуется -XTypeApplications Спасибо! - person dino; 16.12.2016
comment
@dino coerce иногда может иметь большое значение в эффективности, но в этом случае это почти наверняка будет незаметно. - person Daniel Wagner; 16.12.2016