如何抽象"来回"转型?

Lan*_*dei 15 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
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,TypeSynonymInstancesFlexibleInstances-这可能是不言而喻的指标略有洁癖.

phi*_*ler 41

lifted其实是一样dimapData.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.在日常场景中没那么有用,但有趣.

  • 最佳答案,以及我遇到的第一个可理解的Profunctor示例. (2认同)

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)

在概念上与你的类型类相同,除非没有滥用类型类机器.

  • 这实际上只是 DarkOtter 答案的更容易理解、不太通用的版本。它也有同样的小缺陷。 (2认同)

小智 10

你可以使用镜头.镜头比我想象的要普遍得多,但任何有这种双向功能的镜头都可以制作成镜头.

例如,给定wordsunwords,我们可以制作一个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尝试在GHCI中运行`unwords(单词"我有空格")`.我怀疑@ tel的评论基本上是一样的.不幸的是,Stack Overflow软件会自动删除评论中的多个空格!我最后粘贴了一些unicode en-quads. (4认同)
  • 但这不是同构...... =( (2认同)
  • @DarkOtter类似`unwords':: String - > [Int int String]`可以用来创建一个真正的同构(至少在拉丁语1上). (2认同)

J. *_*son 7

像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的答案.