简体   繁体   English

外壳内有其他图案匹配

[英]Additional pattern matching inside case

Hopefully, the code is commented well enough. 希望该代码的注释足够好。

-- I have 2 data types:

data Person = Person { firstName :: String, lastName :: String, age :: Int }
  deriving (Show)

data Error = IncompleteDataError | IncorrectDataError String
  deriving (Show)

-- This function should take a list a pairs like:
-- fillPerson [("firstName","John"), ("lastName","Smith"), ("dbdf", "dff"), ("age","30"), ("age", "40")]
-- And fill the record with values of the fields with corresponding names.
-- It ignores the redundant fields.
-- If there are less then 3 meaningful fields, it should throw an error IncompleteDataError
-- If the field age doesn't have a number, if should return IncorrectDataError str, where str — is the value of age.

fillPerson :: [(String, String)] -> Either Error Person

fillPerson [] = Left IncompleteDataError
fillPerson (x:xs) = let

  -- Int stores number of fields
  helper :: [(String, String)] -> Person -> Int -> Either Error Person
  helper _ p 3 = Right p
  helper [] _ _ = Left IncompleteDataError
  helper ((key, value):xs) p n = case key of
    "firstName" -> helper xs p{firstName=value} (n + 1)
    "lastName" -> helper xs p{lastName=value} (n + 1)
    -- how to return IncorrectDataError str here?
    -- I need to store reads value :: [(Int, String)]
    -- if the String is not empty, return Left IncorrectDataError value
    -- but how to write this?
    "age" -> helper xs p{age=read value::Int} (n + 1)
    _ -> helper xs p n
  in
    helper (x:xs) Person{} 0

You have an association list; 您有一个关联列表; use lookup to get each name, or produce an IncompleteDataError if the lookup fails. 使用lookup获取每个名称,如果查找失败,则产生IncompleteDataError maybe converts each Nothing to a Left value and each Just value to Right value . maybe将每个Nothing转换为Left值,并将每个Just valueRight value

-- lookup :: Eq a => a -> [(a,b)] -> Maybe b
-- maybe :: b -> (a -> b) -> Maybe a -> b

verifyInt :: String -> Either Error Int
verifyInt x = ... -- E.g. verify "3" == Right 3
                  --      verify "foo" == Left IncorrectDataError


fillPerson kv = Person
                <$> (get "firstName" kv)
                <*> (get "lastName" kv)
                <*> (get "age" kv >>= verifyInt)
    where get key kv = maybe (Left IncompleteDataError) Right $ lookup key kv

Since get :: String -> [(String, String)] -> Either Error String , the Applicative instance for functions ensures that fillPerson :: [(String, String)] -> Either Error Person . get :: String -> [(String, String)] -> Either Error String fillPerson :: [(String, String)] -> Either Error Person get :: String -> [(String, String)] -> Either Error String ,函数的Applicative实例确保fillPerson :: [(String, String)] -> Either Error Person If any call to get returns Left IncompleteDataError , the result of Person <$> ... will do so as well; 如果对get任何调用返回Left IncompleteDataError ,则Person <$> ...的结果也将这样做; otherwise, you'll get a Right (Person ...) value. 否则,您将获得Right (Person ...)值。

The problem that you have is trying to do all the things at once in a single recursive function, interleaving several different concerns. 您遇到的问题是尝试在单个递归函数中一次完成所有事情,并涉及多个不同的问题。 It's possible to write that way, but better to follow the format of @chepner's answer and break things down into pieces. 可以这样写,但是最好遵循@chepner的回答格式并将其分解成碎片。 This is a supplement to their answer re. 这是对他们答案的补充。 the verification of age . age验证。 With the addition of an import: 加上导入:

-- readMaybe :: Read a => String -> Maybe a
import Text.Read (readMaybe)

And a helper function to turn Maybe “failures” ( Nothing ) into the corresponding Either ( Left ): 以及一个帮助程序功能,可以将Maybe “失败”( Nothing )转换为相应的EitherLeft ):

maybeToEither :: a -> Maybe b -> Either a b
maybeToEither x = maybe (Left x) Right

Here is a solution that does all the verification you describe: 这是一个解决方案,可完成您描述的所有验证:

fillPerson store = do  -- Either monad

  -- May fail with ‘IncompleteDataError’
  f <- string "firstName"
  l <- string "lastName"

  -- May fail with ‘IncompleteDataError’ *or* ‘IncorrectDataError’
  a <- int "age"

  pure Person
    { firstName = f
    , lastName = l
    , age = a
    }

  where

    string :: String -> Either Error String
    string key = maybeToEither IncompleteDataError (lookup key store)

    int :: String -> Either Error Int
    int key = do
      value <- string key  -- Reuse error handling from ‘string’
      maybeToEither (IncorrectDataError value) (readMaybe value)

You can make this more compact using RecordWildCards , although this is less advisable because it's not explicit, so it's sensitive to renaming of fields in Person . 您可以使用RecordWildCards使其更紧凑,尽管不建议这样做,因为它不是显式的,因此对Person中字段的重命名很敏感。

fillPerson store = do
  firstName <- string "firstName"
  lastName <- string "lastName"
  age <- int "age"
  pure Person{..}  -- Implicitly, ‘firstName = firstName’ &c.
  where
    …

