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

检查密码是否足够强-Haskell vs过程语言,haskell,functional-programming,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, l

我在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
我想知道我是否能够以更优雅的方式在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中实现一个优雅的解决方案而不存在这个缺点


顺便问一下:Haskell中是否有内置的功能,我至少可以使用它来代替我实现的
功能?

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

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是ok的,则在本例中返回True。然后检查是否已到达密码的末尾,在这种情况下,passwd无效。否则,我们还没有建立所有需要的标准,但是有一个非空的passwd。因此,我们通过将计数器尚未为0的所有函数应用于当前字符、递减成功次数并递归来转换条件列表


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

您的
函数至少有一个稍微高效的版本是
Data.Edison.Seq.ListSeq
中的
inBounds
函数:

inBounds i xs
  | i >= 0    = not (null (drop i xs))
  | otherwise = False

看看这个。这样做的目的是,到目前为止,拥有无数成功检查的列表。 导入数据.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

只是为了好玩,我想我会想出一个与其他答案完全不同的答案。我的想法是定义一个用于跟踪密码统计信息的幺半群,并对该幺半群进行单调检查以确定密码是否有效。首先,一些预备工作:

{-# 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
Statistic
Monoid
实例添加每个bucket中的计数。现在,为了将
Bucket
数据类型连接到更纯净的FP世界,我们将有一个解释器从“EDSL”转换到Haskell函数

interpret :: Bucket -> (Char -> Bool)
interpret Char  = const True
interpret Upper = isUpper
interpret Lower = isLower
interpret Digit = isDigit
interpret Punctuation = isPunctuation
使用此解释器,我们可以将单个字符转换为统计类型的元素。其来源可能看起来有点吓人,但事实上它只不过是串在一起的一组转换函数

statistic :: Char -> Statistic
statistic c b = fromIntegral . fromEnum . interpret b $ c
到目前为止,所有代码中实际上都没有太多的“业务逻辑”——也就是说,每个bucket中我们想要有多少个字符,或者我们想要填充多少bucket。让我们写下这两件事。前者我们将作为一个统计数据来写。检查我们是否已经装满了足够的桶有点棘手,所以它值得一点解释。其思想是迭代所有bucket,如果bucket已“满”,则通过将其与阈值进行比较,留下一个标记。如果我们得到足够的代币,统计数据是有效的

thresholds :: Statistic
thresholds Char = 10
thresholds _    = 2

validStatistic :: Statistic -> Bool
validStatistic f = length [() | b <- universe, f b >= thresholds b] >= 3
在ghci中试用:

*Main> validPassword (cycle "UUxxxxxxx")
True
*Main> validPassword (cycle "!!xxxxxxx")
True
*Main> validPassword "!!xxxxxxx"
False
*Main> validPassword "!!xxxxxxxx"
True

另外,您是否注意到
validPassword
基本上是以与map reduce相同的方式实现的?(
map statistic
是map部分;
scanl()mempty
可以是
foldr()mempty
而是reduce部分;
foldr
一起使用时,任何validStatistic
都将成为
validStatistic
,并且是后处理部分)如果您确实拥有100MB密码,则可以轻松查看如何并行计算
validPassword
。-)

您可以使用幺半群或单子群:

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
data Pass a=NotPass | TryPass Int | Pass a派生显示
实例Monad Pass在哪里
返回=通过
失败=不通过
NotPass>>==NotPass
通过k>>=f=f k
(TryPass a k)>>=f=的情况f k
NotPass->if(a<0)NotPass else TryPass(a-1)
胰蛋白酶b->胰蛋白酶b
通行证a
_是否有效::String->Bool
_有效密码=toBool$do吗
胰蛋白酶3
托帕斯在上
托帕斯岛
托帕斯是一个特殊的人物
托帕斯数字
哪里
toPass bf=除非(bf bassword)未通过
toBool NotPass=错误
toBool=真

我很好奇;“无限密码”是我不知道的一个术语吗?@AndrewBarber-它不是可以在实践中使用的东西。这只是“更糟的情况”的假设。您也可以假定密码为100MB。如果密码足够强,过程算法将不会在整个100MB上迭代,而Haskell中的算法可能会。啊,好的。我明白你的意思!总是考虑什么…这是一个有趣的解决方案。我对此有一些评论:1。难道不能用
foldl
代替accum吗?2.isValid“uxxx”由于函数accum中的非穷举模式而引发错误。由于您
检查而不是检查它们,因此您的解决方案将“uxxx!”标记为强密码。但是,它应该至少包含到个特殊字符和至少两个大写字母才能成为强字符。@darwish更新的解决方案和问题的答案为“否”-fold\mapAccum将在此处强制进行完全计算。@darwish
foldl
在无限输入时始终无限递归。
foldr
(比较两个函数的定义,试着写出两个递归步骤,看看它们在这方面有什么不同),但递归
*Main> validPassword (cycle "UUxxxxxxx")
True
*Main> validPassword (cycle "!!xxxxxxx")
True
*Main> validPassword "!!xxxxxxx"
False
*Main> validPassword "!!xxxxxxxx"
True
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