HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Leap year check in Haskell, using pattern matching or bind operator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
yearoperatorleapusinghaskellbindcheckpatternmatching

Problem

One of the first Haskell puzzles on http://exercism.io/ is to implement a leap year check. I've done this twice.

Using pattern matching:

isLeapYear :: Integer -> Bool
isLeapYear year
  | year `mod` 400 == 0 = True
  | year `mod` 100 == 0 = False
  | year `mod` 4 == 0   = True
  | otherwise           = False


Using the bind operator >>=:

isLeapYear :: Integer -> Bool
isLeapYear year = head $
                  [(400, True), (100, False), (4, True), (1, False)]
                  >>= check year
  where check y (interval, isLeap) = [isLeap | y `mod` interval == 0]


I'd like to know which implementation is “better“ / more idiomatic in Haskell. I am unsure whether I might have misused a t0o powerful concept in the second try, and the first try might just be more readable.

Solution

The first one is a lot more readable, whereas the second one uses a "hack". I would go with the first one, except that I would use rem, which is a little bit faster. And one could introduce some DRY:

isDivisibleBy :: Integral n => n -> n -> Bool
isDivisibleBy x n = x `rem` n == 0

isLeapYear :: Integer -> Bool
isLeapYear year
  | divBy 400 = True
  | divBy 100 = False
  | divBy   4 = True
  | otherwise = False
 where
   divBy n = year `isDivisibleBy` n


That being said, for a programming challenge, your version is perfectly fine:

isLeapYear :: Integer -> Bool
isLeapYear year
  | year `rem` 400 == 0 = True
  | year `rem` 100 == 0 = False
  | year `rem`   4 == 0 = True
  | otherwise           = False


The latter can be rewritten without >>= as list comprehension:

isLeapYear :: Integer -> Bool
isLeapYear year = head [isLeap | (interval, isLeap) <- classifications
                               , year `isDivisibleBy` interval]
  where
    classifications = [(400, True), (100, False), (4, True), (1, False)]


You could get rid of the "hack" with safeHead and maybe False, but that's left as an exercise.

If you really want to use check, remove the y. It just introduces an additional error source:

isLeapYear :: Integer -> Bool
isLeapYear year = head $
                  [(400, True), (100, False), (4, True), (1, False)]
                  >>= check
  where check (interval, isLeap) = [isLeap | year `rem` interval == 0]


Note that this shows perfectly that >>= is just flip concatMap for lists. So let's take advantage:

isLeapYear :: Integer -> Bool
isLeapYear year = head $ concatMap check classifications ++ [False]
  where 
    check (interval, isLeap) = [isLeap | year `rem` interval == 0]
    classifications = [(400, True), (100, False), (4, True)]


Which is easier to grasp than the version with >>=.

Code Snippets

isDivisibleBy :: Integral n => n -> n -> Bool
isDivisibleBy x n = x `rem` n == 0

isLeapYear :: Integer -> Bool
isLeapYear year
  | divBy 400 = True
  | divBy 100 = False
  | divBy   4 = True
  | otherwise = False
 where
   divBy n = year `isDivisibleBy` n
isLeapYear :: Integer -> Bool
isLeapYear year
  | year `rem` 400 == 0 = True
  | year `rem` 100 == 0 = False
  | year `rem`   4 == 0 = True
  | otherwise           = False
isLeapYear :: Integer -> Bool
isLeapYear year = head [isLeap | (interval, isLeap) <- classifications
                               , year `isDivisibleBy` interval]
  where
    classifications = [(400, True), (100, False), (4, True), (1, False)]
isLeapYear :: Integer -> Bool
isLeapYear year = head $
                  [(400, True), (100, False), (4, True), (1, False)]
                  >>= check
  where check (interval, isLeap) = [isLeap | year `rem` interval == 0]
isLeapYear :: Integer -> Bool
isLeapYear year = head $ concatMap check classifications ++ [False]
  where 
    check (interval, isLeap) = [isLeap | year `rem` interval == 0]
    classifications = [(400, True), (100, False), (4, True)]

Context

StackExchange Code Review Q#160749, answer score: 6

Revisions (0)

No revisions yet.