如何在不遍历多次的情况下跟踪字符串的多个属性?

时间:2016-03-09 09:27:26

标签: list haskell functional-programming traversal

我最近遇到了验证字符串的练习题。

  

使用Maybe类型编写一个计算元音数量的函数   在一个字符串和辅音的数量。如果元音的数量超过   辅音的数量,函数返回Nothing。

我为每个计数元音和计数辅音结束了两个函数:

isVowel :: Char -> Bool
isVowel c = c `elem` "aeiou"

countVowels :: String -> Integer
countVowels []     = 0
countVowels (x:xs) =
  if isVowel x
  then countVowels xs + 1
  else countVowels xs

countConsonants :: String -> Integer
countConsonants []     = 0
countConsonants (x:xs) =
  if not $ isVowel x
  then countConsonants xs + 1
  else countConsonants xs

然后只是比较两者的值来得到我的答案。

makeWord :: String -> Maybe String
makeWord [] = Nothing
makeWord s  =
  if countVowels s < countConsonants s
  then Nothing
  else Just s

我的问题是它遍历字符串两次,一次获得元音数量,另一次获得辅音数量。

我想也许我可以通过从字符串长度中减去元音的数量来解决这个问题,但这也需要两次遍历。

所以我尝试制作一个同时跟踪元音和辅音的函数,将结果存储在一个元组中,但由于我不知道添加元组实际上非常难,结果更复杂。

countVowelsAndConsonants :: String -> [(Integer, Integer)]
countVowelsAndConsonants []     = []
countVowelsAndConsonants (x:xs) =
  if isVowel x
    then (1, 0) : countVowelsAndConsonants xs
    else (0, 1) : countVowelsAndConsonants xs

makeWord :: String -> Maybe String
makeWord [] = Nothing
makeWord s  =
  if countVowels s < countConsonants s
    then Nothing
    else Just s
  where counts = let unzipped = unzip (countVowelsAndConsonants s)
                 in (sum $ fst unzipped, sum $ snd unzipped)

而且,说实话,我认为这比我开始时更糟糕。

另外,如果我还要跟踪大写和小写字母怎么办?然后我认为元组方法不会扩展。

在一种命令式语言中,例如javascript,我比较习惯,只需要遍历一次就可以这么简单。

const word        = "somestring"
let numVowels     = 0
let numConsonants = 0

for (var s of word) isVowel(s) ? numVowels++ : numConsonants++

我确信Haskell的方式同样简单,但不幸的是我不熟悉。

保持String多个属性标签的惯用方法是什么,而不必多次遍历它?

4 个答案:

答案 0 :(得分:14)

我首先要定义传统的“指标功能”

indicate :: Num a => Bool -> a
indicate b = if b then 1 else 0

这样

indicate . isVowel :: Char -> Integer

接下来,我将从Control.Arrow

获取两个关键工具包
(&&&) :: (x -> y) -> (x -> z) -> x -> (y, z)
(***) :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)

所以(记住一些字符既不是元音也不是辅音)

(indicate . isVowel) &&& (indicate . isConsonant)
  :: Char -> (Integer, Integer)

然后我从Sum抓住Data.Monoid

(Sum . indicate . isVowel) &&& (Sum . indicate . isConsonant)
  :: Char -> (Sum Integer, Sum Integer)
getSum *** getSum :: (Sum Integer, Sum Integer) -> (Integer, Integer)

现在我部署了foldMap,因为我们正在做某种单一的“迷恋”。

(getSum *** getSum) .
foldMap ((Sum . indicate . isVowel) &&& (Sum . indicate . isConsonant))
  :: String -> (Integer, Integer)

然后我记得我写了一些变成Control.Newtype的代码,我发现以下内容丢失但应该在那里。

instance (Newtype n o, Newtype n' o') => Newtype (n, n') (o, o') where
  pack = pack *** pack
  unpack = unpack *** unpack

现在我只需要写

ala' (Sum *** Sum) foldMap ((indicate . isVowel) &&& (indicate . isConsonant))
  :: String -> (Integer, Integer)

关键小工具是

ala' :: (Newtype n o, Newtype n' o') =>
 (o -> n) -> ((a -> n) -> b -> n') -> (a -> o) -> b -> o'
-- ^-packer    ^-higher-order operator  ^-action-on-elements

封隔器的工作是选择具有正确行为实例的newtype,并确定解包器。它的设计旨在支持以更具体的类型在本地工作,以指示预期的结构。

答案 1 :(得分:6)

foldr与状态一起使用有点简单:

countVCs :: String -> (Int, Int)
countVCs str = foldr k (0, 0) str
   where
     k x (v, c) = if isVowel x then (v + 1, c ) else (v , c + 1 )

另一种方法是Data.List.partition,然后length对该对的两个元素都有countVCs :: String -> (Int, Int) countVCs str = both length (partition isVowel str) where both f (x,y) = (f x, f y)

foldMap

另一种方法是将(Sum Int, Sum Int)countVCs :: String -> (Int, Int) countVCs str = both getSum (foldMap k str) where k x = if isVowel x then (Sum 1, Sum 0) else (Sum 0, Sum 1) both f (x,y) = (f x, f y)

一起使用
.htaccess

答案 2 :(得分:2)

另一种方法是使用像foldl这样的'漂亮折叠'库。那么这个问题基本上减少了写一个表达式。我会把它分开:

import qualified Control.Foldl as L
import Control.Lens

isVowel :: Char -> Bool
isVowel c = c `elem` "aeiou"

countVowels, countConsonants :: L.Fold Char Int
countVowels = L.handles (filtered isVowel) L.length
countConsonants  = L.handles (filtered (not.isVowel)) L.length

lessVowels :: L.Fold Char Bool
lessVowels  = (<) <$> countVowels <*> countConsonants 

makeWord :: String -> Maybe String
makeWord s =  if L.fold lessVowels s then Nothing else Just s

所以我有,说

 >>> makeWord "aaa"
 Just "aaa"
 >>> makeWord "ttt"
 Nothing

使用foldl,您可以同时在同一材料上无限次地运行多次折叠。元素序列可以是纯列表,向量或数组,也可以是有效的流,如管道或管道。褶皱的构成与最终将它折叠的东西无动于衷。

我上面使用handles,这可能有点复杂,因为它使用镜头,但这允许不同的折叠考虑不同种类的元素。所以在这里,一个折叠只是'看'元音,另一个是辅音。用手写这些折叠很简单,省去filtered isVowel镜头。

答案 3 :(得分:1)

坦率地说,函数式编程中最常用的方法是不用担心额外的遍历,而是通过更简单的解决方案提供更高的可读性和可组合性。两次遍历仍为O(n)。除非您的字符串很大或者从不可逆的源(如网络流)读取,否则两次遍历的性能差异可以忽略不计。

此外,您不需要使用递归。 countVowelscountConsonants可以实现为:

countVowels = length . filter isVowel
countConsonants = length . filter (not . isVowel)
相关问题