保持简单但不安全的最简单方法是仅使用带有键和值的Arbitrary
字段的基本 ADT:String
data FilterKey
= Arbitrary String String
| Sentiment Sentiment
| Gender Gender
deriving (Eq, Show)
data Sentiment
= Positive
| Negative
| Neutral
deriving (Eq, Show, Bounded, Enum)
data Gender
= Male
| Female
| Any
deriving (Eq, Show, Bounded, Enum)
然后您需要一个函数来将 a 转换FilterKey
为您的 API 的基本(String, String)
过滤器类型
filterKeyToPair :: FilterKey -> (String, String)
filterKeyToPair (Arbitrary key val) = (key, val)
filterKeyToPair (Sentiment sentiment) = ("sentiment", showSentiment sentiment)
filterKeyToPair (Gender gender) = ("gender", showGender gender)
showSentiment :: Sentiment -> String
showSentiment s = case s of
Positive -> "positive"
Negative -> "negative"
Neutral -> "neutral"
showGender :: Gender -> String
showGender g = case g of
Male -> "male"
Female -> "female"
Any -> "male,female"
最后你可以只包装你的基本 API 的comments
函数,这样 filters 参数就更安全了,并且它在(String, String)
内部被转换为表单来发送请求
comments :: String -> [FilterKey] -> Config -> Result
comments name filters conf = do
let filterPairs = map filterKeyToPair filters
commentsRaw name filterPairs conf
这将工作得很好并且相当容易使用:
comments "tide-pods" [Arbitrary "limits" "3", Sentiment Positive, Gender Female] config
但它不是很可扩展。如果您的库的用户想要扩展它以添加一个Limit Int
字段,他们必须将其写为
data Limit = Limit Int
limitToFilterKey :: Limit -> FilterKey
limitToFilterKey (Limit l) = Arbitrary "limit" (show l)
相反,它看起来像
[limitToFilterKey (Limit 3), Sentiment Positive, Gender Female]
这不是特别好,特别是如果他们试图添加许多不同的字段和类型。一个复杂但可扩展的解决方案是使用单一Filter
类型,实际上为了简单起见,它能够表示单个过滤器或过滤器列表(尝试在 where 实现它Filter = Filter [(String, String)]
,它有点难以干净地完成):
import Data.Monoid hiding (Any)
-- Set up the filter part of the API
data Filter
= Filter (String, String)
| Filters [(String, String)]
deriving (Eq, Show)
instance Monoid Filter where
mempty = Filters []
(Filter f) `mappend` (Filter g) = Filters [f, g]
(Filter f) `mappend` (Filters gs) = Filters (f : gs)
(Filters fs) `mappend` (Filter g) = Filters (fs ++ [g])
(Filters fs) `mappend` (Filters gs) = Filters (fs ++ gs)
然后有一个类来表示转换为Filter
(很像Data.Aeson.ToJSON
):
class FilterKey kv where
keyToString :: kv -> String
valToString :: kv -> String
toFilter :: kv -> Filter
toFilter kv = Filter (keyToString kv, valToString kv)
的实例Filter
非常简单
instance FilterKey Filter where
-- Unsafe because it doesn't match the Fitlers contructor
-- but I never said this was a fully fleshed out API
keyToString (Filter (k, _)) = k
valToString (Filter (_, v)) = v
toFilter = id
您可以在此处轻松组合此类型的值的一个快速技巧是
-- Same fixity as <>
infixr 6 &
(&) :: (FilterKey kv1, FilterKey kv2) => kv1 -> kv2 -> Filter
kv1 & kv2 = toFilter kv1 <> toFilter kv2
FilterKey
然后,您可以编写使用的类的实例:
data Arbitrary = Arbitrary String String deriving (Eq, Show)
infixr 7 .=
(.=) :: String -> String -> Arbitrary
(.=) = Arbitrary
instance FilterKey Arbitrary where
keyToString (Arbitrary k _) = k
valToString (Arbitrary _ v) = v
data Sentiment
= Positive
| Negative
| Neutral
deriving (Eq, Show, Bounded, Enum)
instance FilterKey Sentiment where
keyToString _ = "sentiment"
valToString Positive = "positive"
valToString Negative = "negative"
valToString Neutral = "neutral"
data Gender
= Male
| Female
| Any
deriving (Eq, Show, Bounded, Enum)
instance FilterKey Gender where
keyToString _ = "gender"
valToString Male = "male"
valToString Female = "female"
valToString Any = "male,female"
加一点糖:
data Is = Is
is :: Is
is = Is
sentiment :: Is -> Sentiment -> Sentiment
sentiment _ = id
gender :: Is -> Gender -> Gender
gender _ = id
您可以编写查询,例如
example
= comments "tide-pods" config
$ "limit" .= "3"
& sentiment is Positive
& gender is Any
如果您不将构造函数导出到Filter
并且不导出,此 API 仍然是安全的toFilter
。我把它作为类型类上的一个方法简单地留下了,以便Filter
可以覆盖它以id
提高效率。然后您图书馆的用户只需
data Limit
= Limit Int
deriving (Eq, Show)
instance FilterKey Limit where
keyToString _ = "limit"
valToString (Limit l) = show l
如果他们想保持is
他们可以使用的风格
limit :: Is -> Int -> Limit
limit _ = Limit
写一些类似的东西
example
= comments "foo" config
$ limit is 3
& sentiment is Positive
& gender is Female
但是这里只是作为一个例子,你可以让 Haskell 中的 EDSL 看起来非常易读。