考虑这个例子(来自https://codereview.stackexchange.com/questions/23456/crtitique-my-haskell-function-capitalize):
import Data.Char
capWord [] = []
capWord (h:t) = toUpper h : map toLower t
capitalize = unwords . map capWord . words
是否有一种很好的方式来抽象“来回”变换,例如: unwords . f . words
?我能提出的最好的是
class Lift a b | a -> b where
up :: a -> b
down :: b -> a
instance Lift String [String] where
up = words
down = unwords
lifted :: (Lift a b) => (b -> b) -> a -> a
lifted f = down . f . up
capitalize = lifted (map capWord)
但感觉不是很灵活,需要MultiParamTypeClasses
,FunctionalDependencies
,TypeSynonymInstances
和FlexibleInstances
- 这可能表明它略微超过顶部。
答案 0 :(得分:39)
您的lifted
实际上与Data.Profunctor
中的dimap
相同:
onWords = dimap words unwords
capitalize = onWords (map capWord)
这可能不是你想到的泛化方向。但请查看来自category-extras
的{{3}}中的等效函数的类型:
dimap :: Bifunctor f (Dual k) k k => k b a -> k c d -> k (f a c) (f b d)
此版本将其概括为QFunctor
和共 - PFunctor
的所有内容。 在日常场景中非常有用,但很有趣。
答案 1 :(得分:11)
我会说最好的答案是“不,因为抽象不会给你买任何东西”。事实上,您的解决方案灵活性要低得多:范围内只能有一个Lift String [String]
实例,并且有更多方法可以将字符串拆分为字符串列表而不仅仅是words/unwords
(这意味着您将开始将新类型或更多的奥术扩展投入混合中)。保持简单 - 原始的capitalize
就好了。
或者,如果你真的坚持:
lifted :: (a -> b, b -> a) -> (b -> b) -> a -> a
lifted (up, down) f = down . f . up
onWords = lifted (words, unwords)
onLines = lifted (lines, unlines)
capitalize = onWords $ map capWord
在概念上与你的类型类相同,除非没有滥用类型类机制。
答案 2 :(得分:10)
你可以使用镜头。镜头比我想象的更加通用,但任何你有这种双向功能的镜头都可以制成镜头。
例如,给定words
和unwords
,我们可以制作worded
镜头:
worded :: Simple Iso String [String]
worded = iso words unwords
然后你可以使用它在镜头内部应用一个功能,例如lifted f x
变为(worded %~ f) x
。镜头的唯一缺点是图书馆非常复杂,并且有许多模糊的操作员,如%~
,尽管镜头的核心理念实际上非常简单。
我错误地认为unwords . words
等同于身份函数,而不是,因为单词之间的额外空格会丢失,正如几个人正确指出的那样。
相反,我们可以使用更复杂的镜头,如下所示,它确实保留了单词之间的间距。虽然我认为它仍然不是同构,但我希望这至少意味着x == (x & worded %~ id)
。另一方面,它至少不是一种非常好的做事方式,而且效率不高。单词本身的索引镜头(而不是单词列表)可能会更好,虽然它允许更少的操作(我认为,当涉及镜头时很难分辨)。
import Data.Char (isSpace)
import Control.Lens
worded :: Simple Lens String [String]
worded f s =
let p = makeParts s
in fmap (joinParts p) (f (takeParts p))
data Parts = End | Space Char Parts | Word String Parts
makeParts :: String -> Parts
makeParts = startPart
where
startPart [] = End
startPart (c:cs) =
if isSpace c then Space c (startPart cs) else joinPart (Word . (c:)) cs
joinPart k [] = k [] End
joinPart k (c:cs) =
if isSpace c then k [] (Space c (startPart cs)) else joinPart (k . (c:)) cs
takeParts :: Parts -> [String]
takeParts End = []
takeParts (Space _ t) = takeParts t
takeParts (Word s t) = s : takeParts t
joinParts :: Parts -> [String] -> String
joinParts _ [] = []
joinParts (Word _ End) (ws@(_:_:_)) = unwords ws
joinParts End ws = unwords ws
joinParts (Space c t) ws = c : joinParts t ws
joinParts (Word _ t) (w:ws) = w ++ joinParts t ws
答案 3 :(得分:7)
像DarkOtter建议的那样,Edward Kmett的lens
库已经覆盖了你,Lens
太弱了,Iso
稍微太强了{{1}不是身份。您可以尝试使用unwords . words
。
Prism
现在您可以将wordPrism :: Prism' String [String]
wordPrism = prism' unwords $ \s ->
-- inefficient, but poignant
if s == (unwords . words) s then Just (words s) else Nothing
定义为
capitalize
但是对于您的情况,这具有相当病态的默认行为。对于无法映射为同构的capitalize' :: String -> String
capitalize' = wordPrism %~ map capWord
-- a.k.a = over wordPrism (map capWord)
s(其中包含多行空格的字符串)String
。 over wordPrism g == id
s应该有一个“over if if possible”运算符,但我不知道一个。你可以定义它:
Prism
现在,实际上,这些都非常不令人满意,因为你真正想要的是大写所有单词并保留间距。因为overIfPossible :: Prism s t a b -> (a -> b) -> (s -> Maybe t)
overIfPossible p f s = if (isn't p s) then Nothing else Just (over p f s)
capitalize :: String -> Maybe String
capitalize = wordPrism `overIfPossible` map capWord
通常由于我在上面已经强调的同构不存在而太弱。你必须编写自己的自定义机器来维护空间,之后你有(words, unwords)
并且可以直接使用DarkOtter的答案。
答案 4 :(得分:1)
它确实不够灵活!你如何解除一个逐行工作的功能?你需要一个newtype
包装器!像这样
newtype LineByLine = LineByLine { unLineByLine :: String }
instance Lift LineByLine [String] where
up = lines . unLineByLine
down = LineByLine . unlines
但现在没有充分的理由比逐行更喜欢逐字逐句。
我只会使用unwords . map f . words
给我,这是惯用的“将f应用于所有单词并将它们重新组合在一起”。如果你经常这样做,可以考虑写一个函数。