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 看起来非常可读。
我正在写一个 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 看起来非常可读。