Ascii fractals

Getting hypnotized by the shape of a fractal is certainly fascinating. In this blog, we will write a Haskell program that creates fractals from a base pattern. The recursive nature of the fractals allow a simple implementation in Haskell. In case you don't know what a fractal is, I invite you to take a look at the Wikipedia page.

So, let's start coding. First let's write the types for what we want.

type Pattern = [[Char]]
fractalize :: Pattern -> Int -> Pattern
render :: Pattern -> String

The function fractalize takes a base pattern and the number of times that we want to augment it. It returns the resulting fractal.

Given a pattern, let $n, m$ be the number of rows and columns of the base pattern. We define:

\[ charAt(k, i, k)= \begin{cases} p[i][j], & \text{if } k = 1 \\ \text{' '} & \text{if } charAt(k - 1, \frac{i}n, \frac{j}m) = \text{' '} \\ p[i\bmod n][j\bmod m], & \text{otherwise} \end{cases} \]

This function returns the character in the resulting fractal at the specified position.

Let's translate it into Haskell code:

fractalize :: Pattern -> Int -> Pattern
fractalize [] _ = []
fractalize pat k = [ [ charAt k i j | j <- [0..m^k - 1] ] | i <- [0..n^k - 1] ]
  where
    n = length pat
    m = length (head pat)
    charAt k i j
      | k <= 1 = pat!!i!!j
      | ' ' == charAt (k - 1) (i`div`n) (j`div`m) = ' '
      | otherwise = pat!!(i`mod`n)!!(j`mod`m)

render :: Pattern -> String
render = unlines

Let's try this base pattern:

pattern1 :: Pattern
pattern1 = [ " # "
           , "###"
           , " # "]

Fractalizing the pattern three times gives:

             #
            ###
             #
          #  #  #
         #########
          #  #  #
             #
            ###
             #
    #        #        #
   ###      ###      ###
    #        #        #
 #  #  #  #  #  #  #  #  #
###########################
 #  #  #  #  #  #  #  #  #
    #        #        #
   ###      ###      ###
    #        #        #
             #
            ###
             #
          #  #  #
         #########
          #  #  #
             #
            ###
             #

Cool! let's try another pattern!

pattern2 :: Pattern
pattern2 = [ "# #"
           , " # "
           , "# #"]
# #   # #         # #   # #
 #     #           #     #
# #   # #         # #   # #
   # #               # #
    #                 #
   # #               # #
# #   # #         # #   # #
 #     #           #     #
# #   # #         # #   # #
         # #   # #
          #     #
         # #   # #
            # #
             #
            # #
         # #   # #
          #     #
         # #   # #
# #   # #         # #   # #
 #     #           #     #
# #   # #         # #   # #
   # #               # #
    #                 #
   # #               # #
# #   # #         # #   # #
 #     #           #     #
# #   # #         # #   # #

Awesome!

The glue code of our main program is very simple. The number of augments is passed as an argument to the program.

main :: IO ()
main = getK >>= putStr . render . fractalize pattern1
  where getK = read . head <$> getArgs

In order to run the example by yourself, get the full code from here and run:

runhaskell fractals.hs 3

We are done! Try your base patterns and have fun!

Final comments

It could be nice to adapt the code to use arrays with constant access time instead of lists, but for small inputs it is irrelevant.

Related