Рекомендации по применению DRY в определениях функций Haskell - PullRequest
3 голосов
/ 06 мая 2009

У меня есть вопрос о том, считается ли конкретный способ применения принципа DRY хорошей практикой в ​​Haskell. Я собираюсь представить пример, а затем спросить, считается ли подход, который я использую, хорошим Стиль Хаскеля. В двух словах, вопрос заключается в следующем: если у вас длинная формула, а затем вам нужно повторить некоторые небольшие подмножества этой формулы в другом месте, всегда ли вы помещаете это повторное подмножество формулы в переменную, чтобы вы может остаться сухим? Почему или почему нет?

Пример: Представьте, что мы берем строку цифр и преобразуем эту строку в соответствующее значение Int. (Кстати, это упражнение из "Real World Haskell").

Вот решение, которое работает, за исключением того, что оно игнорирует крайние случаи:

asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) = (newValue, newPlace)
      where 
        newValue = (10 ^ place) * (digitToInt char) + sum
        newPlace = place + 1

Он использует foldr, и аккумулятор является кортежем следующего значения места и суммы на данный момент.

Пока все хорошо. Теперь, когда я приступил к реализации проверок краевого регистра, я обнаружил, что мне нужны маленькие порции формулы "newValue" в разных местах для проверки на наличие ошибок. Например, на моей машине было бы переполнение Int, если бы входное значение было больше (2 ^ 31 - 1), поэтому максимальное значение, которое я мог бы обработать, составляет 2 147 483 647. Поэтому я поставил 2 чека:

  1. Если значение места 9 (место в миллиардах) и значение цифры> 2, возникает ошибка.
  2. Если сумма + (10 ^ место) * (digitToInt char)> maxInt, возникает ошибка.

Эти две проверки заставили меня повторить часть формулы, поэтому я ввел следующие новые переменные:

  • digitValue = digitToInt char
  • newPlaceComponent = (10 ^ место) * digitValue

Причина, по которой я ввел эти переменные, заключается просто в автоматическом применении принципа СУХОЙ: Я обнаружил, что повторяю эти части формулы, поэтому я определил их один раз и только один раз.

Однако мне интересно, считается ли это хорошим стилем Хаскеля. Есть очевидные преимущества, но я вижу и недостатки. Это определенно удлиняет код, тогда как большая часть кода на Haskell, который я видел, довольно лаконична.

Итак, вы считаете это хорошим стилем Хаскелла, и придерживаетесь ли вы этой практики или нет? Почему / почему нет?

И что бы это ни стоило, вот мое окончательное решение, которое имеет дело с рядом крайних случаев и, следовательно, имеет довольно большой блок where. Вы можете увидеть, насколько большим стал блок из-за моего применения принципа СУХОЙ.

Спасибо.

asInt_fold "" = error "You can't be giving me an empty string now"
asInt_fold "-" = error "I need a little more than just a dash"
asInt_fold string | isInfixOf "." string = error "I can't handle decimal points"
asInt_fold ('-':xs) = -1 * (asInt_fold xs) 
asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) | place == 9 && digitValue > 2 = throwMaxIntError
               | maxInt - sum < newPlaceComponent      = throwMaxIntError
                   | otherwise                             = (newValue, newPlace)
            where
              digitValue =  (digitToInt char)
              placeMultiplier = (10 ^ place)
              newPlaceComponent = placeMultiplier * digitValue
              newValue = newPlaceComponent + sum
              newPlace = place + 1
              maxInt = 2147483647
              throwMaxIntError = 
                        error "The value is larger than max, which is 2147483647"

Ответы [ 3 ]

9 голосов
/ 06 мая 2009

DRY в Хаскеле так же хорош, как и везде :) Во многом из-за краткости, о которой вы говорите в haskell, заключается в том, что многие идиомы извлечены из библиотек, и что часто те примеры, на которые вы смотрите, очень тщательно продуманы, чтобы сделать их краткими:)

Например, вот альтернативный способ реализации вашего алгоритма преобразования цифр в строку:

asInt_fold ('-':n) = negate (asInt_fold n)
asInt_fold "" = error "Need some actual digits!"
asInt_fold str = foldl' step 0 str
    where
        step _ x
            | x < '0' || x > '9'
            = error "Bad character somewhere!"
        step sum dig =
            case sum * 10 + digitToInt dig of
                n | n < 0 -> error "Overflow!"
                n -> n

Несколько замечаний:

  1. Мы обнаруживаем переполнение, когда оно происходит, а не путем определения ограничений произвольного числа на то, какие цифры мы допускаем. Это значительно упрощает логику обнаружения переполнения и позволяет работать с любым целочисленным типом от Int8 до Integer [до тех пор, пока переполнение не приводит к циклическому переходу, не происходит или приводит к утверждению самого оператора сложения]
  2. При использовании другого сгиба нам не нужно два отдельных состояния.
  3. Не повторяться, даже не пытаясь найти способ поднять вещи - это естественно выпадает из повторения того, что мы пытаемся сказать.

Теперь не всегда возможно просто переформулировать алгоритм и убрать дублирование, но всегда полезно сделать шаг назад и пересмотреть, как вы думали о проблеме:)

4 голосов
/ 08 мая 2009

Как отмечает bdonlan, ваш алгоритм может быть чище - особенно полезно, чтобы сам язык обнаруживал переполнение. Что касается самого кода и стиля, я думаю, что главный компромисс в том, что каждое новое имя накладывает небольшую когнитивную нагрузку на читателя . Когда назвать промежуточный результат, становится призывом к суждению.

Лично я бы не стал называть placeMultiplier, так как я думаю, что цель place ^ 10 гораздо яснее. И я бы посмотрел на maxInt в Prelude, так как вы рискуете быть ужасно неправильным, если работаете на 64-битном оборудовании. В противном случае единственное, что я нахожу нежелательным в вашем коде - это лишние скобки. Так что у вас есть приемлемый стиль.

(Мои учетные данные: на данный момент я написал порядка 10 000–20 000 строк кода на Haskell и, возможно, прочитал это в два или три раза. У меня также в десять раз больше опыта работы с языком семейства ML , которые требуют от программиста принятия аналогичных решений.)

2 голосов
/ 06 мая 2009

Думаю, то, как вы это сделали, имеет смысл.

Вы, конечно, всегда должны разбивать повторные вычисления на отдельно определенные значения, если важно избегать повторных вычислений, но в этом случае это не выглядит необходимым. Тем не менее, вычеркнутые значения имеют простые для понимания имена, поэтому они облегчают понимание вашего кода. Я не думаю, что тот факт, что ваш код немного длиннее в результате, является плохой вещью.

Кстати, вместо жесткого кодирования максимального значения Int, вы можете использовать (maxBound :: Int), что исключает риск того, что вы допустите ошибку или другую реализацию с другим максимальным значением Int, нарушающим ваш код.

...