π΅οΈ 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.
Intro
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.
Typeclasses
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]
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
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
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]}
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
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]}
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)