patternMinor
Factorials and Powers calculator
Viewed 0 times
andfactorialscalculatorpowers
Problem
I just started learning Haskell, and I made a simple program which can calculate factorials and powers:
Input:
What do you want to do (0=quit, 1=factorial, 2=power) 1
Enter a number: 10
The factorial of 10 is: 3628800
What do you want to do (0=quit, 1=factorial, 2=power) 2
Enter the base number: 2
Enter the exponent: 10
2^10 = 1024.0
What do you want to do (0=quit, 1=factorial, 2=power) 0
The code is:
I am looking for suggestions on how to improve the code, make it shorter, ...
Input:
What do you want to do (0=quit, 1=factorial, 2=power) 1
Enter a number: 10
The factorial of 10 is: 3628800
What do you want to do (0=quit, 1=factorial, 2=power) 2
Enter the base number: 2
Enter the exponent: 10
2^10 = 1024.0
What do you want to do (0=quit, 1=factorial, 2=power) 0
The code is:
import System.IO
import System.Exit
import Control.Monad
factorial :: Integer -> Integer
factorial n = product [1..n]
powerInternal :: Double -> Integer -> Double
powerInternal b 0 = 1
powerInternal b e = b * powerInternal b (e - 1)
power :: Double -> Integer -> Double
power b e = if e >= 0
then powerInternal b e
else 1 / powerInternal b (-e)
main :: IO ()
main = do
hSetBuffering stdout NoBuffering
putStr "What do you want to do (0=quit, 1=factorial, 2=power) "
input <- getLine
when (read input == 0) exitSuccess
if read input == 1
then do
putStr "Enter a number: "
hFlush stdout
num <- getLine
putStrLn ("The factorial of " ++ num ++ " is: " ++ show (factorial (read num)))
else do
putStr "Enter the base number: "
base <- getLine
putStr "Enter the exponent: "
expe <- getLine
putStrLn (base ++ "^" ++ expe ++ " = " ++ show (power (read base) (read expe)))
mainI am looking for suggestions on how to improve the code, make it shorter, ...
Solution
The rise for power
Let's start with
$$b^e = \begin{cases}\left(b^2\right)^\frac{e}{2},&e~\text{even}\\
b\left(b^2\right)^\frac{e-1}{2},&e~\text{odd}\\
\end{cases}
$$
Implementing this is left as an exercise. But it seems like you want to use
However, we're reinventing the wheel. There's already a function of type
that supports negative exponents. It's called
This is probably the shortest variant you can get. Onwards
Don't trust the user
Users will provide wrong input. Either because they intended to do so, or because their finger slipped and they accidentally put
We have now several ways to handle this. We could use
but that's error prone. Instead, we use
We can shorten it if we use
Now we know that
Foul magic
However, what's
Now you can write
instead of
But aren't the choices of the user rather limited in comparison to
instead? Now we can use
to convert 0-2 to our own enumeration.
Now, with
I will come to the yet unknown functions in a second. What did we gain so far? Well,
Splitting hairs and concerns
Now to the
The twist is, that
Summary
So, what has been improved?
From a beginners perspective, your code is fine, by the way. Some will tell you to use
Let's start with
powerInternal. First of all, there's a better algorithm for \$b^e\$, which takes \$\mathcal O(\log e)\$ instead of \$\mathcal O(e)\$:$$b^e = \begin{cases}\left(b^2\right)^\frac{e}{2},&e~\text{even}\\
b\left(b^2\right)^\frac{e-1}{2},&e~\text{odd}\\
\end{cases}
$$
Implementing this is left as an exercise. But it seems like you want to use
powerInternal only in power, since it doesn't work with negative exponents. Therefore, you should reduce its scope:power :: Double -> Integer -> Double
power b e = if e >= 0
then powerInternal b e
else 1 / powerInternal b (-e)
where
powerInternal :: Double -> Integer -> Double
powerInternal b 0 = 1
powerInternal b e = b * powerInternal b (e - 1)However, we're reinventing the wheel. There's already a function of type
(Fractional a, Integral b) => a -> b -> athat supports negative exponents. It's called
(^^). So we can change your power function:power :: Double -> Integer -> Double
power b e = b ^^ e
-- or, shorter, but less readable
-- power = (^^)This is probably the shortest variant you can get. Onwards
Don't trust the user
What do you want to do (0=quit, 1=factorial, 2=power) abc
Run.hs: Prelude.read: no parseUsers will provide wrong input. Either because they intended to do so, or because their finger slipped and they accidentally put
12 instead of 1. If you don't want your program to crash, you probably want to ask again instead. Therefore, we should probably use a function:askUser :: IO Int
askUser = do
putStr "What do you want to do? (0: quit; 1: factorial; 2: power) "
input <- getLineWe have now several ways to handle this. We could use
casecase input of
"0" -> return 0
"1" -> return 1
"2" -> return 2
_ -> putStrLn "Wrong input, please try again" >> askUserbut that's error prone. Instead, we use
readMaybe from Text.Read:let input' = readMaybe input
if 0 > askUserWe can shorten it if we use
fmap readMaybe getLine:askUser :: IO Int
askUser = do
putStr "What do you want to do? (0: quit; 1: factorial; 2: power) "
input return x
_ -> putStrLn "Wrong input, please try again" >> askUserNow we know that
askUser will always return a number in the range 0-2. Foul magic
However, what's
0? What is 1? All three numbers are magic numbers, which is usually a code smell. You, as a developer, have to remember what a certain number stands for. Save them somewhere, likeuserQuit, userFactorial, userPower :: Int
userQuit = 0
userFactorial = 1
userPower = 2Now you can write
if answer == userQuitinstead of
if answer == 0But aren't the choices of the user rather limited in comparison to
Int? How aboutdata UserChoice = Quit | Factorial | Power deriving (Enum, Bounded, Show)instead? Now we can use
askUserChoice :: IO UserChoice
askUserChoice = fmap toEnum askUserto convert 0-2 to our own enumeration.
Now, with
askUserChoice, we get the following main:main :: IO ()
main = do
hSetBuffering stdout NoBuffering
input exitSuccess
Factorial -> userFactorial >> main
Power -> userPower >> mainI will come to the yet unknown functions in a second. What did we gain so far? Well,
input is going to be a valid answer when we try to inspect it, and we can pattern match on it. Even better, the compiler can tell us that we forgot a pattern if we decide to add Addition or something similar later.Splitting hairs and concerns
Now to the
user* functions. They are pretty much the same as yours, with a small twist:userFactorial :: IO ()
userFactorial = do
num <- getInteger "a number"
putStrLn ("The factorial of " ++ show num ++ " is: " ++ show (factorial num))
userPower :: IO ()
userPower = do
base <- getInteger "the base number"
expe <- getInteger "the exponent"
putStrLn (show base ++ "^" ++ show expe ++ " = " ++ show (power base expe))The twist is, that
getInteger will ask the user till they actually provide an integer:getInteger :: String -> IO Integer
getInteger what = do
putStrLn ("Enter " ++ what ++ ": ")
hFlush stdout
input putStrLn "Not a number, please try again." >> getInteger what
Just x -> return xSummary
So, what has been improved?
- The
powerfunction uses(^^), a standard library function
- We check user input for validity
- We don't
readvalues multiple times
- We show our intentions for the user's action with a data type
- We provide a succinct
mainand let other functions handle the actual work
- We share common problems (getting an integer from the user) and don't repeat ourselfs.
From a beginners perspective, your code is fine, by the way. Some will tell you to use
($) instead of parentheses in your putStrLn lines, but that's a personal preferencCode Snippets
power :: Double -> Integer -> Double
power b e = if e >= 0
then powerInternal b e
else 1 / powerInternal b (-e)
where
powerInternal :: Double -> Integer -> Double
powerInternal b 0 = 1
powerInternal b e = b * powerInternal b (e - 1)(Fractional a, Integral b) => a -> b -> apower :: Double -> Integer -> Double
power b e = b ^^ e
-- or, shorter, but less readable
-- power = (^^)What do you want to do (0=quit, 1=factorial, 2=power) abc
Run.hs: Prelude.read: no parseaskUser :: IO Int
askUser = do
putStr "What do you want to do? (0: quit; 1: factorial; 2: power) "
input <- getLineContext
StackExchange Code Review Q#139869, answer score: 3
Revisions (0)
No revisions yet.