检查密码是否足够强 - Haskell与过程语言

时间:2014-01-12 09:44:00

标签: haskell functional-programming

我在Python中实现了一个函数,用于检查密码是否足够强大。如果密码通过5次检查中的3次,则密码足够强。这是Python函数:

def is_valid(password):
    checks = {
        lambda x : True : 10,
        lambda x : x.isupper() : 2,
        lambda x : x.islower() : 2,
        lambda x : x.isdigit() : 2,
        lambda x : x in frozenset("~!@#$%^&*()-=_+[]{}<>?/\\`") : 2,
    }

    for c in password:
        for func in list(checks):
            if func(c):
                checks[func] -= 1
                if checks[func] == 0:
                    del checks[func]

        if len(checks) <= 2:
            return True

    return False

如果足够强大,此功能可以在无限密码上运行。如果不是,则该功能将挂起:

>>> is_valid(itertools.cycle("!!xxxxxxxx"))
True
>>> is_valid(itertools.cycle("UUxxxxxxxx"))
True

我想知道如果我能以更优雅的方式在Haskell中实现相同的功能,那么我想出了这个解决方案:

isValid :: String -> Bool
isValid password =
  let
    checks = atLeast 10 password:map containsChars [isUpper, isLower, isSpecialChar, isDigit]
  in atLeast 3 $ filter (==True) checks
  where
    containsChars predicate = length (take 2 $ filter predicate password) == 2
    isSpecialChar c = isPunctuation c || isSymbol c
    atLeast n seq = length (take n seq) == n

这个解决方案似乎更优雅,但与程序解决方案相比有一个缺点。如果密码是无限的并且没有足够的大写字符,则即使该函数通过其他条件,该函数也会挂起:

*Main Data.Char> isValid (cycle "UUxxxxxxxx")
True
*Main Data.Char> isValid (cycle "!!xxxxxxxx")
-- hangs

有没有办法在Haskell中实现一个没有这个缺点的优雅灵魂?

BTW:我可以使用Haskell内置的内容而不是我实现的atLeast函数吗?

5 个答案:

答案 0 :(得分:4)

你是对的:如果已知密码正常,它仍会尝试建立其他条件。因此我会采用不同的方式:

check passwd = go criteria passwd
    where 
      criteria = [(10, const True), (2, isUpper), (2, isLower), (2, isDigit), (2, isSpecial)]
      -- password is ok if at least 3 criteria have counted down to 0
      ok = (>=3) . length . filter (==0) . map fst
      go crit pwd
         | ok crit = True
         | null pwd = False
         | otherwise = go (map (trans (head pwd)) crit) (tail pwd)
      trans ch (0, p) = (0, p)
      trans ch (n, p) = if p c then (n-1, p) else (n, p)

如果条件列表指示passwd正常并且在这种情况下返回True,则想法检查每个字符。然后检查是否已到达密码的结尾,在这种情况下,passwd无效。否则,我们还没有建立所有需要的标准,但是有一个非空的密码。因此,我们通过将计数器不为0的所有函数应用于当前字符,向下计算成功并递归来转换条件列表。

请注意(10, const True)对条件进行编码:“Passwd长度至少为10个字符。”

答案 1 :(得分:1)

来自atLeast的{​​{1}}函数的inBounds函数稍微高效一点:

Data.Edison.Seq.ListSeq

答案 2 :(得分:1)

检查一下。我们的想法是拥有无限的成功检查清单。 import Data.Char

isValid password = let
    checks                      = [isUpper, isLower, isSpecialChar, isDigit]
    isSpecialChar c             = isPunctuation c || isSymbol c
    toInt b                     = if b then 1 else 0
    check c                     = map toInt $ map ($c) checks
    tests                       = zip [1..] $ map check password
    accum passed ((len,t):ts)   = (len,result):accum result ts where result = zipWith (+) passed t
    accum _ []                  = []
    valid (len,test)            = toInt ( len > 10 )  + (length $ filter (>=2) test ) >= 3
    in any valid $ accum (repeat 0) tests

答案 3 :(得分:0)

只是为了好玩,我想我会把一个非常不同的答案扔给其他人。我的想法是定义一个跟踪密码统计信息的monoid,以及对密码有效的monoid进行单调检查。首先,一些预赛:

