Representing Vectors and Points :: Why doesn't Lens work here?

Hi,

I want to build a 3d graphics engine a la: but in haskell.

TL:DR; I cannot define addVecToPoint with lenses:

addVecToPoint :: Num a => Point a -> Point a -> Point a
addVecToPoint p1@(Point3D {}) = over' xLens (+ x p1) . over' yLens (+ y p1) . over' zLens (+ z p1) -- gives wrong answers

Right now I’m on this page with making Vectors.

I have Points defined like so:

import Control.Lens ( view, over, Identity(..) )
import Control.Lens.Fold
import Data.Functor.Identity

data Point a = Point2D {x :: a, y :: a}
                    | Point3D {x :: a, y :: a, z :: a}
    deriving Show

instance Functor Point where
    fmap :: (a -> b) -> Point a -> Point b
    fmap f (Point2D x y) = Point2D (f x) (f y)
    fmap f (Point3D x y z) = Point3D (f x) (f y) (f z)

newtype Vector a = Vector {runVector :: Point a -> Point a}

And some auxilliary functions like so:

getVec :: Num a => Point a -> Point a -> Vector a
getVec from = mkVector . addVecToPoint from . fmap negate

addVecs :: Vector a -> Vector a -> Vector a
addVecs (Vector f) (Vector g) = Vector (f . g)

origin3D = Point3D 0 0 0

I made lenses and traversals like so:

type MyLens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type MyLens' s a = MyLens s s a a
type MyGetter s a = forall f. Functor f => (a -> f a) -> s -> f s
type MyTraversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
type MySetter s t a b = forall f. Applicative f => (a -> f b) -> s -> f t

newtype MyConst a b = MyConst {getMyConst :: a}

instance Functor (MyConst a) where
    fmap _ (MyConst a) = MyConst a

lens' :: (s -> a) -> (s -> a -> s) -> MyLens' s a
lens' getterf setterf f s = setterf s <$> f (getterf s)

view' :: MyLens' s a -> s -> a
view' l s = let fa = l MyConst s
             in getMyConst fa

mySet' :: MyLens' s a -> s -> a -> s
mySet' l s val = let fa = l (const (Identity val)) s
                  in runIdentity fa

over' :: MyLens' s a -> (a -> a) -> s -> s
over' l f s = let fa = l (fmap f . Identity) s
               in runIdentity fa

pointCoords :: MyTraversal (Point a) (Point a) a a --  Traversal for points
pointCoords g (Point2D x y) = Point2D <$> g x <*> g y
pointCoords g (Point3D x y z) = Point3D <$> g x <*> g y <*> g z


xLens, yLens, zLens :: MyGetter (Point a) a
xLens g p = (\val -> p {x = val}) <$> g (x p)
yLens g p = (\val -> p {y = val}) <$> g (y p)
zLens g p = (\val -> p {y = val}) <$> g (z p)
...

but I can’t use lenses to do it

addVecToPoint :: Num a => Point a -> Point a -> Point a
addVecToPoint p1@(Point3D {}) = over' xLens (+ x p1) . over' yLens (+ y p1) . over' zLens (+ z p1) -- gives wrong answers

because my tests return:

myTest = do

    let p1 = Point3D 1 2 1

        p2 = Point3D 0 4 4

        v1 = mkVector $ Point3D 2 0 0

        v2 = getVec p1 p2 -- Gets the vector p1 -> p2

        v3 = addVecs v1 v2 -- Adds v1 to v2

    shouldBe (Point3D 1 2 1) p1

    shouldBe (Point3D 0 4 4) p2

    shouldBe (Point3D 2 0 0) (runVector v1 origin3D)

    let showV2 = runVector v2 origin3D -- views the v2 vector which is p1 -> p2

        showV3 = runVector v3 origin3D -- view v3 which is v1 + v2

        drawp1 = runVector v3 p1 -- applies v3 to p1

        -- drawp2 = runVector (invert v2) p2 -- applies (inverse v2) to p2

    zipWithM_ shouldBe

             

              [ Point3D 2 0 0

              , Point3D 3 (-2) (-3)

              , Point3D 4 0 (-2)

            --   , Point3D (-1) 6 7

              ]

             

              [ showV2

              , showV3

              , drawp1

            --   , drawp2 -- not working :(

              ]
ghci> myTest 
Should be: Point3D {x = 1, y = 2, z = 1} -- and is: Point3D {x = 1, y = 2, z = 1}
Should be: Point3D {x = 0, y = 4, z = 4} -- and is: Point3D {x = 0, y = 4, z = 4}
Should be: Point3D {x = 2, y = 0, z = 0} -- and is: Point3D {x = 2, y = 0, z = 0}
Should be: Point3D {x = 1, y = -2, z = -3} -- and is: Point3D {x = 1, y = -2, z = -3}
Should be: Point3D {x = 3, y = -2, z = -3} -- and is: Point3D {x = 3, y = 0, z = 0} -- very wrong
Should be: Point3D {x = 4, y = 0, z = -2} -- and is: Point3D {x = 4, y = 1, z = 1} -- also wrong

so now I define it like:

addVecToPoint :: Num a => Point a -> Point a -> Point a
addVecToPoint p1@(Point3D {}) p2@(Point3D {}) = Point3D (x p1 + x p2) (y p1 + y p2) (z p1 + z p2)
addVecToPoint p1@(Point2D {}) p2@(Point2D {}) = Point2D (x p1 + x p2) (y p1 + y p2)
addVecToPoint _ _ = error "Not possible!"

this gives:

ghci> myTest 
Should be: Point3D {x = 1, y = 2, z = 1} -- and is: Point3D {x = 1, y = 2, z = 1}
Should be: Point3D {x = 0, y = 4, z = 4} -- and is: Point3D {x = 0, y = 4, z = 4}
Should be: Point3D {x = 2, y = 0, z = 0} -- and is: Point3D {x = 2, y = 0, z = 0}
Should be: Point3D {x = 1, y = -2, z = -3} -- and is: Point3D {x = 1, y = -2, z = -3}
Should be: Point3D {x = 3, y = -2, z = -3} -- and is: Point3D {x = 3, y = -2, z = -3}
Should be: Point3D {x = 4, y = 0, z = -2} -- and is: Point3D {x = 4, y = 0, z = -2}

I’m a bit stumped as to why lenses can’t work here.

Have you tried using the standard lens functions instead of defining most of it yourself?

Also, can you make the tests simpler? It is quite a lot and there are still some undefined parts like getVec, mkVector, and origin3D.

Yeah, it doesn’t work with the standard lens functions sadly :frowning:. I did it because I wanted to learn how lenses work but it should still work with the standard lens but the same behaviour occurs :frowning:

I’ll add the missing parts:

getVec :: Num a => Point a -> Point a -> Vector a
getVec from = mkVector . addVecToPoint from . fmap negate

addVecs :: Vector a -> Vector a -> Vector a
addVecs (Vector f) (Vector g) = Vector (f . g)

origin3D = Point3D 0 0 0

Probably this line?

zLens g p = (\val -> p {y = val}) <$> g (z p)

Note that the simple correction won’t work here.

ghci> :{
ghci| zLens :: Functor f => (a -> f a) -> Point a -> f (Point a)
ghci| zLens g p = (\val -> p {z = val}) <$> g (z p)
ghci| :}
ghci> zLens (\z -> pure (z + 1)) (Point2D 0 0)
*** Exception: <interactive>:12:22-32: Non-exhaustive patterns in case

There has to be a preparation for when you don’t have z field! What do you want it to do for Point2D?

1 Like

Ah man :’) it’s the simple things…

