考虑这个例子(来自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
Run Code Online (Sandbox Code Playgroud)
是否有一种很好的方式来抽象"来回"转换,例如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)
Run Code Online (Sandbox Code Playgroud)
但感觉不是很灵活,并且需要MultiParamTypeClasses,FunctionalDependencies,TypeSynonymInstances和FlexibleInstances-这可能是不言而喻的指标略有洁癖.
phi*_*ler 41
你lifted其实是一样dimap的Data.Profunctor:
onWords = dimap words unwords
capitalize = onWords (map capWord)
Run Code Online (Sandbox Code Playgroud)
这可能不是你想到的泛化方向.但在同等功能的类型看Control.Functor来自category-extras:
dimap :: Bifunctor f (Dual k) k k => k b a -> k c d -> k (f a c) (f b d)
Run Code Online (Sandbox Code Playgroud)
这个版本将它概括为a QFunctor和a的所有内容PFunctor.在日常场景中没那么有用,但有趣.
Cat*_*lus 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
Run Code Online (Sandbox Code Playgroud)
在概念上与你的类型类相同,除非没有滥用类型类机器.
小智 10
你可以使用镜头.镜头比我想象的要普遍得多,但任何有这种双向功能的镜头都可以制作成镜头.
例如,给定words和unwords,我们可以制作一个worded镜头:
worded :: Simple Iso String [String]
worded = iso words unwords
Run Code Online (Sandbox Code Playgroud)
然后你可以用它在镜头内部应用一个功能,例如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
Run Code Online (Sandbox Code Playgroud)
像DarkOtter建议,爱德华Kmett的lens图书馆有你覆盖,但Lens太弱Iso是稍微过强,因为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
Run Code Online (Sandbox Code Playgroud)
现在你可以定义capitalize为
capitalize' :: String -> String
capitalize' = wordPrism %~ map capWord
-- a.k.a = over wordPrism (map capWord)
Run Code Online (Sandbox Code Playgroud)
但是对于你的情况,这有相当的病态默认行为.对于String无法映射为同构的s(在其中的行中有多个空格的字符串)over wordPrism g == id.对于Prisms 应该有一个"over if if possible"运算符,但我不知道一个.你可以定义它:
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
Run Code Online (Sandbox Code Playgroud)
现在,真的,这两个都非常不令人满意,因为你真正想要的是大写所有单词并保留间距.因为(words, unwords)我上面强调的同构不存在,因此这种情况太弱了.您必须编写自己的自定义机器,以保留空间,之后您Iso可以直接使用DarkOtter的答案.