1

我正在编写一个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 Gender = Male | Female | Any 
-- desired values of above data types
-- Male = "male"
-- Female = "female"
-- Any = "male,female"

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

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

4

1 回答 1

1

保持简单但不安全的最简单方法是仅使用带有键和值的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 看起来非常易读。

于 2015-05-01T19:01:57.773 回答