以无点样式将字符串中的字符替换为 Haskell 中的字符串

Replacing a character in a string with a string in Haskell in point-free style

我们的想法是编写一个函数 replace,它接受三个参数、一个通配符、一个替换字符串和一个输入字符串。一个例子看起来像 replace '*' "foo" "foo*" = "foobar"。通常这不是什么大问题,我只是写一些递归的东西并检查字符串中的每个字符是否等于我的通配符。但是,我需要以无点风格编写它。我不知道该怎么做。我知道我可以删除最后一个参数,即输入字符串,但在那之后我就卡住了。

我的非无点式解决方案是: replace wildcard sub = concatMap (\c -> if c==wildcard then sub else [c]).

注意:我们不允许导入外部库,即不允许Text.Replace。

你写的函数不能只用Prelude来缩减,因为没有办法缩减if语句。 (Erich 指出这可以用 Data.Bool 中的 bool 来完成。)在这里,我设计了一种可以减少的替代治疗方法,但我希望到最后我已经说服你不要这样做.

您可能会发现此处有用的一个函数是 break。来自 Hackage:

applied to a predicate p and a list xs, returns a tuple where first element is longest prefix (possibly empty) of xs of elements that do not satisfy p and second element is the remainder of the list

因此我们可以构建一个函数来在特定元素处拆分您的列表:

splitOnChar :: Char -> String -> (String, String)
splitOnChar char = break (char ==)

从那里我们可以制定一个函数来按照您的描述执行操作:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  case break (char ==) instr of
    (front, _:back) -> front ++ repstr ++ back
    _               -> error "Character to replace not found!"

这让你摆脱了不可能写无点的if语句。为什么你会想免费写这一点是超出我的理解,但这样做,我们需要牺牲我们的错误处理。再看丢弃处理的版本

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  let ~(front, _:back) = break (char ==) instr
  in  front ++ repstr ++ back

然后我们可以用表达式替换frontback

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  let split = break (char ==) instr
  in  (fst split) ++ repstr ++ (tail $ snd split)

现在让我们将 split 移动到 in 语句的末尾:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  let split = break (char ==) instr
  in  (\a b -> a ++ repstr ++ b) <$> fst <*> tail . snd $ split

现在我们可以替换 split:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  (\a b -> a ++ repstr ++ b) <$> fst <*> tail . snd $ break (char ==) instr

接下来让我们从 lambda 表达式中减少 b

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  (\a -> ((a ++ repstr) ++)) <$> fst <*> tail . snd $ break (char ==) instr

然后对a做同样的事情:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr instr =
  ((. (repstr ++)) . (++)) <$> fst <*> tail . snd $ break (char ==) instr

然后替换instr:

replaceChar :: Char -> String -> String -> String
replaceChar char repstr =
  (((. (repstr ++)) . (++)) <$> fst <*> tail . snd) . break (char ==)

接下来减少 char 会更容易,所以我们只是翻转参数,并记得稍后再 flip 它们。

replaceChar :: String -> Char -> String -> String
replaceChar repstr char =
  (((. (repstr ++)) . (++)) <$> fst <*> tail . snd) . break (char ==)

现在我们实际上要减少 char:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  ((((. (repstr ++)) . (++)) <$> fst <*> tail . snd) .) . break . (==)

现在我们需要重新排列整个函数以得到最后的 repstr。首先将 . break . (==) 变成一个部分:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ ((((. (repstr ++)) . (++)) <$> fst <*> tail . snd) .)

解开下半场:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ ((. (repstr ++)) . (++)) <$> fst <*> tail . snd

<*> tail . snd

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ ((. (repstr ++)) . (++)) <$> fst

<$> fst 部分:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (repstr ++)) . (++)

. (++) 部分:

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (++)) $ (. (repstr ++))

取消分段 (. (repstr ++)):

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (++)) $ flip (.) $ (repstr ++)

取消分段 (repstr ++):

replaceChar :: String -> Char -> String -> String
replaceChar repstr =
  (. break . (==)) $ (.) $ (<*> tail . snd) $ (<$> fst) $ (. (++)) $ flip (.) $ (++) repstr

减少 Eta:

replaceChar :: String -> Char -> String -> String
replaceChar =
  (. break . (==)) . (.) . (<*> tail . snd) . (<$> fst) . (. (++)) . flip (.) . (++)

flip 将参数按正确的顺序放回:

replaceChar :: Char -> String -> String -> String
replaceChar =
  flip $ (. break . (==)) . (.) . (<*> tail . snd) . (<$> fst) . (. (++)) . flip (.) . (++)

Et-voilà:一堆完全难以辨认的胡言乱语,可以神奇地做你需要的事情,而人类无法理解。

由于我不完全同意@Andrew Ray 的观点,将您的函数版本转换为无点样式是不可能的,因此我想提出另一种可能性。

我们可以用 Data.Bool 中的 bool 替换 if 子句。首先,我们处理 lambda 子句 (\c -> if c==wildcard then sub else [c])。为了让我们的 replace 函数无指向性,我首先添加了一个 helper,它接受两个额外的参数,替换字符串和一个谓词,决定保留哪些字符和替换哪些字符:

