I'm reading Real World Haskell, and this is my first try with the language.
This is the result of the exercises of chapter 3:
- Consider three two-dimensional points a, b, and c. If we look at the angle formed by the line segment from a to b and the line segment from b to c, it either turns left, turns right, or forms a straight line. Define a Direction data type that lets you represent these possibilities.
- Write a function that calculates the turn made by three 2D points and returns a Direction.
- Define a function that takes a list of 2D points and computes the direction of each successive triple. Given a list of points [a,b,c,d,e], it should begin by computing the turn made by [a,b,c], then the turn made by [b,c,d], then [c,d,e]. Your function should return a list of Direction.
- Using the code from the preceding three exercises, implement Graham's scan algorithm for the convex hull of a set of 2D points. You can find good description of what a convex hull is, and how the Graham scan algorithm should work.
For short, the Graham scan algorithm is:
- find the point \$P\$ with the smallest \$y\$
- sort the points \$A\$ according to the angle \$(Ox, PA)\$ and add P at the end
- in the sorted list, form the turns \$(A_{i-1}, A_i, A_{i+1})\$ and remove the points \$A_i\$ for which it is a "turn right".
Now my code is the following:
import Data.List
-- 9
data Direction = TurnLeft
| TurnRight
| StraightLine
deriving (Show, Eq)
-- 10
data Point = Point {
x :: Float
, y :: Float
} deriving (Show, Eq)
turn :: Point -> Point -> Point -> Direction
turn a b c
| z > 0 = TurnLeft
| z < 0 = TurnRight
| otherwise = StraightLine
where
bax = x a - x b
bay = y a - y b -- coordinates of b->a
bcx = x c - x b
bcy = y c - y b -- coordinates of b->c
z = bax * bcy - bcx * bay
-- 11
turns :: [Point] -> [Direction]
turns [] = []
turns [_] = []
turns [_,_] = []
turns (x:y:z:zs) = (turn x y z) : (turns (y:z:zs))
-- 12
angle :: Point -> Point -> Point -> Float
angle a b c = - (bax * bcx + bay * bcy) /
(sqrt ((bax * bax + bay * bay) * (bcx * bcx + bcy * bcy)))
-- more efficient than acos for the algorithm and is the same for sorting
where
bax = x a - x b
bay = y a - y b -- coordinates of b->a
bcx = x c - x b
bcy = y c - y b -- coordinates of b->c
argmin :: Ord b => (a -> b) -> [a] -> a
argmin _ [] = error "empty list"
argmin f (x:xs)
| null xs = x
| (f x) < (f argminrest) = x
| otherwise = argminrest
where argminrest = argmin f xs
hull :: [Point] -> [Point]
hull pts =
origin : ((map snd (filter keepPt results)) ++ [origin])
where origin = argmin y pts
oxaxis = Point (1 + x origin) (y origin)
oxangle = angle oxaxis origin
compare_angle pt1 pt2 = compare (oxangle pt1) (oxangle pt2)
list = reverse (sortBy compare_angle pts)
-- origin is the first element of the list
results = zip (turns (list ++ [origin])) (drop 1 list)
keepPt (dir, _) = dir /= TurnRight
It works, at least according to the following unit test:
hull [(Point (-3) 1),(Point (-4) 1),(Point (-1) 4),(Point 0 0),(Point 2 2),(Point (-1) 3),(Point (-1) 2),(Point 1 0),(Point 3 (-1)),(Point (-1) (-1))]
which gives
[Point {x = -1.0, y = -1.0},Point {x = -3.0, y = 1.0},Point {x = -1.0, y = 2.0},Point {x = -1.0, y = 3.0},Point {x = -1.0, y = 4.0},Point {x = 2.0, y = 2.0},Point {x = 3.0, y = -1.0},Point {x = -1.0, y = -1.0}]
Is it an idiomatic code? How would you improve it?