The convex hull is the smallest set of points that contain all the other points in the set. The image below from Wikipedia is a much better visual description:

One approach for finding the convex hull is the Graham Scan algorithm and this is the challenge presented in the book. The algorithm can be broken down into the following steps:

- Find the minimum point (with the lowest Y co-ordinate). In the event of a tie, the lowest x co-ordinate determines the minimum. Call this point P.
- Sort the remaining points relative to the angle formed by point and P relative to the x-axis.
- Consider each point in the sequence. For each point, determine whether it is a left turn or a right turn relative to the previous two points. If it is a "right turn" the second to last point is not in the convex hull. Continue until the list is finished.

To solve this, we'll declare some data structures which should be self explanatory.

data Direction = Straight

| LeftTurn

| RightTurn

deriving (Show)

data Point = Point Double Double

deriving (Show,Eq)

Next we define a function that calculates the turn made by three points and returns a Direction. We don't need to calculate the actual angle as the cotangent is proportional to the angle.

cross :: Point -> Point -> Point -> Double

cross (Point x1 y1) (Point x2 y2) (Point x3 y3) = ((x2-x1)*(y3-y1)-(x3-x1)*(y2-y1))

dist :: Point -> Point -> Double

dist (Point x1 y1) (Point x2 y2) = sqrt((x1-x2)^2 + (y1-y2)^2)

turn :: Point -> Point -> Point -> Direction

turn a b c = makeDirection (cross a b c) where

makeDirection x | x == 0 = Straight

| x < 0 = LeftTurn

| x > 0 = RightTurn

In order to find the minimum Point and sort the list we need to define two comparator functions for use with sortBy and minimumBy (both members of Data.List).

compareYPoint :: Point -> Point -> Ordering

compareYPoint (Point x1 y1) (Point x2 y2)

| y1 == y2 = compare x1 x2

| y1 <= y2 = LT

| otherwise = GT

compareCross :: Point -> Point -> Point -> Ordering

compareCross pvt a b = if (angle == EQ) then distance else angle where

angle = compare (cross pvt a b) 0

distance = compare (dist pvt a) (dist pvt b)

Even though compareCross takes 3 arguments, we can still use this as type

`Point -> Point -> Ordering`

by using currying.

lowestY :: [Point] -> Point

lowestY = minimumBy compareYPoint

grahamScan :: [Point] -> [Point]

grahamScan points = undefined where

p = nub points

pvt = lowestY p

sortedPoints = pvt : (sortBy (compareCross pvt) (delete pvt p))

`nub`

is a function that removes duplicate elements from a list. This gives the skeleton of the program. Now we have to do the complicated bit. From Wikipedia: The algorithm proceeds by considering each of the points in the sorted array in sequence. For each point, it is determined whether moving from the two previously considered points to this point is a "left turn" or a "right turn". If it is a "right turn", this means that the second-to-last point is not part of the convex hull and should be removed from consideration. This process is continued for as long as the set of the last three points is a "right turn". As soon as a "left turn" is encountered, the algorithm moves on to the next point in the sorted array

Let's take the simplest example possible. For the following points (0,0), (2,0), (2,2), (0,2), (1,1) the convex hull should be everything apart from (1,1).

In order to check how this is going to work, we need to define a quick helper function to make building lists of Point types less verbose.

pointsFromTupleList :: [(Double,Double)] -> [Point]

pointsFromTupleList = map (\(x,y) -> Point x y)

examplePoints :: [Point]

examplePoints = pointsFromTupleList [(0,0),(2,0),(2,2),(0,2),(1,1)]

Now we can explore the functions that we're going to use and verify they work

-- The minimum point appears correct

*Main> lowestY examplePoints

Point 0.0 0.0

-- The points are sorted as expected

*Main> let sortedPoints = (Point 0 0):(sortBy (compareCross (Point 0 0)) (delete (Point 0 0) examplePoints))

*Main>sortedPoints

[Point 0.0 0.0,Point 0.0 2.0,Point 1.0 1.0,Point 2.0 2.0,Point 2.0 0.0]

The next task is to calculate the turns made.

*Main> getTurns sortedPoints

[LeftTurn,RightTurn,LeftTurn]

That's not much use because I don't know the points which constitute the turns. Let's tuple up the results:

getTurns :: [Point] -> [((Point,Point,Point),Direction)]

getTurns (x:y:z:ps) = ((x,y,z),turn x y z) : getTurns (y:z:ps)

getTurns _ = []

*Main> getTurns sortedPoints

