需要配置数据的类型类实例。我有什么选择?

时间:2015-03-07 17:16:18

标签: haskell configuration yesod typeclass

使用yesod和persistent,我做了我认为处理Markdown数据的方便类型:

{-# LANGUAGE OverloadedStrings #-}

module Utils.MarkdownText where

import Prelude
import Data.Text.Lazy
import Data.Text as T
import Database.Persist
import Database.Persist.Sql
import Text.Blaze
import Text.Markdown

newtype MarkdownText = MarkdownText { rawMarkdown :: T.Text }

instance PersistField MarkdownText  where
    toPersistValue = PersistText . rawMarkdown

    fromPersistValue (PersistText val) = Right $ MarkdownText { rawMarkdown = val }
    fromPersistValue _ = Left "invalid type"

instance PersistFieldSql MarkdownText where
    sqlType _ = SqlString

instance ToMarkup MarkdownText where
    toMarkup = (markdown def) . fromStrict . rawMarkdown

    preEscapedToMarkup = toMarkup . rawMarkdown

您可能会在ToMarkup实例中注意到我使用def来获取降价参数。如果我想更改这些设置,而不是在此模块中硬编码,我有哪些选择?

我考虑过让MarkdownText将设置信息作为参数的选项,但还有其他选项(如果有的话)?

1 个答案:

答案 0 :(得分:4)

我将简化问题,以便我们只需要核心库。我们希望根据包含ShowMarkdownText的某些ExampleSettings更改prefix suffix的方式。

{-# LANGUAGE OverloadedStrings #-}

import Data.Text as T
import Data.Monoid
import Data.String

newtype MarkdownText = MarkdownText { rawMarkdown :: T.Text}

instance IsString MarkdownText where
    fromString = MarkdownText . fromString

data ExampleSettings = ExampleSettings { prefix :: T.Text, suffix :: T.Text }
def = ExampleSettings "" ""

emphasise = def { prefix = "*", suffix = "*" }

showWithSettings :: ExampleSettings -> T.Text -> String
showWithSettings set = show . (\x -> prefix set <> x <> suffix set)

instance Show MarkdownText where
    show = showWithSettings def . rawMarkdown

main = print $ MarkdownText "Hello World"

如何解决此问题有很多选项,首先是在值级别,然后是类型级别,最后是全局类型级别。

添加字段

我们可以选择如何进行。最简单的选项是在值级别添加设置。我们将使用MarkdownText包装设置。

data ConfiguredMarkdownText = ConfiguredMarkdownText {
                                  markdownText :: MarkdownText,
                                  settings :: ExampleSettings }

instance Show ConfiguredMarkdownText where
    show t = showWithSettings (settings t) (rawMarkdown . markdownText $ t)

main = print $ ConfiguredMarkdownText "Hello World" emphasise

为方便起见,我们在第一部分为IsString添加了MarkdownText个实例。

添加类型参数

我们可以在类型级别而不是在值级别上携带我们需要的额外数据。我们向MarkdownText添加了一个类型参数,以指示要使用的设置。

newtype MarkdownText s = MarkdownText { rawMarkdown :: T.Text}

我们使用类型来表示可能的设置

data Def = Def
data Emphasise = Emphasise

我们可以为确定设置的类型和可能设置的实例添加类型类。

{-# LANGUAGE FunctionalDependencies #-}

class Setting v k | k -> v where
    setting :: proxy k -> v

instance Setting ExampleSettings Def where
    setting _ = def

instance Setting ExampleSettings Emphasise where
    setting _ = emphasise

只要Show提供MarkdownText s,我们就可以s任意Setting

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}

instance (Setting ExampleSettings s) => Show (MarkdownText s) where
    show t = showWithSettings (setting t) (rawMarkdown t)

main = print ("Hello World" :: MarkdownText Emphasise)

MarkdownText :: * -> *需要稍微不同的IsString实例。

instance IsString (MarkdownText s) where
    fromString = MarkdownText . fromString

从类型参数

中反映值

reflection包提供了一种临时将值与类型相关联的方法。这让我们可以像前面的例子那样做,但不需要自己的类型来表示设置。

import Data.Reflection

我们首先向MarkdownText添加一个额外的类型参数,与上一节相同。

newtype MarkdownText s = MarkdownText { rawMarkdown :: T.Text}

反射包定义了一个类Reifies,它几​​乎与我们为上一节制作的Setting类相同。这让我们直接跳到定义Show实例。

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}

instance (Reifies s ExampleSettings) => Show (MarkdownText s) where
    show t = showWithSettings (reflect t) (rawMarkdown t)

我们将定义一个简便的函数来标记MarkdownText的类型参数

markdownText :: proxy s -> T.Text -> MarkdownText s
markdownText _ = MarkdownText

并完成设置显示ExampleSettings时要使用的MarkdownText的示例。我们使用reify :: a -> (forall s. Reifies s a => Proxy s -> r) -> r提供带有值的值,该值会传回代理值,该代理值已经被赋予了值。

main = reify emphasise (\p -> print (markdownText p "Hello World"))

这比下一节更简单的版本有优势;多个设置可用于具有不同类型参数的MarkdownText

main = reify emphasise $ \p1 ->
       reify def $ \p2 ->
       do
           print (markdownText p1 "Hello World")
           print (markdownText p2 "Goodbye")

反映全局配置

反射包还定义了一个更简单的类Given。它被定义为class Given a where given :: a。它表示可以根据值本身的类型确定的值。这允许我们为特定类型提供单个全局配置值,例如ExampleSettings。我们可以直接编写MarkdownText的show实例。

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}

instance (Given ExampleSettings) => Show (MarkdownText) where
    show = showWithSettings given . rawMarkdown

我们为given ExampleSettings提供了give :: a -> (Given a => r) -> r

main = give emphasise $ print (MarkdownText "Hello World")