2
\$\begingroup\$

I am trying to learn me some Haskell because I found it to be an interesting language. Right now, I am trying to implement a function in Haskell to get acquainted with the language. The function just gets user input and adds the input numbers to a list of floats.

get_budget_values _values _index _length = do
    tmp_value <- getLine

    let new_value = read tmp_value :: Float
    let new_values = new_value : _values
    let new_index = _index + 1

    if new_index < _length
        then get_budget_values new_values new_index _length
        else return new_values

So, is this a proper way to implement a function like that in Haskell? What beginner mistakes am I making? Are there any standard ways of doing something like this?

\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

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

\$\endgroup\$
2
\$\begingroup\$

You just want call getLine function length_ times? It is as simple as

values <- (map read) <$> replicateM length_ getLine :: IO [Float]

Here replicateM c a performs a action c times. The replicateM length_ getLine expression returns an action that produces a list of strings, IO [String]. Using <$> operator (aka fmap) you can apply a function to [String] part of that IO [String] thing. Finally, the function we apply is map read converts a String into a Float, because we explicitly wrote the type of the whole expression.

It might be useful to you to see how replicateM itself is implemented:

replicateM cnt f =
    loop cnt
  where
    loop cnt = if cnt <= 0 then return [] else do
                    first <- f
                    leftover <- loop (cnt - 1)
                    return (f:leftover)
\$\endgroup\$
3
  • \$\begingroup\$ I don't know what I am doing wrong, but the implementation you provided crashes after I enter the number 2 \$\endgroup\$
    – Joan Vene
    Mar 9, 2018 at 22:38
  • \$\begingroup\$ Crashes? Any error messages? \$\endgroup\$
    – arrowd
    Mar 10, 2018 at 7:52
  • \$\begingroup\$ I had imported the wrong module \$\endgroup\$
    – Joan Vene
    Mar 14, 2018 at 0:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.