I wrote my first image processing application in Haskell yesterday based on a tutorial by Mark Karpov. It writes a Mandelbrot fractal to an image file like this:

The library I used is called JuicyPixels, which you can install with cabal like this:

cabal install JuicyPixels

It also depends on the Safe library, just because I like the Maybe variant of the *head* function. You can install that like this:

cabal install Safe

Let’s walk through the code.

The main function is pretty simple. I avoided using do notation so that it’s really clear what’s going on with the monadic binding:

main = let imageWidth = 1920 imageHeight = 1080 fractal = mandelbrot (Counter 1000) (Dimensions (Width imageWidth) (Height imageHeight)) in getArgs >>= (\args -> printFileName (headMay args)) >>= (\fileName -> return (fileName, (ImageRGB8 (generateImage fractal 1920 1080)))) >>= (\(fileName, image) -> savePNGFile fileName image) >> exitSuccess

First, I define an *imageWidth* and *imageHeight*. Then I define a *fractal* function by calling the *mandelbrot* function. I bind the command line arguments to *args* and maybe print the file name which should be stored in the head of the arguments list. Because I use *headMay* instead of head, the *fileName* is stored in a type Maybe String, instead of String, which means that if a command line argument is not provided, the program does nothing except print a usage string.

Secondly, I lift a tuple of the *fileName* and an image generated by the function *mandelbrot* into the IO monad and bind that to the next statement, which saves the image file. Finally, I use a >> bind to continue execution with *exitSuccess*, which exits with a success code.

>> bind operations allow you to call a monadic function which doesn’t take a parameter. Since *exitSuccess* doesn’t take a parameter, we need to use >> bind here.

Let’s look quickly at the *printFileName* function:

printFileName :: Maybe String -> IO (Maybe String) printFileName Nothing = putStrLn "Usage: fractal " >> return Nothing printFileName (Just fileName) = putStrLn ("Generating " ++ fileName) >> return (Just fileName)

This function takes the output of *headMay* as an input.

In the case where it pattern matches *Nothing*, it prints a usage string. It needs to evaluate to an IO (Maybe String) because that is later bound to the *fileName* constant, so I lift *Nothing* into the IO monad after printing.

In the case where it pattern matches *Just fileName*, I print a string and bind that to an expression which lifts *Just fileName* into the IO monad, so that it can be bound to *fileName* later.

*generateImage* is a function provided by the JuicyPixels library which creates a function by mapping a function over an ADT (presumably a list). The function I wrote for it to map is *mandelbrot*. The Mandelbrot function is defined as:

mandelbrot :: Counter -> Dimensions -> Int -> Int -> PixelRGB8 mandelbrot (Counter maxIter) (Dimensions (Width w) (Height h)) x y = let maxDimension = max w h --Coordinates recentered so that the origin is in the middle of the image Coordinate rx ry = Coordinate (x - w `div` 2) (y - h `div` 2) --Coordinates normalized into the range [-1, 1] Point nx ny = Point (fromIntegral rx / fromIntegral maxDimension) (fromIntegral ry / fromIntegral maxDimension) pixelValue = genMandelbrot (Counter 0) (Counter maxIter) (Point 0.0 0.0) (Point (nx * 3.0) (ny * 3.0)) --Scale point to the range [-3, 3] in PixelRGB8 0 0 (fromIntegral (min pixelValue 255))

*mandelbrot* takes a counter, image dimensions and two integers representing the texel coordinates, and evaluates to the texel value. The Counter defines the maximum number of iterations of Mandelbrot’s function to run. Notice how I used Currying and partial evaluation to pass a valid function, *mandelbrot (Counter 1000) (Dimensions (Width imageWidth) (Height imageHeight))*, to *generateImage*, which expects to get a function with two Int parameters as an argument.

The let expression computes the pixel value.

We need the *maxDimension* to ensure that the fractal fits inside the image.

*Coordinate rx ry* re-centers the pixel coordinate from a range of ([0, width], [0, height]) to a coordinate in the range ([-width/2, width/2], [-height/2, height/2]) so that the center of the fractal, (0, 0), will be in the middle of the image.

*Point nx ny* converts the re-centered coordinates to the range ([-1, 1], [-1, 1]) so that the fractal will be scaled to fill the image correctly.

When computing the final texel value in *genMandelbrot*, *nx* and *ny* are scaled by 3.0 because that looks prettier.

This function calls the pure implementation of the Mandelbrot function:

genMandelbrot :: Counter -> Counter -> Point -> Point -> Int genMandelbrot (Counter iter) (Counter maxIter) (Point x y) (Point px py) | diverges || iter > maxIter = iter | otherwise = genMandelbrot (Counter (iter + 1)) (Counter maxIter) (Point updatedX updatedY) (Point px py) where diverges = x * x + y * y > 4.0 --Update coordinates based on the definition of the Mandelbrot Set updatedX = x * x - y * y + px updatedY = 2.0 * x * y + py

This function recursively re-evaluates a pair of local coordinates, Point x y, based on the pixel coordinates, *Point px py*, by repeatedly applying the fractal iteration. If the coordinates diverge (distance squared > 4) or the function exceeds the maximum iterations, it evaluates to the iteration count, which determines the pixel color.

fractal.hs produces a simple blue Mandelbrot fractal. Later, I wrote coloredfractal.hs which was used to generate the image above using the discrete Escape Time algorithm to color the fractal.

Special thanks to **u/jared–w** who gave me feedback and suggested improvements to my code and **@geophf** for suggesting that I should use the Escape Time algorithm to color the fractal.

The source code is available at https://github.com/WhatTheFunctional/ImmutableMandelbrot.

References:

Image processing with Juicy Pixels and Repa by Mark Karpov

Nice! Haskell needs these examples of graphics and charting. For the coloring I saw that you when with blue. Maybe use the whole color palette to represent each point’s escape? https://en.wikipedia.org/wiki/Mandelbrot_set#Escape_time_algorithm

LikeLike

HA! I see you added a colorized version! Just as I was about to make that very problem today’s @1HaskellADay problem. Good show!

LikeLiked by 1 person

Thanks geophf!

LikeLike

If you want a challenge, you could try making the continuous version of the Escape Time algorithm. It requires a precomputed histogram which is an extra piece of state that I just didn’t want to deal with in this demo.

LikeLike