15

分享两道有意思的 Haskell 习题(可以看看我的中文注解),其中我写的答案很垃圾,所以我就不放上我的答案了。我这里用的是官方答案,写得很好,让我得以自我反思和自我批评、自我更正:

------------------------------------------------------------------------------
-- Ex 8: Improved phone number validation. Implement the function
-- normalizePhone that, given a String:
--
-- * removes all spaces from the string
-- * checks that there are at most 10 remaining characters
-- * checks that all remaining characters are digits, and logs an
--   error for every nonvalid character
-- * returns the string, stripped of whitespace, if no errors
--
-- Examples:
--  normalizePhone "123 456 78" ==> Ok "12345678"
--  normalizePhone "123 4x6 78"
--    ==> Errors ["Invalid character: x"]
--  normalizePhone "123 4x6 7y"
--    ==> Errors ["Invalid character: x","Invalid character: y"]
--  normalizePhone "123 4x6 7y 999"
--    ==> Errors ["Too long","Invalid character: x","Invalid character: y"]
--  normalizePhone "123 456 78 999"
--    ==> Errors ["Too long"]






-- (<$>) 和 (<*>) 函数用于一串的确定数目变量构造
-- traverse 用于一串不确定数目变量(储存在list中)构造
-- (*>) , (<*) 用于有一些虽然输出结果,但是我们只用于判断是否 valid,而不用它的结果的情况(如本题)
-- 另外,如下题所示,有时候需要配合上 pattern matching 使用。
  -- 也就是,先 tokenize,再自底而上地 parse
    -- 兼用 
      -- (<$>) 和 (<*>)(与,全利用)
      -- (<*), (*>)(与,只利用左边的/右边的)
      -- 和 (<|>)(或,只利用左边的**或者**右边的)
    -- 三种
  -- 并配合上 pattern matching,实现对不同情况,不同 parse 地精确掌握
normalizePhone :: String -> Validation String
normalizePhone xs = checkLength trimmedStr *> traverse validateChar trimmedStr
  where
    trimmedStr = filter (not . isSpace) xs
    checkLength :: String -> Validation String
    checkLength xs = check (length xs <= 10) "Too long" xs
    validateChar :: Char -> Validation Char
    validateChar c = check (isDigit c) ("Invalid character: " ++ [c]) c

------------------------------------------------------------------------------
-- Ex 9: Parsing expressions. The Expression type describes an
-- arithmetic expression that has an operator (+ or -) and two
-- arguments that can be either numbers or single-letter variables.
-- The operator and the arguments are always separated by spaces. Here
-- are some examples of expressions like this: 1 + 2, y + 7, z - w
--
-- Implement the function parseExpression that uses the Validation
-- applicative to convert strings like "y + 7" to Expression values
-- like Plus (Variable 'y') (Number 7).
--
-- The parser should produce the following errors:
--  * For operators other than + or -: "Unknown operator: %"
--  * For variables that aren't single letters: "Invalid variable: xy"
--  * For arguments that aren't numbers: "Invalid number: 1x" --
--  * For expressions that don't consist of three words:
--    "Invalid expression: 1 + 2 +"
--    "Invalid expression: 1 -"
--
-- Hint: The functions `words` and `isAlpha`
--
-- Hint: If you have problems with the ordering of errors, remember
-- that Validation collects errors left-to-right!
--
-- Examples:
--  parseExpression "1 + 2" ==> Ok (Plus (Number 1) (Number 2))
--  parseExpression "z - A" ==> Ok (Minus (Variable 'z') (Variable 'A'))
--  parseExpression "1 * 2" ==> Errors ["Unknown operator: *"]
--  parseExpression "1 + 2x"
--    ==> Errors ["Invalid number: 2x","Invalid variable: 2x"]
--  parseExpression ". % 2x"
--    ==> Errors ["Unknown operator: %",
--                "Invalid number: .","Invalid variable: .",
--                "Invalid number: 2x","Invalid variable: 2x"]



-- 如本题所示,有时候需要配合上 pattern matching 使用。
  -- 也就是,先 tokenize,再自底而上地 parse
    -- 兼用 
      -- (<$>) 和 (<*>)(与,全利用)
      -- (<*), (*>)(与,只利用左边的/右边的)
      -- 和 (<|>)(或,只利用左边的**或者**右边的)
    -- 三种
  -- 并配合上 pattern matching,实现对不同情况,不同 parse 地精确掌握

data Arg = Number Int | Variable Char
  deriving (Show, Eq)

data Expression = Plus Arg Arg | Minus Arg Arg
  deriving (Show, Eq)

parseExpression :: String -> Validation Expression
parseExpression ss = parseTokens (words ss)
  where
    parseTokens [left, op, right] = parseOperator op <*> parseOperand left <*> parseOperand right
    parseTokens s = invalid $ "Invalid expression: " ++ ss

parseVariable :: String -> Validation Arg
parseVariable [c]
  | isAlpha c = pure (Variable c)
parseVariable s = invalid $ "Invalid variable: " ++ s

parseNumber :: String -> Validation Arg
parseNumber s
  | all isDigit s = pure (Number (read s))
parseNumber s = invalid $ "Invalid number: " ++ s

parseOperand :: String -> Validation Arg
parseOperand s = parseNumber s <|> parseVariable s

parseOperator :: String -> Validation (Arg -> Arg -> Expression)
parseOperator "+" = pure Plus
parseOperator "-" = pure Minus
parseOperator s = invalid $ "Unknown operator: " ++ s