Haskell 数据,自定义字符串值

Haskell data, custom string values

我正在写一个 Haskell SDK,一切正常,但我想在我的搜索过滤器(url 参数)中引入更强大的类型。

示例调用如下所示:

-- list first 3 positive comments mentioned by females
comments "tide-pods" [("limit", "3"),("sentiment", "positive"),("gender", "female")] config

虽然这对我来说还不算太糟糕,但我真的很希望能够传递如下内容:

comments "tide-pods" [("limit", "3"),(Sentiment, Positive),(Gender, Male)] config

或类似的东西。

在DataRank.hs中你可以看到我的url参数类型type QueryParameter = (String, String),以及转换http-conduit参数的代码convertParameters :: [QueryParameter] -> [(ByteString, Maybe ByteString)]

我一直在试验data/types,例如:

data Gender = Male | Female | Any 
-- desired values of above data types
-- Male = "male"
-- Female = "female"
-- Any = "male,female"

api 还需要对任意字符串键、字符串值保持足够的灵活性,因为我希望 SDK 能够在不依赖 SDK 更新的情况下提供新的过滤器。出于好奇,最新的搜索过滤器列表位于最近构建的 Java SDK

我在寻找在 Haskell 中提供搜索界面的好方法时遇到了问题。提前致谢!

保持简单但不安全的最简单方法是仅使用带有 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)

然后您需要一个函数将 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 类型,实际上为了简单起见,让它能够表示单个过滤器或过滤器列表(尝试在 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)

然后有一个class来表示转换为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 class 的实例,它们适用于:

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 并且不导出 toFilter,那么此 API 仍然是安全的。我将其作为类型 class 的方法保留下来,以便 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 看起来非常可读。