Могу ли я сделать это Haskell больше идиоматических c? - PullRequest
0 голосов
/ 20 марта 2020

Я работаю над набором задач 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

Ответы [ 3 ]

4 голосов
/ 20 марта 2020

Мне не нравится тип compTrips; это слишком разрешительно. Объявление о том, что он может вернуть [Int], говорит, что вы не знаете, сколько Int s он вернет. Но вы знаете, что будет ровно два; Итак:

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) на использование комбинации length и filter (или, в этом случае, когда мы хотим оба "стороны" фильтра, partition). Итак:

import Data.Bifunctor
import Data.List

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

Я думаю, что вы можете абстрагировать чтение и анализ строки, чтобы этот лог c не реплицировался. Итак:

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, говорит о том, что он может анализировать что угодно, хотя в действительности он может анализировать только очень специфический c язык. readMaybe имеет лучший тип, говоря, что иногда он может не анализироваться:

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

Существует большая коллекция основанных на Applicative методов для комбинирования обработки ошибок многих вызовов с readMaybe ; особенно проверьте traverse (что немного похоже на map, но с возможностью обработки ошибок) и liftA2 (который может преобразовать любую двоичную операцию в одну, которая может обрабатывать ошибки).

One мы могли бы использовать его, чтобы напечатать красивое сообщение об ошибке, когда оно не удалось, поэтому:

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

(Существуют другие варианты обработки ошибок.)

Это оставляет нас со следующим последним program:

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

Хотя текст на самом деле немного длиннее, я бы посчитал это более идиоматической реализацией c; две основные вещи - уделять пристальное внимание тому, чтобы избегать использования частичных функций (таких как read и ваше частичное сопоставление с шаблоном в результате compTrips) и увеличивать зависимость от массовых операций, предоставляемых библиотекой, при обработке списков (таких как наш использование length и partition).

2 голосов
/ 20 марта 2020

Вы можете получить количество элементов, соответствующих length . filter. Таким образом, вы можете подсчитать количество совпадений с помощью:

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

Обычно лучше возвращать кортежи, чтобы вернуть «множественные» значения. С тех пор эти два значения могут иметь различный тип, и, кроме того, механизм проверки типов гарантирует, что вы вернете два элемента.

Вы можете выполнить fmap :: Functor f => (a -> b) -> f a -> f b или его операторный синоним (<$>) :: Functor f => (a -> b) -> f a -> f b по результатам getLine для "последующей обработки" результата:

main :: IO ()
main = do
  line1 <- <b>map read . words <$></b> getLine
  line2 <- <b>map read . words <$></b> getLine
  let <b>(a, b)</b> = compTrips line1 line2
  putStrLn (show a ++ " " ++ show b)
1 голос
/ 20 марта 2020

Возможно, вам нравится

λ> :{
λ| 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]
...