3

New to Haskell

The problem is

-- The centroid of a list of points is a point whose x (and y) coordinates are
-- the means of the x (and y) coordinates of the points in the list.
--
-- You may assume the list contains at least one point.
--
-- > centroid [Pt 1 1, Pt 2 2]
-- Pt 1.5 1.5
-- > centroid [Pt (-1.5) 0, Pt 3 2, Pt 0 1]
-- Pt 0.5 1.0

Try to code like this

data Point = Pt Double Double deriving (Show, Eq)

centroid :: [Point] -> Point

pointX :: Point -> Double
pointX (Pt x y) = x
pointY :: Point -> Double
pointY (Pt x y) = y

pointsX :: [Point] -> [Double]
pointsX xs = map pointX xs
pointsY :: [Point] -> [Double]
pointsY xs = map pointY xs

average :: [Double] -> Double
average xs = (sum xs) `div` (genericLength xs)

centroid cenpoint = (Pt average(pointsX cenpoint) average(pointsY cenpoint))

And I got

Project1.hs:35:22: error:
    • Couldn't match expected type ‘([Double] -> Double)
                                    -> [Double] -> Point’
                  with actual type ‘Point’
    • The function ‘Pt’ is applied to four arguments,
      but its type ‘Double -> Double -> Point’ has only two
      In the expression:
        (Pt average (pointsX cenpoint) average (pointsY cenpoint))
      In an equation for ‘centroid’:
          centroid cenpoint
            = (Pt average (pointsX cenpoint) average (pointsY cenpoint))
   |
35 | centroid cenpoint = (Pt average(pointsX cenpoint) average(pointsY cenpoint))
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Project1.hs:35:25: error:
    • Couldn't match expected type ‘Double’
                  with actual type ‘[Double] -> Double’
    • Probable cause: ‘average’ is applied to too few arguments
      In the first argument of ‘Pt’, namely ‘average’
      In the expression:
        (Pt average (pointsX cenpoint) average (pointsY cenpoint))
      In an equation for ‘centroid’:
          centroid cenpoint
            = (Pt average (pointsX cenpoint) average (pointsY cenpoint))
   |
35 | centroid cenpoint = (Pt average(pointsX cenpoint) average(pointsY cenpoint))
   |                         ^^^^^^^

Project1.hs:35:33: error:
    • Couldn't match expected type ‘Double’ with actual type ‘[Double]’
    • In the second argument of ‘Pt’, namely ‘(pointsX cenpoint)’
      In the expression:
        (Pt average (pointsX cenpoint) average (pointsY cenpoint))
      In an equation for ‘centroid’:
          centroid cenpoint
            = (Pt average (pointsX cenpoint) average (pointsY cenpoint))
   |
35 | centroid cenpoint = (Pt average(pointsX cenpoint) average(pointsY cenpoint))
   |
Will Ness
  • 70,110
  • 9
  • 98
  • 181
Lmmmmmmc
  • 87
  • 2
  • 1
    FYI, with record syntax, `data Point = Pt { pointX, pointY :: Double } deriving (Show, Eq)`, you get the two functions `pointX`, `pointY` for free. – Will Ness Oct 12 '19 at 10:01

3 Answers3

3

The main problem here is that you call the functions, like you would do in a language like Java, C++, or Python.

You need to wrap average into parenthesis, like:

centroid cenpoint = Pt (average (pointsX cenpoint)) (average (pointsY cenpoint))

As @dfeuer says, the outer parenthesis are not necessary here.

Furthermore you probably want to use (/) here over div, since you are working with Doubles, whereas div works on Integral types:

average :: [Double] -> Double
average xs = sum xs / genericLength xs

but like @leftroundabout says, we can better use length, and then use fromIntegral:

average :: [Double] -> Double
average xs = sum xs / fromIntegral (length xs)
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • I think it's worth mentioning that the outermost parentheses are unnecessary. – dfeuer Oct 11 '19 at 20:19
  • `genericLength` is controversial. I'd just use `fromIntegral (length xs)`. – leftaroundabout Oct 11 '19 at 23:14
  • @leftaroundabout why is `genericLength` controversial? I thought it was identical to `fromIntegral . length`, that is a version of `length` with a polymorphic return value (which really should have been used in the Prelude instead of the monomorphic `length` that's actually there). – Robin Zigmond Oct 12 '19 at 08:04
  • 1
    @RobinZigmond: I think because for a `Double` there are (large) values, for which `x + 1.0 == x`, since the mantissa can no has the "resolution" to increment with one. This thus means that for large lists, the `genericLength`, will "stop counting" at a given length. – Willem Van Onsem Oct 12 '19 at 08:07
  • 2
    @RobinZigmond yeah, that about `x + 1 ≡ x` is one concrete problem with floats in particular – but in case of `Double` it would only be an issue for _gargantuan_ lists. But more generally, it just doesn't make sense to carry out lots of additions in a custom number type, when `Int` is basically _always_ the sweet spot of sufficient size and guaranteed good performance. With `genericLength` it can easily happen that you do lots of additions in a really inefficient type, instead of doing them all in `Int` and only converting the result in the end. – leftaroundabout Oct 12 '19 at 09:23
  • Thanks for the explanations, I looked up the implementation and can see what you mean. Which of course begs the question of why it wasn't defined as `fromIntegral . length`. – Robin Zigmond Oct 12 '19 at 09:28
  • Nevermind, that's obvious when I think about it, it wouldn't work if the aim is to get an `Integer` or some other value whose bounds are less restrictive than those of `Int` – Robin Zigmond Oct 12 '19 at 09:37
3

You were doing pretty well - note how the compilation errors all point to your final line (the definition of centroid). And they're all due to incorrect bracketing. This is what it should be instead:

centroid cenpoint = Pt (average (pointsX cenpoint)) (average (pointsY cenpoint))

That is, the x co-ordinate of the resulting Pt value is the average of the x co-ordinates of the corresponding points (pointsX cenpoint is the list of x-coordinates, so average (pointsX cenpoint) is their average), and similarly for the y co-ordinate.

Comparing to your incorrect version (which I have tidied up a little without changing how it is interpreted by the compiler):

Pt average (pointsX cenpoint) average (pointsY cenpoint)

This means you apply the Pt function to 4 arguments: average, pointsX cenpoint, average again, and pointsY cenpoint. This simply doesn't work because the Pt function only takes 2 arguments.

Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34
1

Not really an answer to the question, but – this is the preferred way of doing it:

{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}

import Data.VectorSpace
import GHC.Generics

data Point = Pt Double Double
   deriving (Eq, Show, Generic, AdditiveGroup, VectorSpace)

average :: (VectorSpace v, Fractional (Scalar v)) => [v] -> v
average ps = sumV ps ^/ fromIntegral (length xs)

Then you can directly do

> average [Pt (-1.5) 0, Pt 3 2, Pt 0 1]
Pt 0.5 1.0

I.e. centroid ≡ average.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319