patternMinor
Graham Scan convex hull algorithm
Viewed 0 times
convexgrahamscanhullalgorithm
Problem
I'm beginning to learn Haskell. I've implemented the Graham Scan algorithm for detection of convex hull following the Real World Haskell book.
I'm looking for general advice regarding the style and convention of my code, as well as best practices and ways to refactor several ugly places:
-
Point-free usage (or possibility of it) in following places:
-
Graham Scan implementation as two-level function — interface one and internal one. Maybe there's a way to merge them?
```
import Prelude hiding (Left, Right)
import Data.List
import Test.Framework (defaultMain, testGroup)
import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.QuickCheck
import Test.HUnit
data Direction = Left
| Right
| Straight
deriving (Show, Eq)
data Point2D = Point2D { x :: Integer
, y :: Integer
} deriving (Show, Eq, Ord)
direction :: Point2D -> Point2D -> Point2D -> Direction
direction a b c =
let x1 = x a
x2 = x b
x3 = x c
y1 = y a
y2 = y b
y3 = y c
s = (x2 - x1) (y3 - y1) - (y2 - y1) (x3 - x1)
in case compare s 0 of
GT -> Left
LT -> Right
EQ -> Straight
comparePoints :: Point2D -> Point2D -> Ordering
comparePoints a b
| y1 x2 = GT
| y1 > y2 = GT
where x1 = x a
x2 = x b
y1 = y a
y2 = y b
sortPoints :: [Point2D] -> [Point2D]
sortPoints l = sortBy comparePo
I'm looking for general advice regarding the style and convention of my code, as well as best practices and ways to refactor several ugly places:
Vector2Dand its accessors. It's structurally equivalent toPoint2Dbut I want to typecheck its usage. Hence I usenewtypeand nottype, but it makes me implement custom accessors to unwrap the underlyingPoint2D. The nesting looks redundant.
-
Point-free usage (or possibility of it) in following places:
sqrt . fromIntegral $ (vectorX v) ^ 2 + (vectorY v) ^ 2sortBy (\ (_,b1) (_,b2) -> (b1 :: Double) ``compare`` (b2 :: Double))
(zip l (angleWithXByPoint2DList p l))-
Graham Scan implementation as two-level function — interface one and internal one. Maybe there's a way to merge them?
```
import Prelude hiding (Left, Right)
import Data.List
import Test.Framework (defaultMain, testGroup)
import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.QuickCheck
import Test.HUnit
data Direction = Left
| Right
| Straight
deriving (Show, Eq)
data Point2D = Point2D { x :: Integer
, y :: Integer
} deriving (Show, Eq, Ord)
direction :: Point2D -> Point2D -> Point2D -> Direction
direction a b c =
let x1 = x a
x2 = x b
x3 = x c
y1 = y a
y2 = y b
y3 = y c
s = (x2 - x1) (y3 - y1) - (y2 - y1) (x3 - x1)
in case compare s 0 of
GT -> Left
LT -> Right
EQ -> Straight
comparePoints :: Point2D -> Point2D -> Ordering
comparePoints a b
| y1 x2 = GT
| y1 > y2 = GT
where x1 = x a
x2 = x b
y1 = y a
y2 = y b
sortPoints :: [Point2D] -> [Point2D]
sortPoints l = sortBy comparePo
Solution
Some ideas:
-
When comparing points, you can use the fact that
-
I strongly encourage to keep this separation. The (un)wrapping can be somewhat avoided by defining all required operations and then use only those, hiding the internal representation. Actually, I'd make them completely distinct (saves one constructor) and instead define their mathematical relationship. The vector-space library provides the proper type classes:
-
Point-free usage (or possibility of it) in following places:
It's possible to convert any term into the point-free notation, but in some cases it makes things actually worse. Like in those where a variable is repeated: Surely
Function
-
It seems to me that
using that we can make
-
Graham Scan implementation as two-level function — interface one and internal one. Maybe there's a way to merge them?
On the contrary, I'd recommend keeping them split. Splitting code into more smaller functions is usually better than having one big complex one.
-
Instead of having
I'd strongly suggest using
Both
Moreover,
-
When comparing points, you can use the fact that
(,) is lexicographically ordered:comparePoints :: Point2D -> Point2D -> Ordering
comparePoints a b = compare (y a, x a) (y b, x b)-
Vector2D and it's accessors. It's structurally equivalent to Point2D but I want to typecheck its usage. Hence I use newtype and not type, but it makes me implement custom accessors to unwrap the underlying Point2D. The nesting looks redundant.I strongly encourage to keep this separation. The (un)wrapping can be somewhat avoided by defining all required operations and then use only those, hiding the internal representation. Actually, I'd make them completely distinct (saves one constructor) and instead define their mathematical relationship. The vector-space library provides the proper type classes:
{-# LANGUAGE TypeFamilies #-}
import Data.AffineSpace
import Data.VectorSpace
data Vector2D = Vector2D { x :: Integer
, y :: Integer
} deriving (Show, Eq, Ord)
instance AdditiveGroup Vector2D where
zeroV = Vector2D 0 0
(Vector2D x1 y1) ^+^ (Vector2D x2 y2)
= Vector2D (x1+x2) (y1+y2)
negateV (Vector2D x1 y1)
= Vector2D (-x1) (-y1)
instance VectorSpace Vector2D where
type Scalar Vector2D = Integer
k *^ (Vector2D x1 y1)
= Vector2D (k*x1) (k*y1)
instance InnerSpace Vector2D where
(Vector2D x1 y1) (Vector2D x2 y2) = x1*x2 + y1*y2
euclideanNorm2D :: Vector2D -> Double
euclideanNorm2D = sqrt . fromIntegral . magnitudeSq
data Point2D = Point2D { xv :: Integer
, yv :: Integer
} deriving (Show, Eq, Ord)
instance AffineSpace Point2D where
type Diff Point2D = Vector2D
(Point2D x1 y1) .-. (Point2D x2 y2)
= Vector2D (x2 - x1) (y2 - y1)
(Point2D x1 y1) .+^ (Vector2D x y)
= Point2D (x1 + x) (y1 + y)-
Point-free usage (or possibility of it) in following places:
It's possible to convert any term into the point-free notation, but in some cases it makes things actually worse. Like in those where a variable is repeated: Surely
\x -> x x is more readable than (() id). Anyway, using magnitudeSq from vector-space we can make euclideanNorm2D point-free (see above).Function
sortedPointsByAngleWithPX can be simplified using on from Data.Function:sortedPointsByAngleWithPX p l =
sortBy (on compare snd) (zip l (angleWithXByPoint2DList p l))-
It seems to me that
angleWithXByPoint2DList can be simplified asangleWithXByPoint2DList :: Point2D -> [Point2D] -> [Double]
angleWithXByPoint2DList p = map (angleWithXBy2Points2D p)using that we can make
sortedPointsByAngleWithPX partially point-free using (&&&) from Control.Arrow, but I have doubts if it's really useful (for me readability is more important):sortedPointsByAngleWithPX p =
sortBy (on compare snd) . map (id &&& angleWithXBy2Points2D p)-
Graham Scan implementation as two-level function — interface one and internal one. Maybe there's a way to merge them?
On the contrary, I'd recommend keeping them split. Splitting code into more smaller functions is usually better than having one big complex one.
-
Instead of having
grahamScanInternal acc l =
let ...
c = head l
...
... (tail l)I'd strongly suggest using
grahamScanInternal acc (c:cs) =
let ...
...
... csBoth
head and tail are partial functions and can be source of exceptions when used accidentally on the empty list. Pattern matching instead makes it clear that it can't happen.Moreover,
grahamScanInternal can be rewritten as a fold, which makes its design slightly more clear:grahamScanInternal :: [Point2D] -> [Point2D] -> [Point2D]
grahamScanInternal = foldl f
where
f acc c | (direction a b c) == Right = init acc ++ [c]
| otherwise = acc ++ [c]
where
b = last acc
a = last (init acc)Code Snippets
comparePoints :: Point2D -> Point2D -> Ordering
comparePoints a b = compare (y a, x a) (y b, x b){-# LANGUAGE TypeFamilies #-}
import Data.AffineSpace
import Data.VectorSpace
data Vector2D = Vector2D { x :: Integer
, y :: Integer
} deriving (Show, Eq, Ord)
instance AdditiveGroup Vector2D where
zeroV = Vector2D 0 0
(Vector2D x1 y1) ^+^ (Vector2D x2 y2)
= Vector2D (x1+x2) (y1+y2)
negateV (Vector2D x1 y1)
= Vector2D (-x1) (-y1)
instance VectorSpace Vector2D where
type Scalar Vector2D = Integer
k *^ (Vector2D x1 y1)
= Vector2D (k*x1) (k*y1)
instance InnerSpace Vector2D where
(Vector2D x1 y1) <.> (Vector2D x2 y2) = x1*x2 + y1*y2
euclideanNorm2D :: Vector2D -> Double
euclideanNorm2D = sqrt . fromIntegral . magnitudeSq
data Point2D = Point2D { xv :: Integer
, yv :: Integer
} deriving (Show, Eq, Ord)
instance AffineSpace Point2D where
type Diff Point2D = Vector2D
(Point2D x1 y1) .-. (Point2D x2 y2)
= Vector2D (x2 - x1) (y2 - y1)
(Point2D x1 y1) .+^ (Vector2D x y)
= Point2D (x1 + x) (y1 + y)sortedPointsByAngleWithPX p l =
sortBy (on compare snd) (zip l (angleWithXByPoint2DList p l))angleWithXByPoint2DList :: Point2D -> [Point2D] -> [Double]
angleWithXByPoint2DList p = map (angleWithXBy2Points2D p)sortedPointsByAngleWithPX p =
sortBy (on compare snd) . map (id &&& angleWithXBy2Points2D p)Context
StackExchange Code Review Q#31285, answer score: 8
Revisions (0)
No revisions yet.