以无点样式将字符串中的字符替换为 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
然后我们可以用表达式替换front
和back
:
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
子句。请注意 if
和 else
分支的相反顺序,这是因为 bool
以相反的方式工作。
helper repl pred c = bool [c] repl $ pred c
由于 c
在 helper
的主体中使用了两次,我们可以使用 Applicative
函数实例将 c
应用于多个函数,使用 liftAN
函数。虽然repl
不需要c
,但是我们可以通过const
把它变成一个带c
的函数。因此我们现在使用 liftA3
:
helper repl pred c = liftA3 bool ((:[])) (const repl) pred $ c
现在我们可以很容易地切掉 c
和 pred
:
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))
由于我们需要 wildcard
和 sub
的顺序不同,我们 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
我们的想法是编写一个函数 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
然后我们可以用表达式替换front
和back
:
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
子句。请注意 if
和 else
分支的相反顺序,这是因为 bool
以相反的方式工作。
helper repl pred c = bool [c] repl $ pred c
由于 c
在 helper
的主体中使用了两次,我们可以使用 Applicative
函数实例将 c
应用于多个函数,使用 liftAN
函数。虽然repl
不需要c
,但是我们可以通过const
把它变成一个带c
的函数。因此我们现在使用 liftA3
:
helper repl pred c = liftA3 bool ((:[])) (const repl) pred $ c
现在我们可以很容易地切掉 c
和 pred
:
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))
由于我们需要 wildcard
和 sub
的顺序不同,我们 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