2016-12-16 4 views
4

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

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

module Main where 

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


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

Пользовательский экземпляр для чтения одного из них из простой синтаксис Int. Идея здесь заключается в том, что было бы здорово, если бы вместо «Bar {unBar = 42» «42» было добавлено «42» »

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

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

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

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


main :: IO() 
main = do 

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

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

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

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

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

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 in a test project on darcshub

+3

Это очень хороший вопрос. Мне нравится, насколько легко вы сделали это для того, чтобы кто-то начал отлаживать ваш код. Надеюсь, мой ответ поможет определить конкретную проблему, с которой вы сталкиваетесь. В остальном я бы не рекомендовал использовать «Чтение» для чего-то большее, чем отладку, а затем убедиться, что «прочитал». show = id'. Я бы поместил свою конфигурацию в JSON (и использовал 'aeson' для кодирования/декодирования), или (если вы настаиваете на настраиваемом парсере) используйте что-то вроде' attoparsec' или 'megaparsec'. 'Read' - феноменально неэффективный синтаксический анализатор, потому что он готов отступить в любом месте. – Alec

+2

Вы ошибаетесь: производный экземпляр для 'Foo' * is * использует экземпляр' Read' для 'Bar', который вы написали! Просто потому, что экземпляр «Foo» терпит неудачу, прежде чем он пытается заставить значение «Бар» (следовательно, никогда не заставляет thunk с «трассировкой» в нем), потому что «Бар» неправильно сообщает, что он потреблял все оставшиеся данные и поэтому читатель 'Foo' не видит' '', он должен преуспеть. –

+0

@Alec Я не рассматривал использование JSON для конфигов. Сохраняет типизацию и иерархическую структуру. И тогда вы получите файл конфигурации, который можно использовать другими языками/системами. Я изучу это с помощью новых типов. Благодаря! – dino

ответ

4

Проблема в Read. readsPrec должен учитывать возможность того, что после Bar может появиться еще несколько вещей. Quoting the 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, он использует Bar s Read экземпляр (он не пытается восстановить код, который Bar был бы сгенерирован, если бы вы получили Read на нем).

+1

Я знаю, что это своего рода касательная, но я не могу не предлагать ее: 'instance Read Bar, где readsPrec = coerce (readsPrec @Int)' –

+0

@Alec Ah! Я не осознавал эту разницу между 'read' и' readsPrec', но имеет смысл использовать одну и ту же функцию. Спасибо. – dino

+0

@ Даниэль Вагнер. О, мне все равно. Я не знал о Data.Coerce. Интересно, эффективнее ли это. Также требует '-XTypeApplications' Спасибо! – dino