【问题标题】:Avoiding repeated instance declarations in Haskell避免在 Haskell 中重复实例声明
【发布时间】:2019-03-20 19:18:32
【问题描述】:

我的问题似乎与this 密切相关 一。

我的代码解析一个 yaml 文件,重新排列对象并写入一个新的 yaml 文件。它工作得很好,但其中有一个特别丑陋的部分。

我必须将我的数据结构声明为 FromJsonToJson 的实例,如下所示:

instance FromJSON Users where
  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance ToJSON Users where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })

问题是我必须在其他 8 个左右的情况下重复此操作:

instance FromJSON Role where
  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
instance ToJSON Role where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })

...
...

我不知道如何避免这种重复。是否有某种方法可以只声明这两个函数一次(例如在一个新类中)并让所有这些数据类型从中派生?

解决方案(另见 dfeuer 接受的答案):

我个人喜欢这个解决方案。您需要添加

{-# language DerivingVia #-}
{-# language UndecidableInstances #-}

newtype NP a = NP {unNP::a} 

instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (NP a) where
  parseJSON = fmap NP . genericParseJSON 
    (defaultOptions { fieldLabelModifier = body_noprefix })

instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (NP a) where
  toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix }) . unNP

然后你可以像这样声明类型:

data User = User { ... } deriving (Show, Generic)
                         deriving FromJSON via (NP User)
                         deriving ToJSON via (NP User)

【问题讨论】:

    标签: haskell instance dry deriving derivingvia


    【解决方案1】:

    这就是相当新的DerivingVia 扩展的用途。

    {-# language DerivingVia #-}
    
    newtype NP a = NP {unNP::a}
    
    instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (NP a) where
      parseJSON = fmap NP . genericParseJSON 
        (defaultOptions { fieldLabelModifier = body_noprefix })
    
    instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (NP a) where
      toJSON = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix }) . unNP
    

    现在,你可以写了

    deriving via (NP User) instance FromJSON User
    

    或者

    data User = ...
      deriving Generic
      deriving (FromJSON, ToJSON) via (NP User)
    

    等等。

    这并没有比 leftaroundabout 的答案节省很多。但是,一旦您添加了 toEncoding 的定义,它就开始显得有价值了。

    注意:我没有测试过这些。

    【讨论】:

    • 我认为这是迄今为止最优雅的解决方案。这里只是一个小错字:ToJSON = 应该是 toJSON =
    【解决方案2】:

    喜欢,

    noPrefixParseJSON :: (Generic a, GFromJSON Zero (Rep a)) => Value -> Parser a
    noPrefixParseJSON
        = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
    noPrefixToJSON :: (Generic a, GToJSON Zero (Rep a)) => a -> Value
    noPrefixToJSON
        = genericToJSON (defaultOptions { fieldLabelModifier = body_noprefix })
    
    instance FromJSON User where parseJSON = noPrefixParseJSON
    instance ToJSON User where toJSON = noPrefixToJSON
    instance FromJSON Role where parseJSON = noPrefixParseJSON
    instance ToJSON Role where toJSON = noPrefixToJSON
    ...
    

    当然这仍然是重复的,但我想说这不再是任何担心的原因。

    【讨论】:

    • 提取函数定义是个好主意,但我认为主要问题仍然存在。也许通过模板 haskell 的元编程解决方案是正确的方法?
    【解决方案3】:

    如果显式声明所有这些类似的实例证明过于繁重,也许您可​​以使用phantom type like 参数化您的数据类型

    data User x = User { aa :: Int, bb :: Bool } deriving Generic
    
    data Role x = Role { xx :: Int, dd :: Bool } deriving Generic
    

    然后定义一个“标记”数据类型,如

    data Marker
    

    此数据类型将仅用作挂起如下实例的挂钩

    {-# language UndecidableInstances #-}
    instance (Generic (f Marker), GFromJSON Zero (Rep (f Marker))) => FromJSON (f Marker) where 
        parseJSON = noPrefixParseJSON
    

    值得吗?可能不会,因为数据类型的定义变得更加复杂。另一方面,您可以通过改变标记来改变序列化的各个方面,从而获得一些灵活性。

    【讨论】:

    • 该实例与 很多 重叠。我认为采用幻像标记(甚至是标记列表)的新类型会更干净。如果您想预先进行大量类型级别的工作,这对于我在回答中概述的 DerivingVia 方法也可能很有用。
    • @dfeuer 我相信它与同样使用 Marker 参数化的更具体的类型重叠,但想法是 Marker 将仅用于在同一模块中定义的实体,以及那些我们想要“统一”实例的实体反正。我看到的 newtype 的一个问题是,如果 Role 嵌套在 User 中,我们还必须将 newtype 放在那里,这似乎不方便。
    • 我很确定它在其他一些情况下也会影响实例解析。例如,假设您想要Const Int a 的实例,其中a 既未指定也未推断。将您的实例置于作用域内将导致解析失败,否则它会成功,因为编译器现在想知道 a不是标记类型。
    • @dfeuer GHC 用户指南指出“有潜在重叠是可以的[…]只有在特定 约束匹配多个 [instance]" downloads.haskell.org/~ghc/latest/docs/html/users_guide/… 因此,只要我们的程序中不需要来自 Const Int MarkerFromJSON,我们就不会出现任何重叠实例错误。
    • 嗯......看起来我对没有那个实例的分辨率过于乐观了。我很确定我遇到过在搞砸Data.Constraint.Forall 时会受伤的情况;我得再深入一点,看看能不能给出一个真实的例子。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-23
    • 2018-03-08
    • 2016-12-21
    • 2022-12-24
    • 2020-06-09
    • 1970-01-01
    • 2023-03-29
    相关资源
    最近更新 更多