Add type signatures to top-level functions
You should always add a type signature to top-level functions:
get_budget_values :: [Float] -> Int -> Int -> IO [Float]
get_budget_values _values _index _length = do
...
Make functions easy to use and hard to misuse
However, this function's type is weird. We want to get _length
values, so why do we have to supply them first? That's error prone. Our funtion should have the type Int -> IO [Float]
instead:
get_budget_values :: Int -> IO [Float]
get_budget_values n = go [] 0 n
where
go values index len = do
tmp_value <- getLine
let new_value = read tmp_value
let new_values = new_value : values
let new_index = index + 1
if new_index < len
then go new_values new_index len
else return new_values
Note that due to get_budget_values
' type signature we don't have to use :: Float
at read temp_value
anymore.
Follow naming conventions and use existing library functions
Still, our function is sub-par, since we don't follow the common naming convention (camelCase
) on the one hand don't use library functions for our advantage.
So let's say we have doNTimes :: Int -> IO a -> IO [a]
so that doNTimes n action
repeats the action
n
times and collects the results in a list. Then we can write
getBudgetValues :: Int -> IO [Float]
getBudgetValues n = doNTimes n getSingleFloat
where
getSingleFloat = do
tmpValue <- getLine
return (read tmpValue)
The getLine
followed by read
approach is so common that there is a function for that, namely readLn
:
getSingleFloat = readLn
We end up with
getBudgetValues :: Int -> IO [Float]
getBudgetValues n = doNTimes n readLn
What's missing is doNTimes
. Now, we know that there is replicate :: Int -> a -> [a]
, which is almost correct, but we would end up with [IO Float]
instead of IO [Float]
. But there's also sequence :: [IO a] -> IO [a]
.*
Therefore, we can write
doNTimes :: Int -> IO a -> IO [a]
doNTimes n action = sequence (replicate n action)
The final result
This function is also often used and available as replicateM
in Control.Monad
. We end up with
import Control.Monad (replicateM)
getBudgetValues :: Int -> IO [Float]
getBudgetValues n = replicateM n readLn
Exercises
Write replicate
and sequence
yourself without looking at their respective source code. replicate :: Int -> a -> [a]
should fulfil the following properties for positive n
:
let result = replicate n x
in length result == n && all (== x) result
For any non-positive n
, the list should be empty.
sequence :: [IO a] -> IO [a]
should execute the actions and collect their results, e.g.
sequence [putStrLn "Hello", putStrLn "World"]
should print Hello
and World
on two lines and return [(),()]
.
* the actual types are more general