Consider replacing isIntegral
with an inline test:
...
main = do
putStrLn "Enter a non-negative integer:"
numberStr <- getLine
if not (null numberStr) && all isDigit numberStr
...
It's hard to explain, both concisely and correctly, what isIntegral
(or as some suggested, isNat
or isNaturalNumber
) does. Sure, you can say it tests if the string contains a non-negative integer. But does it accept hexadecimal syntax? How does it treat octals?
On the other hand, perhaps the definition of isIntegral
can be based on purpose, rather than function:
-- | Test if the string is a valid, non-negative integer that can be parsed
-- with 'read'.
isIntegral :: String -> Bool
isIntegral [] = False
isIntegral st = all isDigit st
By this interpretation, keeping isIntegral
separate makes it easier to extend later (e.g. to allow hexadecimal syntax, which read :: String -> Integer
accepts). It's a matter of fixing a standalone function, rather than modifying code buried in a procedure with broader scope (namely, main
).
However, isIntegral
's definition assumes that the caller calls read
(or similar) after validating the input with isIntegral
. It would be simpler to do the reading and validation in one function. See the readNumber
function in Rotsor's answer, which uses reads
and a guard to accomplish this.
Playing with point-free
Let's see if we can make this more concise:
isIntegral :: String -> Bool
isIntegral [] = False
isIntegral st = all isDigit st
Or, put another way:
isIntegral st = not (null st) && all isDigit st
We can express it in point-free style (i.e. eliminate variables using combinators like .
, flip
, uncurry
, etc.):
isIntegral = liftA2 (&&) (not . null) (all isDigit)
Yes, this is longer and more cryptic. However, with a little practice, it's easy to see that it's combining two predicates, not . null
and all isDigit
, with the &&
operator. Since &&
doesn't like functions as arguments, the noisy liftA2
(from Control.Applicative) is applied to &&
to combine predicates.
Although I would say point-free is overkill here, it's good to practice using it. Here are a couple examples where point-free does make things more concise (in my opinion):
-- Read two lines, and combine their values with (+)
addLines :: IO Integer
addLines = (+) <$> readLn <*> readLn
-- Call "hPutStrLn msg" on each Handle in the list.
--
-- Unfortunately, `hPutStrLn` takes the Handle argument first, so we have to
-- flip it to use it like we want.
broadcast :: String -> [Handle] -> IO ()
broadcast = mapM_ . flip hPutStrLn
Learning to write in point-free style can help you write programs faster. Here's a script I wrote recently that takes a list of file names, and prints lines present in every file:
import Data.List (foldl1')
import System.Environment
import qualified Data.Set as S
main :: IO ()
main = getArgs
>>= mapM (fmap (S.fromList . lines) . readFile)
>>= mapM_ putStrLn . S.toList . foldl1' S.intersection