Thank you so much. I’ve changed it and it’s working. You’re a life-saver mate :’)

At the moment, I’m just going to handle it with error and build in something better later on if needed.

zLens g p@Point3D {} = (\val -> p {z = val}) <$> g (z p)
zLens _ _ = error "Trying to access illegal value."

You can have lawful lenses only for product types (types with only one constructor)*. Your Point a is a sum type. Consider using zLens to get z value from Point2D x y. What should happen? Perhaps it should return 0. Next consider using zLens to set new value of z coordinate on Point2D x y. What should happen in this case? Just ignoring z value and leaving Point2D x y unchanged violates this lens law:

get lens (set lens a s) == a (you should get back what you’ve just set).

So perhaps it should turn Point2D x y into Point3D x y z? But then you are violating this law:

set lens (get lens s) s == s (setting the same value you’ve just got doesn’t change anything)

because you’ve turned Point2D x y into Point3D x y 0. You can counter this with tweaking the implementation of Eq for Point a by making Point2D x y and Point3D x y 0 equal, but then, in the most popular use case of a being Float or Double, equality comparison doesn’t really work that well.

Anyway, I think we painted ourselves into the corner. Just don’t use lenses for sum types.

(*) Technically, they can be made to work if the type has 2 (or more) ways of representing exactly the same thing, like Coordinates a = Cartesian a a | Polar a a, but even this example is problematic with respect to Cartesian 0 0.

Hi,

Thanks for the response.

Is pattern matching and using the lenses for different structures a valid strategy?

Currently, I’m making addition and substitution functions for Point2D and Point3D using pattern matching and wildcard _ to catch illegal point combinations.

At this point, it’s upto me to correctly use the correct functions where necessary, and the program does reliably fail if I use a 3D function on a 2D point.

I’ll copy in an example when I get home from work.

Personally, I think that trying to do arithmetic between points of different dimensions is bad idea. I would have two separate types Point2D and Point3D, define arithmetic operations for each of them (but monomorphically), and then maybe have sum type Point containing either one of them, but without defining arithmetic on Point type.

Another thing is that representing vectors as functions acting on points, while tempting, might also turn out to not be a good idea. Function can do to a point much more than just apply constant translation. And functions are opaque, you can’t see what’s inside (OK, in case of a vector you can recover numeric values of translation by just applying it to point at the origin, but still).

1 Like

By the way, I’d recommend looking at the linear package. It defines pretty elegant lenses for vectors and matrices.

3 Likes

Hi,

I took your advice in terms of representing vectors as functions and changed that to a newtype. This is much easier to work with, just as effective and much more transparent :slight_smile:

newtype Vector a = Vector { getVector :: Point a }
    deriving Show

Regarding them being separate types, I’m starting to agree because while in practice it shouldn’t be done, it still allows for illegal points to work on each other; so I’ll rewrite that over the weekend

Hi,

thanks for this! I’ll take a look through. Part of the reason I’m doing this is to learn linear maths since I haven’t done much maths in 10 years lol so I’m glad I can reference that package for inspiration and hopefully use it in a project one day.

linear actually helped me learn a little bit more about linear algebra fwiw! I took a course like a decade ago but it’s mostly left my brain, so having a Haskell library to guide me was nice.

3 Likes