Applicative operators are more common for this type of thing, and preferable in most cases as they avoid unnecessary intermediate names. Applicative运算符在这种类型的事物上更为常见,并且在大多数情况下更可取,因为它们避免了不必要的中间名称。 However, one caveat of using positional arguments rather than named fields is that it's possible to mix up the order of fields that have the same type (here, firstName and lastName ). 但是,使用位置参数而不是命名字段的一个警告是,可以混淆具有相同类型(此处为firstNamelastName )的字段的顺序。

fillPerson store = Person
  <$> string "firstName"
  <*> string "lastName"
  <*> int "age"
  where
    …

It's also possible to make this definition point-free, omitting store from the parameters of fillPerson and making it instead a parameter of string and int , using liftA3 Person <$> string "firstName" <*> … (the (r ->) applicative); 也可以使这个定义没有意义,使用liftA3 Person <$> string "firstName" <*> …(r ->) ,从fillPerson的参数中fillPerson store ,并使其改为stringint的参数。适用); in this particular case I wouldn't choose that style, but it may be a worthy exercise to try to rewrite it yourself. 在这种情况下,我不会选择那种样式,但是尝试自己重写它可能是一个值得的练习。


As to your question: 关于你的问题:

    -- I need to store reads value :: [(Int, String)]
    -- if the String is not empty, return Left IncorrectDataError value
    -- but how to write this?

You can write: 你可以写:

"age" -> case reads value of
  [(value', "")] -> helper xs p{age=value'} (n + 1)
  _ -> Left (IncorrectValueError value)

However there are a number of problems with your code: 但是,您的代码有很多问题:

  • It starts with a Person whose fields are undefined, and will raise exceptions if accessed, which would be fine if you guaranteed that they were all filled in, but… 它以其字段未定义的Person开头,如果访问则将引发异常,如果您保证所有字段均已填充,那会很好,但是…

  • It tracks the number of fields set but not which fields , so you can set firstName three times and end up returning an invalid Person . 它跟踪设置的字段 ,但不跟踪哪个字段 ,因此您可以设置三次firstName并最终返回无效的Person

So if you want to do this in a single definition, here's how I would restructure it—keep the recursive helper, but make each equation handle one condition, using an accumulator with Maybe s for each of the fields, updating them from Nothing to Just as you find each field. 因此,如果您想在一个定义中执行此操作,请按照以下方法进行结构调整:保留递归帮助器,但使每个方程式处理一个条件,对每个字段使用一个带有Maybe s的累加器,将其从Nothing更新为Just当您找到每个字段时。

fillPerson' :: [(String, String)] -> Either Error Person
fillPerson' = fillFields (Nothing, Nothing, Nothing)
  where

    fillFields
      -- Accumulator of firstName, lastName, and age.
      :: (Maybe String, Maybe String, Maybe Int)
      -- Remaining input keys to check.
      -> [(String, String)]
      -- Final result.
      -> Either Error Person

    -- Set firstName if not set.
    fillFields (Nothing, ml, ma) (("firstName", f) : kvs)
      = fillFields (Just f, ml, ma) kvs

    -- Set lastName if not set.
    fillFields (mf, Nothing, ma) (("lastName", l) : kvs)
      = fillFields (mf, Just l, ma) kvs

    -- Set age if not set, failing immediately if not a valid number.
    fillFields (mf, ml, Nothing) (("age", a) : kvs)
      | all (`elem` ['0'..'9']) a
      = fillFields (mf, ml, Just (read a)) kvs
      | otherwise
      = Left (IncorrectDataError a)

    -- Ignore redundant firstName.
    fillFields acc@(Just{}, ml, ma) (("firstName", _) : kvs)
      = fillFields acc kvs

    -- Ignore redundant lastName.
    fillFields acc@(mf, Just{}, ma) (("lastName", _) : kvs)
      = fillFields acc kvs

    -- Ignore redundant age.
    fillFields acc@(mf, ml, Just{}) (("age", _) : kvs)
      = fillFields acc kvs

    -- Ignore extra fields.
    fillFields acc (_ : kvs)
      = fillFields acc kvs

    -- If all fields are present anywhere in the input,
    -- we can finish early and successfully.
    fillFields (Just f, Just l, Just a) _
      = Right Person
        { firstName = f
        , lastName = l
        , age = a
        }

    -- If any field is missing at the end, fail.
    fillFields __ []
      = Left IncompleteDataError

Note how the structure of the code is very brittle: if we change Person at all, many lines of this definition will have to change. 请注意,代码的结构非常脆弱:如果我们完全更改Person ,则此定义的许多行都必须更改。 That's why it's better to break the problem down into smaller composable parts and put them together. 这就是为什么最好将问题分解成较小的可组合部分,并将它们放在一起。

This does, however, serve as an example of how to translate an “imperative” loop into Haskell: write a recursive function with an accumulator for your “mutable” state, make a recursive call (possibly updating the accumulator) to loop, and stop the recursion to exit the loop. 但是,这确实是如何将“命令式”循环转换为Haskell的示例:使用累加器为您的“可变”状态编写递归函数,进行递归调用(可能会更新累加器)以循环并停止递归退出循环。 (In fact, if you squint, this is essentially a translation of an imperative program into an explicit control graph.) (实际上,如果您斜视,这实质上是命令式程序到显式控制图的转换。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM