Area under curve

Posted on

Problem

The following code is a solution to a Hackerrank problem in Haskell. Given a list of polynomial coeficients a and exponents b, find the area under the polynomial’s curve from l to r using a step size of 0.001. Then to find the volume of the curve rotated around the x-axis.

As a newbie to Haskell, I ran into rather frustrating type errors, and I feel like I’ve used hacky solutions to fix them. Specifically, I think there’s a lot on unnecessary code needed to make sure all of the * + - ^ operators have the same type for left and right parameters.

Any tips on naming conventions would also be appreciated.

import Text.Printf (printf)

-- This function should return a list [area, volume].
solve :: Int -> Int -> [Int] -> [Int] -> [Double]
solve l r a b = [
    sum [(poly x as bs) / 1000.0 | x <- xs], -- area under curve
    sum [pi * (poly x as bs) ^ 2 / 1000.0 | x <- xs] -- volume under rotated curve
    ]
    where
        as = map fromIntegral a
        bs = map fromIntegral b
        ls = fromIntegral l
        rs = fromIntegral r
        xs = [ls, ls+0.001..rs]

poly :: Double -> [Double] -> [Double] -> Double
poly x a b = sum [fst c * x ** snd c | c <- zip a b]

--Input/Output.
main :: IO ()
main = getContents >>= mapM_ (printf "%.1fn"). ([a, b, [l, r]] -> solve l r a b). map (map read. words). lines

The I/O is witchcraft to me, so I’m not too concerned with reviews of that portion.

Solution

solve‘s type is a disaster. I guess its provided by HackerRank, so that’s not a surprise and not your fault. This is also the reason why you get so many type errors. Your poly reflects that perfectly: you want to use Double, not Int.

But while we’re at poly, let’s change it. First of all, the coefficients and exponents should stay together in my opinion, so let’s change poly‘s type slightly:

poly :: [(Double, Double)] -> Double -> Double
poly as x = sum [fst c * x ** snd c | c <- as]

Why x as second argument and not as first? Because you usually have a function f that is a polynom f = poly coeffs. That way you can easily give a name to poly coeffs and then only use f 1, f 2 and so on:

ghci> let f = poly [(1,2)]
ghci> f 1
1
ghci> f 2
4
ghci> f 3
9

However, poly is still not optimal, we can get rid of fst and snd, and we can relax the types if we want to:

poly :: (Num a, Integral n) => [(a, n)] -> a -> a
poly cs x = sum [a * (x ^ b) | (a,b) <- cs]

You can, of course, keep the type to Double (you have to change ^ back to **).

Now to solve. Let’s use proper types first:

solve :: Double -> Double -> [(Double,Int)] -> (Double, Double)

We lifted the Int -> Double part out of the function. Also, instead of a list, we return a pair. We really want two and only two values as a result. Not arbitrary many, which is modelled with a list. The implementation doesn’t differ too much from yours, apart from the pair and some removed magic numbers:

solve l r cs = 
    ( sum [     (p x)     * step | x <- xs]
    , sum [pi * (p x) ^ 2 * step | x <- xs]
    )
  where
    p    = poly cs
    step = 0.001
    xs   = [l,l + step..r]

Remark: You really don’t want to use 1000.0 and 0.001 without a proper name in your code. Later you might notice that you need step = 0.0005, and suddenly you have to change all your numbers throughout the code.

IO can be simple

And now to the last part. Proper IO. Many challenge sites give you an IO skeleton that is rather unusable, to be honest. It’s often somewhat clever, but it really sucks for strict type safe languages. For example the code would be fine in C, since double + int is double. In Haskell, the type of + dictates that both sides need to be of the same type.

To get out of the “I/O [..] witchcraft”, let’s write one that’s easier to understand. First, we write a function to read a line:

getListLine :: Read a => IO [a]
getListLine = do
  line <- getLine
  return (map read (words line))

Then, we use that function to get those three lines, calculate the area and the volume, and then print both values:

main :: IO ()
main = do
  as     <- getListLine
  bs     <- getListLine
  [l, r] <- getListLine

  let (area, volume) = solve l r (zip as bs)
  print area
  print volume

This should be a lot easier to understand than the function provided by the challenge site.

Naming conventions

You call something xs, as, bs or similar if you have a collection. You call something l' if it’s based on l. So in your original solve the usual names would be

solve :: Int -> Int -> [Int] -> [Int] -> [Double]
solve l r as bs = [
    sum [f x / 1000.0 | x <- xs],
    sum [pi * (f x) ^ 2 / 1000.0 | x <- xs]
    ]
    where
        as' = map fromIntegral as
        bs' = map fromIntegral bs
        l'  = fromIntegral l
        r'  = fromIntegral r
        xs  = [l', l'+0.001..r']
        f x = poly x as' bs'

Leave a Reply

Your email address will not be published. Required fields are marked *