16

考虑这个例子(来自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, TypeSynonymInstancesand FlexibleInstances- 这可能表明它略微超出顶部。

4

5 回答 5

43

lifted实际上与dimapfrom相同Data.Profunctor

onWords = dimap words unwords
capitalize = onWords (map capWord)

这可能不是您考虑的泛化方向。Control.Functor但是看一下from中等价函数的类型category-extras

dimap :: Bifunctor f (Dual k) k k => k b a -> k c d -> k (f a c) (f b d)

这个版本将它概括为所有既是 aQFunctor又是 co-的东西PFunctor。在日常场景中不是那么有用,但很有趣。

于 2013-03-05T22:30:38.723 回答
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

在概念上与您的类型类相同,只是没有过多地滥用类型类机制。

于 2013-03-05T11:15:47.093 回答
10

你可以为此使用镜头。镜头比我认为的要通用得多,但是任何具有这种双向功能的东西都可以制成镜头。

例如,给定wordsunwords,我们可以制作一个worded镜头:

worded :: Simple Iso String [String]
worded = iso words unwords

然后你可以用它在镜头内部应用一个函数,例如lifted f xbecome (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
于 2013-03-05T12:11:27.713 回答
8

Like DarkOtter suggested, Edward Kmett's lens library has you covered, but Lens is too weak and Iso is slightly too strong since unwords . words isn't an identity. You could try a Prism instead.

wordPrism :: Prism' String [String]
wordPrism = prism' unwords $ \s ->
   -- inefficient, but poignant
   if s == (unwords . words) s then Just (words s) else Nothing

Now you can define capitalize as

capitalize' :: String -> String
capitalize' = wordPrism %~ map capWord
-- a.k.a    = over wordPrism (map capWord)

but this has fairly pathological default behavior for your case. For Strings which can't be mapped as isomorphisms (strings with multiple spaces in a row inside of them) over wordPrism g == id. There ought to be an "over if possible" operator for Prisms, but I don't know of one. You could define it though:

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

Now, really, both of these are pretty unsatisfactory since what you really want is to capitalize all words and retain the spacing. For this (words, unwords) is too weak generally due to the non-existence of isomorphism that I've highlighted above. You'd have to write your own custom machinery which maintains spaces after which you'd have an Iso and could use DarkOtter's answer directly.

于 2013-03-05T14:52:47.867 回答
1

确实不够灵活!您将如何提升功能以逐行工作?您将需要一个newtype包装器!像这样

newtype LineByLine = LineByLine { unLineByLine :: String }

instance Lift LineByLine [String] where
    up = lines . unLineByLine
    down = LineByLine . unlines

但是现在没有充分的理由更喜欢逐字版本而不是逐行版本。

我只会使用unwords . map f . words, 对我来说这就是惯用的“将 f 应用于所有单词并将它们重新组合在一起”。如果您经常这样做,请考虑编写一个函数。

于 2013-03-05T11:21:04.420 回答