Skip to content

Instantly share code, notes, and snippets.

@tdammers
Last active September 14, 2017 09:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tdammers/88b158ffa10432bb26be55a48f6ab000 to your computer and use it in GitHub Desktop.
Save tdammers/88b158ffa10432bb26be55a48f6ab000 to your computer and use it in GitHub Desktop.
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