[((Point 0.0 0.0,Point 0.0 2.0,Point 1.0 1.0),LeftTurn),

((Point 0.0 2.0,Point 1.0 1.0,Point 2.0 2.0),RightTurn),

((Point 1.0 1.0,Point 2.0 2.0,Point 2.0 0.0),LeftTurn)]

The algorithm states that I should ignore the second to last parameter if it is a right turn, and keep it otherwise. Clearly we're close here because the second to last parameter for a right turn is the only point not in the convex hull. One problem with the sortedPoints is that we're not returning to where we started. Let's change the definition again so that we can get back to where we started. We add two nodes at the end so that the extra padding means we consider all triplets

sortedPoints = pvt : (sortBy (compareCross pvt) (delete pvt p)) ++ [pvt,pvt]

*Main> getTurns sortedPoints

[((Point 0.0 0.0,Point 0.0 2.0,Point 1.0 1.0),LeftTurn),

((Point 0.0 2.0,Point 1.0 1.0,Point 2.0 2.0),RightTurn),

((Point 1.0 1.0,Point 2.0 2.0,Point 2.0 0.0),LeftTurn),

((Point 2.0 2.0,Point 2.0 0.0,Point 0.0 0.0),LeftTurn),

((Point 2.0 0.0,Point 0.0 0.0,Point 0.0 0.0),Straight)]

So now all I need to do to solve the problem is keep the middle point in all cases where we've got a LeftTurn or Straight. I can simplify this further by just only getting the middle point in the tuple.

getTurns :: [Point] -> [Point,Direction)]

getTurns (x:y:z:ps) = (y,turn x y z) : getTurns (y:z:ps)

getTurns _ = []

grahamScan :: [Point] -> [Point]

grahamScan points = map fst (filter (\(x,d) -> d /= RightTurn) (getTurns sortedPoints)) where

p = nub points

pvt = lowestY p

sortedPoints = pvt : (sortBy (compareCross pvt) (delete pvt p)) ++ [pvt,pvt]

-- Hurrah, it finally works (for this one example).

*Main> grahamScan examplePoints

[Point 0.0 2.0,Point 2.0 2.0,Point 2.0 0.0,Point 0.0 0.0]

One example works, that'll do me as I've learnt enough from this exercise to make it worthwhile. I strongly suspect the way I'm extending the point list by appending the pivot twice at the end is just a hack rather than the right way. There's another definition here which looks cleaner, but that apparently has some issues too.

The complete code looks like this. hlint was run on this and pointed out the "uncurry" function which converts a curried function to a function on pairs. This simplifies the definition of

`pointsFromTupleList`

.

data Direction = Straight

| LeftTurn

| RightTurn

deriving (Show,Eq)

data Point = Point Double Double

deriving (Show,Eq)

turn :: Point -> Point -> Point -> Direction

turn a b c = makeDirection (cross a b c) where

makeDirection x | x == 0 = Straight

| x < 0 = LeftTurn

| x > 0 = RightTurn

cross :: Point -> Point -> Point -> Double

cross (Point x1 y1) (Point x2 y2) (Point x3 y3) = (x2-x1)*(y3-y1)-(x3-x1)*(y2-y1)

dist :: Point -> Point -> Double

dist (Point x1 y1) (Point x2 y2) = sqrt((x1-x2)^2 + (y1-y2)^2)

compareCross :: Point -> Point -> Point -> Ordering

compareCross pvt a b = if angle == EQ then distance else angle where

angle = compare (cross pvt a b) 0

distance = compare (dist pvt a) (dist pvt b)

getTurns :: [Point] -> [(Point,Direction)]

getTurns (x:y:z:ps) = (y,turn x y z) : getTurns (y:z:ps)

getTurns _ = []

grahamScan :: [Point] -> [Point]

grahamScan points = map fst (filter (\(x,d) -> d /= RightTurn) (getTurns sortedPoints)) where

p = nub points

pvt = lowestY p

sortedPoints = pvt : sortBy (compareCross pvt) (delete pvt p) ++ [pvt,pvt]

compareYPoint :: Point -> Point -> Ordering

compareYPoint (Point x1 y1) (Point x2 y2)

| y1 == y2 = compare x1 x2

| y1 <= y2 = LT

| otherwise = GT

compareAngle :: Point -> Point -> Point -> Ordering

compareAngle (Point px py) p1 p2 = compare (angle p2) (angle p1) where

angle (Point x1 y1) = y1-py / x1-px

lowestY :: [Point] -> Point

lowestY = minimumBy compareYPoint

pointsFromTupleList :: [(Double,Double)] -> [Point]

pointsFromTupleList = map (uncurry Point)

examplePoints :: [Point]

examplePoints = pointsFromTupleList [(0,0),(2,0),(2,2),(0,2),(1,1)]