让Haskeline尽早退出

时间:2015-03-21 23:57:45

标签: haskell

我正在尝试使用Haskeline来编写一个程序,该程序向用户询问一系列问题,每个问题都可以选择[括号]中的默认值,并读取他们的答案。我希望用户能够

  1. 按Enter键提交[default]值;
  2. 键入字符串,根据需要进行编辑,然后按Enter键提交此值;
  3. 按Ctrl-C将所有值重置为默认值并重新开始;和,
  4. 按Ctrl-D或输入“quit”退出,在这种情况下,他们提交的所有值都将丢失。
  5. 我已经能够让积分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
    

2 个答案:

答案 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'时,我

  1. 忽略basket可能是Nothing
  2. 的情况
  3. 使用basket封装的,参考(并在需要时进行模式匹配)变量basket,该变量仍在basket'的表达式范围内1}}。
  4. 这是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来解决你的问题,但是我还没有想到这个问题(还有)。