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

Factorials and Powers calculator

Submitted by: @import:stackexchange-codereview··
0
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:

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)))

  main


I am looking for suggestions on how to improve the code, make it shorter, ...

Solution

The rise for power

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 -> a


that 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 parse


Users 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 <- getLine


We have now several ways to handle this. We could use case

case input of
    "0" -> return 0
    "1" -> return 1
    "2" -> return 2
    _   -> putStrLn "Wrong input, please try again" >> askUser


but that's error prone. Instead, we use readMaybe from Text.Read:

let input' = readMaybe input

  if 0 > askUser


We 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" >> askUser


Now 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, like

userQuit, userFactorial, userPower :: Int
userQuit      = 0
userFactorial = 1
userPower     = 2


Now you can write

if answer == userQuit


instead of

if answer == 0


But aren't the choices of the user rather limited in comparison to Int? How about

data UserChoice = Quit | Factorial | Power deriving (Enum, Bounded, Show)


instead? Now we can use

askUserChoice :: IO UserChoice
askUserChoice = fmap toEnum askUser


to 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     >> main


I 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 x


Summary

So, what has been improved?

  • The power function uses (^^), a standard library function



  • We check user input for validity



  • We don't read values multiple times



  • We show our intentions for the user's action with a data type



  • We provide a succinct main and 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 preferenc

Code 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 -> a
power :: 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 parse
askUser :: IO Int
askUser = do
  putStr "What do you want to do? (0: quit; 1: factorial; 2: power) "
  input <- getLine

Context

StackExchange Code Review Q#139869, answer score: 3

Revisions (0)

No revisions yet.