{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving #-}
import Data.Char
import Data.Monoid
import Data.Universe
deriving instance Num a => Num (Sum a) -- this should really be in base

我们跟踪的统计数据是我们看到的与您的五个条件相匹配的字符数。从技术上讲,我们实际上可以在数据结构中跟踪这一点,将函数映射到数字,这是最纯粹的FP方式,但如果天真地完成,它的效率会有点低。如果我们有一个明确的数据结构作为我们地图中的关键字,那么跟踪起来要容易得多。

data Bucket = Char | Upper | Lower | Digit | Punctuation
    deriving (Eq, Ord, Show, Read, Bounded, Enum)
instance Universe Bucket

type Natural   = Integer -- lol, let's just pretend, okay?
type Statistic = Bucket -> Sum Natural

Monoid的{​​{1}}实例会在每个存储桶中添加计数。现在,要将Statistic数据类型连接到更纯净的FP世界,我们将有一个解释器来自我们的&#34; EDSL&#34;进入Haskell函数。

Bucket

使用此解释器,我们可以将单个字符转换为统计类型的元素。这个来源可能看起来有些令人生畏,但实际上它只不过是一堆串联的转换功能。

interpret :: Bucket -> (Char -> Bool)
interpret Char  = const True
interpret Upper = isUpper
interpret Lower = isLower
interpret Digit = isDigit
interpret Punctuation = isPunctuation

到目前为止,这些代码中没有一个实际上有太多&#34;业务逻辑&#34;在其中 - 也就是说,我们想要拥有的每个桶中确切的字符数或者我们想要填充多少个桶。所以,让我们写下这两件事。前者我们只是写一个统计数据。检查我们是否填充了足够的桶是有点棘手的,所以它值得一些解释。我们的想法是遍历所有存储桶,如果存储桶已经满了就会留下令牌。通过将其与阈值进行比较。如果我们获得足够的令牌,则统计数据有效。

statistic :: Char -> Statistic
statistic c b = fromIntegral . fromEnum . interpret b $ c

thresholds :: Statistic thresholds Char = 10 thresholds _ = 2 validStatistic :: Statistic -> Bool validStatistic f = length [() | b <- universe, f b >= thresholds b] >= 3 持有的关键属性未在类型系统中反映出来。有很多不同的方式陈述这个属性;一种方法是观察validStatistic无论validStatistic f < validStatistic (f <> g)f是什么(仍然假装没有负数)。在英语中,这对我们来说意味着如果密码的某些子字符串有效,那么整个密码也是有效的。

冷却。现在我们有了检查密码是否有效所需的所有部分。我们要做的是将密码的每个字符变成统计信息;我们所有的统计数据集合在一起;并检查统计数据是否曾经从无效到有效超过阈值(注意,如果确实如此,由于上面的关键属性,它将永远不会交叉回无效)。

g

在ghci中尝试:

validPassword :: String -> Bool
validPassword = any validStatistic . scanl (<>) mempty . map statistic

P.S。您是否注意到*Main> validPassword (cycle "UUxxxxxxx") True *Main> validPassword (cycle "!!xxxxxxx") True *Main> validPassword "!!xxxxxxx" False *Main> validPassword "!!xxxxxxxx" True 基本上以与map-reduce相同的方式实现? (validPassword是地图部分; map statistic可以是scanl (<>) mempty而不是缩小部分; foldr (<>) memptyany validStatistic一起使用时会变为validStatistic并且是后处理部分)如果您确实拥有100MB密码,可以很容易地看到如何并行化foldr的计算。 ; - )

答案 4 :(得分:0)

你可以使用Monoids或Monad:

data Pass a = NotPass | TryPass Int | Pass a deriving Show

instance Monad Pass where
   return = Pass
   fail = NotPass
   NotPass     >>= _       = NotPass
   Pass k      >>= f       = f k
   (TryPass a k) >>= f = case f k of
              NotPass   -> if (a < 0) NotPass else TryPass (a-1)
              TryPass b -> TryPass b
              Pass  _   -> TryPass a

is_valid :: String -> Bool
is_valid password = toBool $ do
    TryPass 3
    toPass isUpper
    toPass isLower
    toPass isSpecialChar
    toPass isDigit
  where
   toPass bf = unless (bf bassword) NotPass
   toBool NotPass = false
   toBool _       = true