Relecture

This commit is contained in:
Yann Esposito (Yogsototh) 2012-06-11 00:34:44 +02:00
parent 570fea3b04
commit 8e0e2fc56a
8 changed files with 153 additions and 130 deletions

View file

@ -1,35 +1,49 @@
## Introduction
TODO: write something nice after reading.
I wanted to go further than my
[preceding article](/Scratch/en/blog/Haskell-the-Hard-Way/) in which I introduced Haskell.
The B in Benoît B. Mandelbrot stand fro Benoît B. Mandelbrot
Instead of arguing that Haskell is better, because it is functional and "Functional Programming! Yeah!", I'll give an example of what benefit
functional programming can provide.
This article is more about functional paradigm than functional language.
The code organization can be used in most imperative language.
As Haskell is designed for functional paradigm, it is easier to talk about functional paradigm using it.
In reality, in the firsts sections I use an imperative paradigm.
As you can use functional paradigm in imperative language,
you can also use imperative paradigm in functional languages.
[^1]: [Source on reddit](http://www.reddit.com/r/math/comments/uirv8/are_math_jokes_okay_here_i_just_came_up_with_one/c4vr2zo)
This article is about creating a useful program.
It can interact with the user in real time.
It uses OpenGL, a library with imperative programming foundations.
But the final code will be quite clean.
Most of the code will remain in the pure part (no `IO`).
Steps:
I believe the main audience for this article are:
1. Mandelbrot set with Haskell OpenGL
2. Mandelbrot edges
3. 3D Mandelbrot because its fun
4. Clean the code from full impure and imperative to purer and purer.
5. Refactor the code to separate nicely important parts
6. Improve efficiency
- Haskell programmer looking for an OpengGL tutorial.
- People interested in program organization (programming language agnostic).
- Fractal lovers and in particular 3D fractal.
- Game programmers (any language)
I wanted to talk about something cool.
For example I always wanted to make a Mandelbrot set explorer.
I had written a [command line Mandelbrot set generator in Haskell](http://github.com/yogsototh/mandelbrot.git).
The cool part of this utility is that it use all the cores to make the computation (it uses the `repa` package)[^001].
[^001]: Unfortunately, I couldn't make this program to work on my Mac. More precisely, I couldn't make the [DevIL](http://openil.sourceforge.net/) library work on Mac to output the image. Yes I have done a `brew install libdevil`. But even a minimal program who simply write some `jpg` didn't worked.
This time, we will display the Mandelbrot set extended in 3D using OpenGL and Haskell.
You will be able to move it using your keyboard.
This object is a Mandelbrot set in the plan (z=0),
and something nice to see in 3D.
Here is what you'll end with:
blogimage("GoldenMandelbulb.png","A golden mandelbulb")
And here are the intermediate steps:
blogimage("HGL_Plan.png","The parts of the article")
From 1 to 3 it will be _dirtier_ and _dirtier_.
At 4, we will make some order in this mess!
Hopefuly for the best!
One of the goal of this article is to show some good properties of Haskell.
In particular, how to make some real world application with a pure functional language.
I know drawing a simple mandelbrot set isn't a "real world" application.
But the idea is not to show you a real world application which would be hard to follows, but to give you a way to pass from the pure mindset to some real world application.
To this, I will show you how should progress an application.
It is not something easy to show.
This is why, I preferred work with a program that generate some image.
In a real world application, the first constraint would be to work with some framework.
And generally an imperative one.
Also, the imperative nature of OpenGL make it the perfect choice for an example.
We start cleaning everything at the 4th part.

View file

@ -1,13 +1,10 @@
## First version
We can consider two parts.
The first being mostly some boilerplate[^1].
The second part, contain more interesting stuff.
Even in this part, there are some necessary boilerplate.
But it is due to the OpenGL library this time.
The first being mostly some boilerplate[^011].
And the second part more focused on OpenGL and content.
[^1]: Generally in Haskell you need to declare a lot of import lines.
[^011]: Generally in Haskell you need to declare a lot of import lines.
This is something I find annoying.
In particular, it should be possible to create a special file, Import.hs
which make all the necessary import for you, as you generally need them all.
@ -50,9 +47,6 @@ We declare some useful functions for manipulating complex numbers:
### Let us start
Well, up until here we didn't made something useful.
Just a lot of boilerplate and default value.
Sorry but it is not completely the end.
We start by giving the main architecture of our program:
> main :: IO ()
@ -69,7 +63,8 @@ We start by giving the main architecture of our program:
> -- We enter the main loop
> mainLoop
The only interesting part is we declared that the function `display` will be used to render the graphics:
Mainly, we initialize our OpenGL application.
We declared that the function `display` will be used to render the graphics:
> display = do
> clear [ColorBuffer] -- make the window black
@ -77,12 +72,12 @@ The only interesting part is we declared that the function `display` will be use
> preservingMatrix drawMandelbrot
> swapBuffers -- refresh screen
Also here, there is only one interesting part,
the draw will occurs in the function `drawMandelbrot`.
Also here, there is only one interesting line;
the draw will occur in the function `drawMandelbrot`.
Now we must speak a bit about how OpenGL works.
We said that OpenGL is imperative by design.
In fact, you must write the list of actions in the right order.
This function will provide a list of draw actions.
Remember that OpenGL is imperative by design.
Then, one of the consequence is you must write the actions in the right order.
No easy parallel drawing here.
Here is the function which will render something on the screen:
@ -111,8 +106,8 @@ drawMandelbrot =
~~~
We also need some kind of global variables.
In fact, global variable are a proof of some bad design.
But remember it is our first try:
In fact, global variable are a proof of a design problem.
We will get rid of them later.
> width = 320 :: GLfloat
> height = 320 :: GLfloat
@ -135,7 +130,7 @@ We need a function which transform an integer value to some color:
> in
> Color3 (t n) (t (n+5)) (t (n+10))
And now the mandel function.
And now the `mandel` function.
Given two coordinates in pixels, it returns some integer value:
> mandel x y =
@ -144,8 +139,8 @@ Given two coordinates in pixels, it returns some integer value:
> in
> f (complex r i) 0 64
It uses the main mandelbrot function for each complex \\(c\\).
The mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
It uses the main Mandelbrot function for each complex \\(c\\).
The Mandelbrot set is the set of complex number c such that the following sequence does not escape to infinity.
Let us define \\(f_c: \mathbb{C} \to \mathbb{C}\\)
@ -163,15 +158,15 @@ Of course, instead of trying to test the real limit, we just make a test after a
> then n
> else f c ((z*z)+c) (n-1)
Well, if you download this lhs file, compile it and run it this is the result:
Well, if you download this file (look at the bottom of this section), compile it and run it this is the result:
blogimage("hglmandel_v01.png","The mandelbrot set version 1")
A first very interesting property of this program is that the computation for all the points is done only once.
The proof is that it might be a bit long before a first image appears, but if you resize the window, it updates instantaneously.
It is a bit long before the first image appears, but if you resize the window, it updates instantaneously.
This property is a direct consequence of purity.
If you look closely, you see that `allPoints` is a pure list.
Therefore, calling `allPoints` will always render the same result.
Therefore, calling `allPoints` will always render the same result and Haskell is clever enough to use this property.
While Haskell doesn't garbage collect `allPoints` the result is reused for free.
We didn't specified this value should be saved for later use.
It is saved for us.
@ -180,7 +175,6 @@ See what occurs if we make the window bigger:
blogimage("hglmandel_v01_too_wide.png","The mandelbrot too wide, black lines and columns")
Yep, we see some black lines.
Why? Simply because we drawn less point than there is on the surface.
We see some black lines because we drawn less point than there is on the surface.
We can repair this by drawing little squares instead of just points.
But, instead we will do something a bit different and unusual.

Binary file not shown.

View file

@ -51,6 +51,10 @@
</div>
This time, instead of drawing all points, I'll simply want to draw the edges of the Mandelbrot set.
The method I use is a rough approximation.
I consider the Mandelbrot set to be almost convex.
The result will be good enough.
We change slightly the drawMandelbrot function.
We replace the `Points` by `LineLoop`
@ -73,18 +77,19 @@ we will choose only point on the surface.
> map (\(x,y,c) -> (x,-y,c)) (reverse positivePoints)
We only need to compute the positive point.
The mandelbrot set is symetric on the abscisse axis.
The Mandelbrot set is symmetric on the abscisse axis.
> positivePoints :: [(GLfloat,GLfloat,Color3 GLfloat)]
> positivePoints = do
> x <- [-width..width]
> let y = findMaxOrdFor (mandel x) 0 height 10 -- log height
> let y = findMaxOrdFor (mandel x) 0 height (log2 height)
> if y < 1 -- We don't draw point in the absciss
> then []
> else return (x/width,y/height,colorFromValue $ mandel x y)
> where
> log2 n = floor ((log n) / log 2)
This function is interresting.
This function is interesting.
For those not used to the list monad here is a natural language version of this function:
~~~
@ -96,7 +101,7 @@ positivePoints =
~~~
In fact using the list monad you write like if you consider only one element at a time and the computation is done non deterministically.
To find the smallest number such that mandel x y > 0 we create a simple dichotomic search:
To find the smallest number such that `mandel x y > 0` we use a simple dichotomy:
> findMaxOrdFor func minval maxval 0 = (minval+maxval)/2
> findMaxOrdFor func minval maxval n =
@ -105,9 +110,7 @@ To find the smallest number such that mandel x y > 0 we create a simple dichotom
> else findMaxOrdFor func medpoint maxval (n-1)
> where medpoint = (minval+maxval)/2
No rocket science here.
I know, due to the fact the mandelbrot set is not convex this approach does some errors. But the approximation will be good enough.
See the result now:
No rocket science here. See the result now:
blogimage("HGLMandelEdges.png","The edges of the mandelbrot set")

View file

@ -1,20 +1,21 @@
## 3D Mandelbrot?
Why only draw the edge?
It is clearly not as nice as drawing the complete surface.
Yeah, I know, but, as we use OpenGL, why not show something in 3D.
But, complex number are only in 2D and there is no 3D equivalent to complex.
In fact, the only extension known are quaternions, 4D.
As I know almost nothing about quaternions, I will use some extended complex.
Now we will we extend to a third dimension.
But, there is no 3D equivalent to complex.
In fact, the only extension known are quaternions (in 4D).
As I know almost nothing about quaternions, I will use some extended complex,
instead of using a 3D projection of quaternions.
I am pretty sure this construction is not useful for numbers.
But it will be enough for us to create something nice.
But it will be enough for us to create something that look nice.
As there is a lot of code, I'll give a high level view to what occurs:
This section is quite long, but don't be afraid,
most of the code is some OpenGL boilerplate.
For those you want to skim,
here is a high level representation:
> - OpenGL Boilerplate
>
> - set some IORef for states
> - set some IORef (understand variables) for states
> - Drawing:
>
> - set doubleBuffer, handle depth, window size...
@ -49,8 +50,8 @@ As there is a lot of code, I'll give a high level view to what occurs:
</div>
We declare a new type `ExtComplex` (for exttended complex).
An extension of complex numbers:
We declare a new type `ExtComplex` (for extended complex).
An extension of complex numbers with a third component:
> data ExtComplex = C (GLfloat,GLfloat,GLfloat)
> deriving (Show,Eq)
@ -67,7 +68,17 @@ An extension of complex numbers:
> signum (C (x,y,z)) = C (signum x, 0, 0)
The most important part is the new multiplication instance.
Modifying this formula will change radically the shape of this somehow 3D mandelbrot.
Modifying this formula will change radically the shape of the result.
Here is the formula written in a more mathematical notation.
I called the third component of these extended complex _strange_.
$$ \mathrm{real} ((x,y,z) * (x',y',z')) = xx' - yy' - zz' $$
$$ \mathrm{im} ((x,y,z) * (x',y',z')) = xy' - yx' + zz' $$
$$ \mathrm{strange} ((x,y,z) * (x',y',z')) = xz' + zx' $$
Note how if `z=z'=0` then the multiplication is the same to the complex one.
<div style="display:none">
@ -104,15 +115,14 @@ And also we will listen the keyboard.
> createWindow "3D HOpengGL Mandelbrot"
> -- We add some directives
> depthFunc $= Just Less
> -- matrixMode $= Projection
> windowSize $= Size 500 500
> -- Some state variables (I know it feels BAD)
> angle <- newIORef ((35,0)::(GLfloat,GLfloat))
> zoom <- newIORef (2::GLfloat)
> campos <- newIORef ((0.7,0)::(GLfloat,GLfloat))
> -- Action to call when waiting
> -- Function to call each frame
> idleCallback $= Just idle
> -- We will use the keyboard
> -- Function to call when keyboard or mouse is used
> keyboardMouseCallback $=
> Just (keyboardMouse angle zoom campos)
> -- Each time we will need to update the display
@ -122,12 +132,16 @@ And also we will listen the keyboard.
> -- We enter the main loop
> mainLoop
The `idle` function necessary for animation.
The `idle` is here to change the states.
There should never be any modification done in the `display` function.
> idle = postRedisplay Nothing
We introduce some helper function to manipulate
standard `IORef`.
Mainly `modVar x f` is equivalent to the imperative `x:=f(x)`,
`modFst (x,y) (+1)` is equivalent to `(x,y) := (x+1,y)`
and `modSnd (x,y) (+1)` is equivalent to `(x,y) := (x,y+1)`
> modVar v f = do
> v' <- get v
@ -139,23 +153,27 @@ And we use them to code the function handling keyboard.
We will use the keys `hjkl` to rotate,
`oi` to zoom and `sedf` to move.
Also, hitting space will reset the view.
Remember that `angle` and `campos` are pairs and `zoom` is a scalar.
Also note `(+0.5)` is the function `\x->x+0.5`
and `(-0.5)` is the number `-0.5` (yes I share your pain).
> keyboardMouse angle zoom pos key state modifiers position =
> kact angle zoom pos key state
> keyboardMouse angle zoom campos key state modifiers position =
> -- We won't use modifiers nor position
> kact angle zoom campos key state
> where
> -- reset view when hitting space
> kact a z p (Char ' ') Down = do
> a $= (0,0)
> z $= 1
> p $= (0,0)
> a $= (0,0) -- angle
> z $= 1 -- zoom
> p $= (0,0) -- camera position
> -- use of hjkl to rotate
> kact a _ _ (Char 'h') Down = modVar a (mapFst (+0.5))
> kact a _ _ (Char 'l') Down = modVar a (mapFst (+(-0.5)))
> kact a _ _ (Char 'j') Down = modVar a (mapSnd (+0.5))
> kact a _ _ (Char 'k') Down = modVar a (mapSnd (+(-0.5)))
> -- use o and i to zoom
> kact _ s _ (Char 'o') Down = modVar s (*1.1)
> kact _ s _ (Char 'i') Down = modVar s (*0.9)
> kact _ z _ (Char 'o') Down = modVar z (*1.1)
> kact _ z _ (Char 'i') Down = modVar z (*0.9)
> -- use sdfe to move the camera
> kact _ _ p (Char 's') Down = modVar p (mapFst (+0.1))
> kact _ _ p (Char 'f') Down = modVar p (mapFst (+(-0.1)))
@ -164,9 +182,8 @@ Also, hitting space will reset the view.
> -- any other keys does nothing
> kact _ _ _ _ _ = return ()
Now, we will show the object using the display function.
Note, this time, display take some parameters.
Mainly, this function if full of boilerplate:
Note `display` take some parameters this time.
This function if full of boilerplate:
> display angle zoom position = do
> -- set the background color (dark solarized theme)
@ -184,9 +201,11 @@ Mainly, this function if full of boilerplate:
> (xangle,yangle) <- get angle
> rotate xangle $ Vector3 1.0 0.0 (0.0::GLfloat)
> rotate yangle $ Vector3 0.0 1.0 (0.0::GLfloat)
>
> -- Now that all transformation were made
> -- We create the object(s)
> preservingMatrix drawMandelbrot
>
> swapBuffers -- refresh screen
Not much to say about this function.
@ -194,9 +213,9 @@ Mainly there are two parts: apply some transformations, draw the object.
### The 3D Mandelbrot
Now, that we talked about the OpenGL part, let's talk about how we
We have finished with the OpenGL section, let's talk about how we
generate the 3D points and colors.
First, we will set the number of detatils to 180 pixels in the three dimensions.
First, we will set the number of details to 200 pixels in the three dimensions.
> nbDetails = 200 :: GLfloat
> width = nbDetails
@ -205,7 +224,9 @@ First, we will set the number of detatils to 180 pixels in the three dimensions.
This time, instead of just drawing some line or some group of points,
we will show triangles.
The idea is that we should provide points three by three.
The function `allPoints` will provide a multiple of three points.
Each three successive point representing the coordinate of each vertex of a triangle.
> drawMandelbrot = do
> -- We will print Points (not triangles for example)
@ -216,14 +237,13 @@ The idea is that we should provide points three by three.
> color c
> vertex $ Vertex3 x y z
Now instead of providing only one point at a time, we will provide six ordered points.
In fact, we will provide six ordered points.
These points will be used to draw two triangles.
blogimage("triangles.png","Explain triangles")
Note in 3D the depth of the point is generally different.
The next function is a bit long.
An approximative English version is:
Here is an approximative English version:
~~~
forall x from -width to width
@ -243,7 +263,8 @@ depthPoints = do
x <- [-width..width]
y <- [-height..height]
let
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep 7
depthOf x' y' = findMaxOrdFor (mandel x' y') 0 deep logdeep
logdeep = floor ((log deep) / log 2)
z1 = depthOf x y
z2 = depthOf (x+1) y
z3 = depthOf (x+1) (y+1)
@ -263,15 +284,16 @@ depthPoints = do
If you look at the function above, you see a lot of common patterns.
Haskell is very efficient to make this better.
Here is a somehow less readable but more generic refactored function:
Here is a harder to read but shorter and more generic rewritten function:
> depthPoints :: [ColoredPoint]
> depthPoints = do
> x <- [-width..width]
> y <- [0..height]
> y <- [-height..height]
> let
> neighbors = [(x,y),(x+1,y),(x+1,y+1),(x,y+1)]
> depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep 7
> depthOf (u,v) = findMaxOrdFor (mandel u v) 0 deep logdeep
> logdeep = floor ((log deep) / log 2)
> -- zs are 3D points with found depth
> zs = map (\(u,v) -> (u,v,depthOf (u,v))) neighbors
> -- ts are 3D pixels + mandel value
@ -288,22 +310,17 @@ Here is a somehow less readable but more generic refactored function:
If you prefer the first version, then just imagine how hard it will be to change the enumeration of the point from (x,y) to (x,z) for example.
Also, we didn't searched for negative values.
For simplicity, I mirror these values.
I haven't even tested if this modified mandelbrot is symetric relatively to the plan {(x,y,z)|z=0}.
This modified Mandelbrot is no more symmetric relatively to the plan `y=0`.
But it is symmetric relatively to the plan `z=0`.
Then I mirror these values.
> allPoints :: [ColoredPoint]
> allPoints = planPoints ++ map inverseDepth planPoints
> where
> planPoints = depthPoints ++ map inverseHeight depthPoints
> inverseHeight (x,y,z,c) = (x,-y,z,c)
> planPoints = depthPoints
> inverseDepth (x,y,z,c) = (x,y,-z+1/deep,c)
I cheat by making these symmetry.
But it is faster and render a nice form.
For this tutorial it will be good enough.
Also, the dichotomic method I use is mostly right but false for some cases.
The rest of the program is very close to the preceeding one.
The rest of the program is very close to the preceding one.
<div style="display:none">
@ -333,7 +350,8 @@ We only changed from `Complex` to `ExtComplex` of the main `f` function.
</div>
We simply add a new dimenstion to the mandel function. Also we simply need to change the type signature of the function `f` from `Complex` to `ExtComplex`.
We simply add a new dimension to the `mandel` function
and change the type signature of `f` from `Complex` to `ExtComplex`.
> mandel x y z =
> let r = 2.0 * x / width
@ -343,9 +361,6 @@ We simply add a new dimenstion to the mandel function. Also we simply need to ch
> f (extcomplex r i s) 0 64
And here is the result (if you use 500 for `nbDetails`):
Here is the result:
blogimage("mandelbrot_3D.png","A 3D mandelbrot like")
This image is quite nice.

View file

@ -1,8 +1,8 @@
## Cleaning the code
## Naïve code cleaning
The first thing to do is to separate the GLUT/OpenGL
part from the computation of the shape.
Here is the cleaned version of the preceeding section.
Here is the cleaned version of the preceding section.
Most boilerplate was put in external files.
- [`YBoiler.hs`](code/04_Mandelbulb/YBoiler.hs), the 3D rendering
@ -79,6 +79,3 @@ But I would have preferred to control the user actions.
On the other hand, we continue to handle a lot rendering details.
For example, we provide ordered vertices.
I feel, this should be externalized.
I would have preferred to make things a bit more general.

View file

@ -15,11 +15,11 @@ tags:
- functional
- tutorial
-----
blogimage("HGL_Plan.png","The plan in image")
blogimage("BenoitBMandelbrot.jpg","The B in Benoît B. Mandelbrot stand for Benoît B. Mandelbrot")
begindiv(intro)
en: %tldr A progressive real world example.
fr: %tlal Un exemple progressif de programmation avec Haskell.
en: %tldr You will see how to go from theory to a real application using Haskell.
fr: %tlal Un exemple progressif d'utilisation d'Haskell.

View file

@ -25,8 +25,8 @@ END
for fic in **/*.lhs(.N); do
contains_haskell=$(( $( egrep '^>' $fic | wc -l) > 0 ))
((contains_haskell)) && \
print -- "\n<hr/><a href=\"code/$fic\" class=\"cut\">${fic:h}/<strong>${fic:t}</strong></a>\n"
print -- "\n<hr/><a href=\"code/$fic\" class=\"cut\">Download the source code of this section → ${fic:h}/<strong>${fic:t}</strong></a>\n"
cat $fic
((contains_haskell)) && \
print -- "\n<a href=\"code/$fic\" class=\"cut\">${fic:h}/<strong>${fic:t}</strong> </a>\n"
print -- "\n<a href=\"code/$fic\" class=\"cut\">Download the source code of this section → ${fic:h}/<strong>${fic:t}</strong> </a>\n"
done | perl -pe 'BEGIN{$/="";} s#((^>.*\n)+)#<div class="codehighlight">\n<code class="haskell">\n$1</code>\n</div>\n#mg' | perl -pe 's#^> ?##' | perl -pe 's/^ #/#/'