如何抽象“来回”转型?

时间:2013-03-05 10:56:57

标签: haskell

考虑这个例子(来自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)

但感觉不是很灵活,需要MultiParamTypeClassesFunctionalDependenciesTypeSynonymInstancesFlexibleInstances - 这可能表明它略微超过顶部。

5 个答案:

答案 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)

你可以使用镜头。镜头比我想象的更加通用,但任何你有这种双向功能的镜头都可以制成镜头。

例如,给定wordsunwords,我们可以制作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(其中包含多行空格的字符串)Stringover 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应用于所有单词并将它们重新组合在一起”。如果你经常这样做,可以考虑写一个函数。