DEV Community

James Green
James Green

Posted on

Raytracing Part I.V: An embarrassing exploration

🕵️ TL;DR - Cheating or ignorance, anytime someone in your life tries to brush something under the rug, they're probably hiding something. In our case, not intentionally.


Well this is embarrassing. Whilst at university, I discovered that someone's use of the word trivially, correlated massively to their lack of understanding on a specific topic. When I discovered this (with a lot of self-realisation in the process), I tried to change the way I spoke to avoid this pitfall. Very often in proofs and explanations, a closer look at a misplaced trivially or clearly, showed a fundamental misunderstanding of the writers knowledge. I have no shame in admitting I fell into exactly this trap in the last part of this series.

Specifically, and sadly, my favourite line in the last article 'But then we'd have to worry about defining map and sum for Vector3 and suddenly I've lost interest.' showed pure ignorance on my part. Today we're going to look at typeclasses and hopefully develop some cleaner code at the end of it.


We were about to talk about colours at the end of the last article, and the first thing we have to realise is that a colour is simply a Vector3. Now our immediate thought should be 'how do we avoid re-defining the functions we just defined?'. The answer to this is typeclasses.

This next part took longer than I'd like to admit; I learnt a huge amount about typeclasses here. I think moving forward this'll be a part we re-visit, probably once we reach monoids / monads.

We build upon this StackOverflow article.

class NumVector v where
  (>+) :: Num a => v a -> v a -> v a
  (>-) :: Num a => v a -> v a -> v a
  prod :: Num a => v a -> v a -> v a
  (>*) :: Num a => a -> v a -> v a
  (>.) :: Num a => v a -> v a -> a
  neg  :: Num a => v a -> v a

data Vector a = Vec { vecList :: [a] }
              deriving (Show, Eq, Ord)

instance NumVector Vector where
  (>+) (Vec u) (Vec v) = Vec $ zipWith (+) u v
  (>-) (Vec u) (Vec v) = Vec $ zipWith (-) u v
  (>*) k (Vec v) = Vec $ map (k*) v
  prod (Vec u) (Vec v) = Vec $ zipWith (*) u v
  (>.) u v = sum $ vecList (u `prod` v)
  neg = (>*) (-1)

cross :: Num a => Vector a -> Vector a -> Vector a
cross (Vec [a,b,c]) (Vec [x,y,z]) = Vec [b*z + c*y, -(a*z + c*x), a*y + b*x]
Enter fullscreen mode Exit fullscreen mode

There's a lot to unpack here (no pun intended). Firstly, we define a typeclass; this tells us that an instance of NumVector must implement: addition, subtraction, multiplication and the dot product. Why not the cross product? I hear you scream, well it turns out the n-dimensional cross product is kinda hard.

Then we define a vector, we define this as a list. There's a few questions here around different length vectors which we need to consider. Take for example:

okay = Vec [1,2,3] >. Vec [1,2,3]
wtf = Vec [1,2,3] >. Vec [1,2,3,4,5,6]

main = do
    print okay -- Outputs: 14
    print wtf  -- Outputs: 14
Enter fullscreen mode Exit fullscreen mode

Clearly this is whacky, the dot product isn't well defined for vectors of differing lengths, let alone being the answer above! For the time being, let's treat this like climate change and bury our head in the sand. zing! I do commentary on society now too.

Then we define our operations we promised for types that instanciate NumVector, this is all pretty standard. Finally we define the cross product, but only for 3-dimensional vectors.

If we try, for example:

main = do
    print $ Vec [1,2,3] `cross` Vec [1,2,3]   -- Vec {vecList = [12,-6,4]}
    print $ Vec [1,2,3] `cross` Vec [1,2,3,4] -- main: main.hs:18:1-76: Non-exhaustive patterns in function cross
Enter fullscreen mode Exit fullscreen mode

we get told that we haven't defined cross for 4-dimensional vectors.

Unfortunately, my knowledge runs out here. I know there's a sensible way to define colours that 'inherit' from vectors; I've spent enough time here that I think I need to move on and come back to it. Thus we will define the functions for colours in terms of Vec.

clamp :: (Num a, Ord a) => Vector a -> Vector a
clamp (Vec u) = Vec $ map (min 1) $ map (max 0) u

main = print $ clamp (Vec [0.1,-100,4.5]) -- Outputs: Vec {vecList = [0.1,0.0,1.0]}
Enter fullscreen mode Exit fullscreen mode

This is protentially a more complex function, so lets pause here and analyze it. map allows us to take an iterator, and a function, and apply the function to every element in the iterator. max and min pretty self explanatory, the only point of note is the partial application of e.g. (min 1). $ this is the most interesting part, I absolutely adore this function, I think it's super cool. It is defined as follows:

$ :: (a -> b) -> a -> b
Enter fullscreen mode Exit fullscreen mode

Thus the operator performs function composition, however in the background it is both right-associative and has a precedence of 0. In our use case we use it to avoid loads of brackets, in a simple example:

main = do
    print $ Vec [1,2,3] -- Vec {vecList = [1,2,3]}
    print (Vec [1,2,3]) -- Vec {vecList = [1,2,3]}
Enter fullscreen mode Exit fullscreen mode

give us the same output. However in clamp above, it is more useful than purely asthetics. This is the only way we can define this function to be point-free. Finally Vec just 'repackages' our operations back into a Vector.

I think this is a natural stopping point, we're bascially back where we started, but with a lot more knowledge. Additionally, I sincerly hope we have started to lay the groundwork for cleaner code moving forward.

Top comments (0)