我正在尝试使用Haskeline来编写一个程序,该程序向用户询问一系列问题,每个问题都可以选择[括号]中的默认值,并读取他们的答案。我希望用户能够
我已经能够让积分1-3工作,但我无法让第4点工作:按Ctrl-D(或输入“退出”)只会调出下一个提示而不是让程序退出提问。看看我的程序(请看下面)我理解为什么会发生这种情况,但我无法弄清楚如何解决这个问题,以便Ctrl-D(或“退出”)实际上使问题停止。如何修复程序以实现此目的?
我确实看到this question似乎在问类似的东西,但我从那里得不到多少;我甚至不确定他们是在问我一样的问题。
作为第二个问题:我当前的程序有很多case
语句可以打开Maybe
值。特别是,我目前检查两个或三个级别Nothing
,以便在用户按下Ctrl-D时我可以正确返回Nothing
。我有一种感觉,这可以使用(类似于)monadic >>=
运算符来简化,但在这种情况下我无法弄清楚如何执行此操作。我的预感是对的吗?有没有办法取消所有寻找Nothing
的模式匹配?
另外:请告诉我其他任何可以改善我的代码的方法。我对此很陌生,所以很可能我在这里遗漏了许多明显的东西。
我的程序询问用户水果篮的成分。与水果篮相关的信息包括水果篮所有者的名字和篮子中不同种类水果的名称。为了能够要求后者,我首先要求在篮子里提供不同种类水果的号,然后询问每种水果的名称。我们从默认的水果篮开始,然后根据用户告诉我们的信息修改其信息。
module Main where
import System.Console.Haskeline
type PersonName = String
type FruitName = String
data FruitBasket = FruitBasket { ownerName :: PersonName,
fruitCount :: Int,
fruitNames :: [FruitName]
} deriving Show
defaultBasket = FruitBasket "Mary" 2 ["Apple", "Peach"]
main :: IO ()
main = do
basket <- getBasketData defaultBasket
putStrLn $ "Got: " ++ show(basket)
-- Prompt the user for information about a fruit basket, and
-- return a FruitBasket instance containing this information. The
-- first argument is an instance of FruitBasket from which we get
-- the default values for the various prompts. The return value
-- has a Maybe type because the user may abort the questioning, in
-- which case we get nothing from them.
getBasketData :: FruitBasket -> IO (Maybe FruitBasket)
getBasketData basket = runInputT defaultSettings $ withInterrupt $ getData basket
where
getData :: FruitBasket -> InputT IO (Maybe FruitBasket)
getData initialBasket = handleInterrupt f $ do
outputStrLn banner
input <- getInputLine $ "Who owns this basket? [" ++ defaultOwner ++ "] : "
basket <- case input of
Nothing -> return Nothing -- User pressed Ctrl-D with the input being empty
Just "" -> return (Just initialBasket) -- User pressed Enter with the input being empty
Just "quit" -> return Nothing -- User typed in "quit" and pressed Enter
Just newOwner -> return (Just initialBasket{ownerName = newOwner})
input <- getInputLine $ "Number of kinds of fruit in the basket? [" ++ show defaultCount ++ "] : "
basket' <- case input of
Nothing -> return Nothing
Just "" -> return basket
Just "quit" -> return Nothing
Just count -> return $ updateFruitCount basket (read count)
where updateFruitCount Nothing _ = Nothing
updateFruitCount (Just realBasket) newCount = Just $ realBasket{fruitCount = newCount}
let defaultFruitNames = pruneOrPadNames basket'
newNames <- getFruitNames defaultFruitNames 1
case newNames of
Nothing -> return (Just defaultBasket)
Just newSetOfNames -> return $ updateFruitNames basket' newSetOfNames
where updateFruitNames Nothing _ = Nothing
updateFruitNames (Just realBasket) realNewNames = Just $ realBasket{fruitNames = realNewNames}
where f = (outputStrLn "Press Ctrl-D or enter \"quit\" to quit." >> getData initialBasket)
defaultOwner = ownerName initialBasket
defaultCount = fruitCount initialBasket
banner :: String
banner = "Please enter details of the fruit basket below. At each prompt you can do one of the following:\n\
\\t (a) Press Enter to submit the [default] value;\n\
\\t (b) Type in a string, edit it if needed, and then press Enter to submit this value;\n\
\\t (c) Press Ctrl-C to reset all values to the defaults and start over;\n\
\\t (d) Press Ctrl-D or enter \"quit\" to quit; all the values you submitted will be lost."
pruneOrPadNames :: Maybe FruitBasket -> Maybe [String]
pruneOrPadNames Nothing = Nothing
pruneOrPadNames (Just basket) = Just $ pruneOrPad (fruitNames basket) (fruitCount basket)
-- When requiredLength is not larger than (length inputList),
-- (pruneOrPad inputList requiredLength) is the prefix of
-- inputList of length requiredLength. Otherwise, it is inputList
-- padded with as many empty strings as required to make the total
-- length equal to requiredLength.
pruneOrPad :: [String] -> Int -> [String]
pruneOrPad inputList requiredLength
| requiredLength <= inputLength = take requiredLength inputList
| otherwise = inputList ++ (replicate difference "")
where inputLength = length inputList
difference = requiredLength - inputLength
getFruitNames Nothing _ = return Nothing
getFruitNames (Just []) _ = return $ Just [""]
getFruitNames (Just (name:names)) count = do
input <- getInputLine $ "Name of fruit " ++ (show count) ++ " [" ++ name ++ "] : "
newNames <- case input of
Nothing -> return Nothing
Just "" -> do -- Keep the default name for this fruit ...
newNames' <- getFruitNames (Just names) (count + 1)
case newNames' of
Nothing -> return Nothing
-- ... unless the user chose to quit
-- while entering a name
Just [""] -> return $ Just [name]
-- At this point names = [] so it is
-- already time to stop asking for
-- more names.
Just furtherNames -> return $ Just (name : furtherNames)
Just "quit" -> return Nothing
Just name' -> do
newNames' <- getFruitNames (Just names) (count + 1)
case newNames' of
Nothing -> return Nothing
Just [""] -> return $ Just [name']
Just furtherNames -> return $ Just (name' : furtherNames)
return newNames
答案 0 :(得分:2)
在一些建议here on the haskell-beginners mailing list的帮助下,我设法解决了我的问题,完全按Ctrl-D问题和我自己满意的保理问题(截至目前!)。我在这里发布答案,希望能帮助其他人摆脱困境。
首先,Ctrl-D的问题:问题是我丢掉了Maybe monad提供的控制逻辑,只是使用了来自的值 monad,通过引用包含这些值的各种变量名称。我在这里执行此操作的第一个地方是getBasketData
函数:
basket <- case input of ...
input <- getInputLine ...
basket' <- case input of
Nothing -> return Nothing
Just "" -> return basket
请注意,在计算basket'
时,我
basket
可能是Nothing
和basket
封装的值,参考(并在需要时进行模式匹配)变量basket
,该变量仍在basket'
的表达式范围内1}}。这是Ctrl-D丢失的地方。在这里,相比之下,getBasketData
的代码是不让Nothing
滑过空白(我将basket
变量重命名为maybeBasket
},因为它们实际上是Maybe FruitBasket
)的实例:
getBasketData basket = runInputT defaultSettings $ withInterrupt $ getData basket
where
getData :: FruitBasket -> InputT IO (Maybe FruitBasket)
getData initialBasket = handleInterrupt f $ do
outputStrLn banner
input <- getInputLine $ "Who owns this basket? [" ++ defaultOwner ++ "] : "
maybeBasket <- case input of
Nothing -> return $ Nothing -- User pressed Ctrl-D with the input being empty
Just "" -> return $ Just initialBasket -- User pressed Enter with the input being empty
Just "quit" -> return $ Nothing -- User typed in "quit" and pressed Enter
Just newOwner -> return $ Just initialBasket{ownerName = newOwner}
maybeBasket' <- case maybeBasket of
Nothing -> return $ Nothing
Just realBasket -> do input <- getInputLine $ "Number of kinds of fruit in the basket? [" ++ show defaultCount ++ "] : "
case input of
Nothing -> return $ Nothing
Just "" -> return $ maybeBasket
Just "quit" -> return $ Nothing
Just count -> return $ Just $ realBasket{fruitCount = (read count)}
maybeBasket'' <- case maybeBasket' of
Nothing -> return $ Nothing
Just realBasket -> do let defaultFruitNames = pruneOrPad (fruitNames realBasket) (fruitCount realBasket)
newNames <- getFruitNames defaultFruitNames 1
case newNames of
Nothing -> return $ Nothing
Just newSetOfNames -> return $ Just $ realBasket{fruitNames = newSetOfNames}
return maybeBasket''
where f = (outputStrLn interruptMessage >> getData initialBasket)
defaultOwner = ownerName initialBasket
defaultCount = fruitCount initialBasket
因此,例如,我们尝试进行任何实际计算以获得maybeBasket'
--- 包括呈现不同种类水果数量的提示---仅当{ {1}}不是maybeBasket
。
这解决了Ctrl-D问题:如果用户按下Ctrl-D以响应任何问题,程序现在停止提问并返回Nothing
。
现在进入保理。这是来自邮件列表答案的建议有帮助的地方:我开始将大Nothing
函数分成三部分,一部分用于getData
运算符的每次“大”使用,并将这些部分放入单独的功能。这为我清理了很多逻辑(事实上,这也是我找到Ctrl-D问题的修复方法)。从这开始,我一直在重述各个部分,直到我得到以下版本,看起来对我来说足够好。请注意<-
函数的小而干净!
getBasketData
这个故事的寓意似乎是:“当困惑时,要把事情搞得一团糟。”
答案 1 :(得分:1)
我认为你的预感就在这里。通过案例完成的大部分模式匹配可以使用Maybe Monad进行更换。 而不是
basket <- case input of
Nothing -> return Nothing -- User pressed Ctrl-D with the input being empty
Just "" -> return (Just initialBasket) -- User pressed Enter with the input being empty
Just "quit" -> return Nothing -- User typed in "quit" and pressed Enter
Just newOwner -> return (Just initialBasket{ownerName = newOwner})
你可以写点像
let basket' = do
i <- input
guard $ i /= "quit"
b <- basket
return $ if (null i) then b else b{fruitCount = read i}
你甚至可以介绍一些帮手,比如
guardInput :: Maybe String -> (String -> Maybe a) -> Maybe a
guardInput input λ = input >>= \i -> ((guard $ i /= "quit") >> λ i)
-- | Custom ternary operator .)
True ? (a, _) = a
False ? (_, b) = b
写
let basket = guardInput input $
\i -> return $ (null i) ? (initialBasket, initialBasket{ownerName = i})
抱歉 - 我知道这并没有用Ctrl + D来解决你的问题,但是我还没有想到这个问题(还有)。