【问题标题】:Could I make this Haskell more idiomatic?我可以让这个 Haskell 更惯用吗?
【发布时间】:2020-07-01 21:40:59
【问题描述】:

我正在研究 HackerRank 问题集。下面的 Haskell 正确地解决了这个问题,但我有一种预感,这不是一个经验丰富的老手会写的。对于如何使这个更漂亮/更具表现力的任何意见,我将不胜感激。

compTrips :: [Int] -> [Int] -> [Int]
compTrips as bs =
  let oneIfGreater a b = if a > b
                         then 1
                         else 0
      countGreater x y = foldr (+) 0 $ zipWith oneIfGreater x y
   in [countGreater as bs, countGreater bs as]

main = do
  line1 <- getLine
  line2 <- getLine
  let alice = map read $ words line1
  let bob = map read $ words line2
  let (a:b:_) = compTrips alice bob
  putStrLn $ show a ++ " " ++ show b

【问题讨论】:

  • 代码审查网站可能更适合这个问题。
  • 一个问题:如果用户输入错误,你的程序就会崩溃。更好:导入Text.Read 并将readMaybetraverse 一起使用。出错时,使用Control.Exception.throwIO 抛出正确的IO 异常。
  • @dfeuer 我会等我的 40 分钟过去并交叉发布。 (自从我上次使用它以来,它已经成长了很多!)我理解您关于使代码更具弹性的反馈。然而,问题陈述保证输入将被清理,所以我想知道更多关于样式的信息。
  • foldr (+) 0 只是sum。但是您可以使用length (filter (...))。此外,如果您返回两个值,则最好使用 2 元组,因为那时类型可以是异质的,此外,通过您指定的类型签名(和编译器检查),它将 always 返回 2 个值。

标签: haskell idioms


【解决方案1】:

我不喜欢compTrips的类型;它太宽容了。声明它可以返回 [Int] 表示您不知道它将返回多少 Ints。但你知道这将是两个;所以:

compTrips :: [Int] -> [Int] -> (Int, Int)

在实现中,我更喜欢sum 而不是foldr (+) 0;它更清楚地说明了您的意图,而且,作为附带的好处,sum 使用严格的左折叠,因此有时可以更节省内存:

compTrips as bs =
  let oneIfGreater a b = if a > b
                         then 1
                         else 0
      countGreater x y = sum $ zipWith oneIfGreater x y
   in (countGreater as bs, countGreater bs as)

我不喜欢我们计算每个比较两次。我想将比较的计算与比较的计算方式分开。同时,我将从使用sum 和您的自定义fromEnum 实现(即oneIfGreater)切换到使用lengthfilter 的组合(或者,在这种情况下,我们想要两者过滤器的“边”,partition)。所以:

import Data.Bifunctor
import Data.List

compTrips as bs = bimap length length . partition id $ zipWith (>) as bs

我认为你可以抽象出一行的读取和解析,这样这个逻辑就不会被复制。所以:

readInts :: IO [Int]
readInts = do
    line <- getLine
    pure (map read $ words line)

main = do
  alice <- readInts
  bob <- readInts
  let (a, b) = compTrips alice bob
  putStrLn $ show a ++ " " ++ show b

我不喜欢read 的类型,原因几乎与我不喜欢compTrips 的类型相同。在这种情况下,声明它可以接受任何String 表示它可以解析任何东西,而实际上它只能解析一种非常特定的语言。 readMaybe 有一个更好的类型,说它有时可能会解析失败:

readMaybe :: Read a => String -> Maybe a

有大量基于Applicative 的方法用于组合对readMaybe 的多次调用的错误处理;尤其是traverse(有点像map,但具有处理错误的能力)和liftA2(可以将任何二进制操作转换为可以处理错误的操作)。

我们可以使用它的一种方法是在失败时打印一个很好的错误消息,所以:

import System.IO
import Text.Read

readInts = do
  line <- getLine
  case traverse readMaybe (words line) of
    Just person -> pure person
    Nothing -> do
      hPutStrLn stderr "That doesn't look like a space-separated collection of numbers! Try again."
      readInts

(存在其他错误处理选项。)

这给我们留下了以下最终程序:

import Data.Bifunctor
import Data.List
import System.IO
import Text.Read

compTrips :: [Int] -> [Int] -> (Int, Int)
compTrips as bs = bimap length length . partition id $ zipWith (>) as bs

readInts :: IO [Int]
readInts = do
  line <- getLine
  case traverse readMaybe (words line) of
    Just person -> pure person
    Nothing -> do
      hPutStrLn stderr "That doesn't look like a space-separated collection of numbers! Try again."
      readInts

main :: IO ()
main = do
  alice <- readInts
  bob <- readInts
  let (a, b) = compTrips alice bob
  putStrLn $ show a ++ " " ++ show b

虽然文本实际上有点长,但我认为这是一个更惯用的实现;主要的两件事是要特别注意避免使用部分函数(如read 和您对compTrips 结果的部分模式匹配),以及在处理列表时增加对库提供的批量操作的依赖(如我们的使用lengthpartition)。

【讨论】:

  • 谢谢。我可以理解为什么 Galois 的员工“不喜欢”这些结构。让我猛烈抨击你的版本,直到我对它有一些直觉。
  • @cyclotropic, bimap length length 这里很糟糕。最好使用foldl 包中的设施。
  • 非常小的一点:原始代码计算了&lt;&gt; 的情况,忽略了== 的情况。上面的代码使用partition,因此== 案例被计算在内(AFAICS,连同&lt; 案例)。这可能是原始代码中的一个错误。
  • 我对组合器的容忍度高于大多数;我想在readInts 中使用whenNothing(M)。另外,我更喜欢&lt;&gt; 而不是++ for String,因为事实上它实际上是[Char],这感觉就像一个实现细节,尽管每个人都很熟悉和熟悉。当您的程序也增长时,更容易切换到Text
  • @chi 重点:你没有阅读我的原帖。
【解决方案2】:

您可以获取与length . filter 匹配的元素数量。所以你可以在这里计算匹配的数量:

compTrips :: [Int] -> [Int] -> (Int, Int)
compTrips as bs = (f as bs, f bs as)
    where f xs = length . filter id . zipWith (>) xs

通常最好返回元组以返回“多个”值。从那时起,这两个值可以有不同的类型,而且类型检查机制将保证您返回两个项目。

您可以对getLine 的结果执行fmap :: Functor f =&gt; (a -&gt; b) -&gt; f a -&gt; f b 或其运算符同义词(&lt;$&gt;) :: Functor f =&gt; (a -&gt; b) -&gt; f a -&gt; f b 以“后处理”结果:

main :: IO ()
main = do
  line1 <- map read . words <$> getLine
  line2 <- map read . words <$> getLine
  let (a, b) = compTrips line1 line2
  putStrLn (show a ++ " " ++ show b)

【讨论】:

  • 投了赞成票,因为您关注样式和实现。
【解决方案3】:

你可能会喜欢

λ> :{
λ| getLine >>= return . map (read :: String -> Int) . words
λ|         >>= \l1 -> getLine
λ|         >>= return . map (read :: String -> Int) . words
λ|         >>= \l2 -> return $ zipWith (\a b -> if a > b then 1 else 0) l1 l2
λ| :}
1 2 3 4 5
2 3 1 4 2
[0,0,1,0,1]

【讨论】:

    猜你喜欢
    • 2011-08-18
    • 2017-11-06
    • 2011-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-02
    相关资源
    最近更新 更多