May 2020
Try to keep the set of primitive operations as small as possible!
type Shape
-- Constructor functions
empty :: Shape
disc :: Shape -- at origin, unit radius
square :: Shape -- at origin, unit size
-- Combinators
translate :: Vec -> Shape -> Shape
scale :: Vec -> Shape -> Shape
rotate :: Angle -> Shape -> Shape
union :: Shape -> Shape -> Shape
intersect :: Shape -> Shape -> Shape
difference :: Shape -> Shape -> Shape
-- Run function
inside :: Point -> Shape -> Bool
invert :: Shape -> Shape
transform :: Matrix -> Shape -> Shape
-- derived transformations;
-- recall sets and linear algebra
scale :: Vec -> Shape -> Shape
scale v = transform (matrix (vecX v) 0
0 (vecY v))
rotate :: Angle -> Shape -> Shape
rotate a = transform (matrix (cos a) (-sin a)
(sin a) (cos a))
difference :: Shape -> Shape -> Shape
difference a b = a `intersect` invert b
type Matrix
type Vec
type Point = Vec -- represented as vectors
-- Constructor functions
vec :: Double -> Double -> Vec
matrix :: Double -> Double ->
Double -> Double -> Matrix
-- Combinator functions
mul :: Matrix -> Vec -> Vec
inv :: Matrix -> Matrix
sub :: Vec -> Vec -> Vec
-- Run functions
vecX, vecY :: Vec -> Double
Could add more operations, but this is enough for our purpose.
Shallow embedding
Deep embedding
…or something in between.
What observations can we make of shapes?
Hence: we can implement shapes as boolean functions.
(Not so easy in general: you might have to generalize the type of the run function to get a compositional shallow embedding.)
If we choose the right embedding, the operations should be easy to implement.
empty = Shape $ \_ -> False
disc = Shape $ \p -> (vecX p)^2+(vecY p)^2 <= 1
square = Shape $ \p -> abs (vecX p) <= 1 &&
abs (vecY p) <= 1
union a b = Shape $ \p -> inside p a ||
inside p b
intersect a b = Shape $ \p -> inside p a &&
inside p b
invert a = Shape $ \p -> not (inside p a)
transform m a
= Shape $ \p -> (m'`mul`p) `inside` a
where m' = inv m -- inverse matrix
translate v a
= Shape $ \p -> (p`sub`v) `inside` a
Note that in both cases we modify the point rather than shape.
Just a datatype of the primitive operations:
data Shape = Empty | Disc | Square
| Translate Vec Shape
| Transform Matrix Shape
| Union Shape Shape
| Intersect Shape Shape
| Invert Shape
empty = Empty
disc = Disc
square = Square
translate = Translate
...
Using generalized algebraic data type (GADT) syntax:
data Shape where
-- Constructor functions
Empty :: Shape
Disc :: Shape
Square :: Shape
-- Combinators
Translate :: Vec -> Shape -> Shape
Transform :: Matrix -> Shape -> Shape
Union :: Shape -> Shape -> Shape
Intersect :: Shape -> Shape -> Shape
Invert :: Shape -> Shape
All work happens in the run function.
inside :: Point -> Shape -> Bool
p `inside` Empty = False
p `ìnside` Disc = (vecX p)^2+(vecY p)^2<=1
p `inside` Square= abs (vecX p)<=1 &&
abs (vecY p)<=1
(continues next slide)
(continued from previous slide)
p `inside` Translate v a
= (p`sub`v) `inside` a
p `inside` Transform m a
= mul (inv m) p `inside` a
p `inside` Union a b
= p `inside` a || p `inside` b
p `inside` Intersect a b
= p `inside` a && p `inside` b
p `inside` Invert a = not (p `inside` a)
module Shape
( module Matrix -- re-export matrix library
, Shape -- don't export the implementation
, empty, disc, square
, translate, transform, scale, rotate
, union, intersect, difference, invert
, inside)
where
import Matrix
...
The interface is the same for shallow and deep embedding — no visible difference to the user!