Last active
September 14, 2017 09:32
-
-
Save tdammers/88b158ffa10432bb26be55a48f6ab000 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Data.Monoid | |
import qualified Data.Set as Set | |
import Data.Set (Set) | |
import Control.Monad (forM_) | |
-- | A rule is expressed as a function from a number to a set of words to say | |
-- instead of the number. If the set is empty, the convention is to say the | |
-- number itself. | |
type Rule a b = a -> Set b | |
-- | Evaluate a set of rules against a number, returning the combined set of | |
-- words to say. | |
evalFB :: Ord b => [Rule a b] -> a -> Set b | |
evalFB = mconcat | |
-- | Format a set of words for a number. If the set is empty, the number itself | |
-- takes its place. | |
formatFB :: (Show a, Show b, Ord b) => a -> Set b -> String | |
formatFB i = formatFB' i . Set.toAscList | |
-- | 'formatFB' flavor that takes a list instead of a 'Set'. | |
formatFB' :: (Show a, Show b) => a -> [b] -> String | |
formatFB' i [] = show i | |
formatFB' _ fb = concat . map show $ fb | |
-- | Execute a rule set against an infinite list of integers starting at 1, | |
-- printing the result one number per line. Passing 'Nothing' as the limit | |
-- will keep looping indefinitely (eventually overflowing the 'Int' range). | |
runFB :: (Ord b, Show b, Show a, Enum a) => [Rule a b] -> a -> Maybe a -> IO () | |
runFB rules start limit = | |
forM_ range step | |
where | |
range = maybe (enumFrom start) (enumFromTo start) limit | |
step i = putStrLn . formatFB i . evalFB rules $ i | |
-- | Helper function to easily make a rule from a predicate. | |
fbRule :: (a -> Bool) -> b -> Rule a b | |
fbRule p fb x = if p x then Set.singleton fb else Set.empty | |
divisibleBy :: Integral a => a -> a -> Bool | |
divisibleBy n d = n `mod` d == 0 | |
fbRuleDiv :: Integral a => a -> b -> Rule a b | |
fbRuleDiv d = | |
fbRule (`divisibleBy` d) | |
fbRuleContains :: Show a => Char -> b -> Rule a b | |
fbRuleContains d = | |
fbRule ((d `elem`) . show) | |
-- Now that we have all the generalized tooling in place, we can trivially | |
-- express the actual FizzBuzz game: | |
data FizzBuzz | |
= Fizz | |
| Buzz | |
deriving (Show, Eq, Ord) | |
fizz :: Rule Integer FizzBuzz | |
fizz = fbRuleDiv 3 Fizz | |
buzz :: Rule Integer FizzBuzz | |
buzz = fbRuleDiv 5 Buzz | |
fizzBuzzMain = | |
runFB [fizz, buzz] | |
-- But we can also, just as trivially, define a similar game called "Skip", | |
-- where you have to say "skip" instead of any number that is divisible by 3 | |
-- and/or contains the digit '3' in decimal notation (i.e., 3, 6, 9, 12, 13, | |
-- 15, 18, 21, 23, 24, ... are "skip"). | |
data Skip | |
= Skip | |
deriving (Show, Eq, Ord) | |
-- | The divisible-by-3 rule | |
skipDiv = fbRuleDiv 3 Skip | |
-- | The contains-digit-3 rule | |
skipContains = fbRuleContains '3' Skip | |
skipMain = | |
runFB [skipDiv, skipContains] | |
-- As an example, we'll run the FizzBuzz game to a limit of 100. | |
main = do | |
fizzBuzzMain 1 (Just 100) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment