希望该代码的注释足够好。
-- 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
答案 0 :(得分:3)
您有一个关联列表;使用lookup
获取每个名称,如果查找失败,则生成IncompleteDataError
。 maybe
将每个Nothing
转换为Left
值,并将每个Just value
转换为Right 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
自get :: String -> [(String, String)] -> Either Error String
起,函数的Applicative
实例可确保fillPerson :: [(String, String)] -> Either Error Person
。如果对get
的任何调用返回了Left IncompleteDataError
,则Person <$> ...
的结果也将返回。否则,您将获得一个Right (Person ...)
值。
答案 1 :(得分:2)
您遇到的问题是尝试在单个递归函数中一次完成所有事情,并涉及多个不同的问题。可以这样写,但最好遵循@chepner的回答格式,然后将其分解成碎片。这是对他们答案的补充。 age
的验证。加上导入:
-- readMaybe :: Read a => String -> Maybe a
import Text.Read (readMaybe)
具有帮助功能,可以将Maybe
“失败”(Nothing
)转换为相应的Either
(Left
):
maybeToEither :: a -> Maybe b -> Either a b
maybeToEither x = maybe (Left x) Right
这是一种解决方案,可完成您描述的所有验证:
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)
您可以使用RecordWildCards
来使其更紧凑,尽管不建议这样做,因为它不是明确的,因此它对重命名Person
中的字段很敏感。
fillPerson store = do
firstName <- string "firstName"
lastName <- string "lastName"
age <- int "age"
pure Person{..} -- Implicitly, ‘firstName = firstName’ &c.
where
…
Applicative
运算符在这种类型的事物上更常见,并且在大多数情况下更可取,因为它们避免了不必要的中间名。但是,使用位置参数而不是命名字段的一个警告是,可以混淆具有相同类型(此处为firstName
和lastName
)的字段的顺序。
fillPerson store = Person
<$> string "firstName"
<*> string "lastName"
<*> int "age"
where
…
也可以使此定义无指向性,使用{将store
的参数中的fillPerson
省略,而将其改为string
和int
的参数{1}}(适用于liftA3 Person <$> string "firstName" <*> …
);在这种情况下,我不会选择那种样式,但是尝试自己重写它可能是一个值得的练习。
关于您的问题:
(r ->)
您可以写:
-- I need to store reads value :: [(Int, String)]
-- if the String is not empty, return Left IncorrectDataError value
-- but how to write this?
但是您的代码存在许多问题:
它以"age" -> case reads value of
[(value', "")] -> helper xs p{age=value'} (n + 1)
_ -> Left (IncorrectValueError value)
开头,该字段的字段未定义,如果访问则将引发异常,如果可以保证所有字段均已填充,则可以,但是……
它会跟踪设置的字段的 number ,但不会跟踪哪些字段,因此您可以设置Person
三次,最后返回无效的{ {1}}。
因此,如果您想在一个定义中执行此操作,请按照以下方法进行结构重组:保留递归帮助器,但使每个方程式处理一个条件,对每个字段使用带有firstName
s的累加器,并在找到每个字段时将其从Person
更新为Maybe
。
Nothing
请注意代码的结构非常脆弱:如果我们完全更改Just
,则此定义的许多行都必须更改。这就是为什么最好将问题分解成较小的可组合部分,并将它们放在一起。
但是,这确实是如何将“命令式”循环转换为Haskell的示例:使用累加器为您的“可变”状态编写递归函数,进行递归调用(可能更新累加器)以循环,然后停止递归退出循环。 (实际上,如果您斜视,这本质上是命令式程序到显式控制图的转换。)