如何在Haskell中处理JSON对象的可变性?

时间:2018-12-06 08:07:08

标签: json rest haskell aeson

某些REST服务具有返回JSON的变量,例如某些字段可能会根据请求的参数出现或消失,结构本身可能会更改,嵌套等。 因此,这导致类型数量的雪崩类型增长(以及FromJSON实例)。选项包括:

  1. 尝试在Maybe下创建很多字段(但这对结构的可变性无济于事)
  2. 介绍很多类型
  3. 创建不同的幻像类型(实际上与以前没什么大不同)

1。的缺点是,如果使用某些固定参数进行的调用始终返回已知字段,则您也必须处理Nothing种情况,代码会变得更加复杂。 2.和3.很累。

在Haskell中处理这种可变性的最简单/便捷的方法是什么(如果您使用Aeson,请确保另一种选择是避免使用Aeson)?

1 个答案:

答案 0 :(得分:1)

使用类型级计算解决现有/不存在字段问题的可能解决方案。

一些必需的扩展名和导入:

{-# LANGUAGE DeriveGeneric, ScopedTypeVariables, DataKinds, KindSignatures,  
             TypeApplications, TypeFamilies, TypeOperators, FlexibleContexts #-}

import Data.Aeson
import Data.Proxy
import GHC.Generics
import GHC.TypeLits

这是一种数据类型(要推广使用),它指示是否缺少某些字段或存在。也是将不存在的类型映射到()的类型族:

data Presence = Present
              | Absent

type family Encode p v :: * where
    Encode Present v = v
    Encode Absent v = ()

现在,我们可以定义一个包含所有可能字段的参数化记录,如下所示:

data Foo (a :: Presence) 
         (b :: Presence) 
         (c :: Presence) = Foo { 
                                  field1 :: Encode a Int,
                                  field2 :: Encode b Bool,
                                  field3 :: Encode c Char
                               } deriving Generic

instance (FromJSON (Encode a Int),
          FromJSON (Encode b Bool),
          FromJSON (Encode c Char)) => FromJSON (Foo a b c)

一个问题:为每次出现/不出现的组合编写完整类型将很繁琐,尤其是如果每次仅出现几个字段时。但是也许我们可以定义一个辅助类型同义词FooWith,让我们仅提及存在的那些字段:

type family Mentioned (ns :: [Symbol]) (n :: Symbol) :: Presence where
    Mentioned '[]       _  = Absent
    Mentioned (n ': _)  n  = Present
    Mentioned (_ ': ns) n  = Mentioned ns n

-- the field names are repeated as symbols, how to avoid this?
type FooWith (ns :: [Symbol]) = Foo (Mentioned ns "field1") 
                                    (Mentioned ns "field2") 
                                    (Mentioned ns "field3") 

使用示例:

ghci> :kind! FooWith '["field2","field3"]
FooWith '["field2","field3"] :: * = Foo 'Absent 'Present 'Present

另一个问题:对于每个请求,我们必须重复必填字段列表两次:一次在URL中(“ fields = a,b,c ...”),另一次在期望的类型中。最好有一个单一的真理来源。

通过使用辅助类型类Demote,我们可以从字段的类型级别列表中推断出要添加到URL的术语级别的字段列表:

class Demote (ns :: [Symbol]) where
    demote :: Proxy ns -> [String]

instance Demote '[] where
    demote _ = []

instance (KnownSymbol n, Demote ns) => Demote (n ': ns) where
    demote _ = symbolVal (Proxy @n) : demote (Proxy @ns)

例如:

ghci> demote (Proxy @["field2","field3"])
["field2","field3"]