helper :: [b] -> (b -> Bool) -> b -> [b]
helper repl pred c = if pred c then repl else [c]

通过使用 bool 我们摆脱了 if 子句。请注意 ifelse 分支的相反顺序,这是因为 bool 以相反的方式工作。

helper repl pred c = bool [c] repl $ pred c

由于 chelper 的主体中使用了两次,我们可以使用 Applicative 函数实例将 c 应用于多个函数,使用 liftAN 函数。虽然repl不需要c,但是我们可以通过const把它变成一个带c的函数。因此我们现在使用 liftA3

helper repl pred c = liftA3 bool ((:[])) (const repl) pred $ c

现在我们可以很容易地切掉 cpred:

helper repl = liftA3 bool ((:[])) (const repl)

并通过使用函数组合将(一般情况下)f (g x)替换为f . g $ x,我们可以将其重写为:

helper repl = liftA3 bool ((:[])) . const $ repl

我们可以再次切碎 repl:

helper = liftA3 bool ((:[])) . const

现在让我们通过将我们的助手放在那里来处理主要功能。

replace :: (Foldable t, Eq b) => b -> [b] -> t b -> [b]
replace wildcard sub = concatMap (helper sub (==wildcard))

由于我们需要 wildcardsub 的顺序不同,我们 flip helper:

replace wildcard sub = concatMap ((flip helper) (==wildcard) sub)

要从第二个术语中得到 sub,我们可以在 (.):

前缀
replace wildcard sub = (.) concatMap ((flip helper) (==wildcard)) $ sub

我们可以轻松切碎 sub:

replace wildcard = (.) concatMap ((flip helper) (==wildcard))

第三项可以用函数组合重写:

replace wildcard = (.) concatMap ((flip helper) . (==) $ wildcard)

现在分段前缀 (.):

replace wildcard = (concatMap .) ((flip helper) . (==) $ wildcard)

最后我们可以再次应用 f (g x) = f . g $ x 模式:

replace wildcard = (concatMap .) . ((flip helper) . (==)) $ wildcard

我们可以在哪里砍掉 wildcard。用我们的定义替换 helper,我们得到:

replace = (concatMap .) . ((flip $ liftA3 bool ((:[])) . const) . (==))

尽管这可能比@Andrew Ray 的解决方案更具可读性,但我仍然强烈建议您不要以这种方式强制使用无点样式。原文比这个废话更具可读性。

显然,为了无积分而写无积分是愚蠢的。

之所以考虑无点风格很有用,是因为如果处理得当,它会导致一种更具类别组合的思维方式。所以如果我们想给这样的任务一个有用的答案,这是我们应该记住的。

concatMap 是一个很好的类别挂钩,因为它只是 >>= 在列表 monad 中。 IOW,它将 list-Kleisli-arrow A->[B] 提升为 list-to-list 函数 [A]->[B]。那么我们重点来说说怎么写

useChar :: Char -> [Char]
useChar = \c -> if c==wildcard then sub else [c]

作为箭头。实际上,我会将它写成一个函数,但您也可以转而使用 Kleisli 类别。

首先要注意的是您正在复制 c。这通常是通过 fanout operator

完成的
(&&&) :: Arrow (~>)
   => (b~>x) -> (b~>y) -> (b~>(x,y))

所以

import Control.Arrow
sub = "SUBST"
useChar = (==wildcard)&&&(:[])
       >>> \(decision, embd) -> if decision then sub else embd

注意,(:[]) 是列表 monad 的恒等式 Kleisli 箭头;不过我不会利用它。

现在,if 决定适用于布尔值,但布尔值是 ugly。明确地说,布尔值只是两个单位类型的总和类型

 Bool ≈ Either () () ≡ (()+())
(≡≡) :: Eq a => a -> a -> Either () ()
x ≡≡ y
 | x==y       = Right ()
 | otherwise  = Left ()

我们也可以将一些有用的信息编码到这些()选项中的任何一个中,特别是Right选项,它显然只是常量sub.

constRight :: c -> Bool -> Either () c
constRight _ False = Left ()
constRight c True = Right c

useChar = ((==wildcard)>>>constRight sub) &&& (:[])
       >>> \(decision, embd) -> case decision of
              Left () -> embd
              Right theSub -> theSub

或更一般的观点

substRight :: c -> Either a b -> Either a c
substRight _ (Left a) = Left a
substRight c (Right _) = Right c

useChar = ((≡≡wildcard)>>>substRight sub) &&& (:[])
       >>> \(decision, embd) -> case decision of
              Left () -> embd
              Right theSub -> theSub

显然我们可以替换左边以及通用运算符

useChar = ((≡≡wildcard)>>>substRight sub) &&& (:[])
       >>> \(decision, embd) -> substLeft embd decision

现在,如果我们围绕 lambda 调整元组以适应 substLeft 的咖喱形式:

useChar = (:[]) &&& ((≡≡wildcard)>>>substRight sub) >>> uncurry substLeft