Learning Haskell part 2exploring Tidal and Diagrams

Learning Haskell was one of my one-project-a-month projects in 2017. I can't say I'm anywhere near fluent in the language, but it's been an interesting journey, and I've learned a lot of new concepts.

October was all about exploring different creative-coding-related uses of Haskell: famous live-coding environment — Tidal, and (a bit less) famous graphics library — Diagrams.

Tidal is a live-coding environment for creating live music. It's mostly sample-based, using SuperDirt as a backend (which itself is SuperCollider addon).

In Tidal, music is based around patterns — there's nine channels (named d1 to d9) where they can be sent through to SuperCollider. The most basic example is playing single sample every cycle:

d1 $ sound "bd"

Complex patterns, in combination with audio effects, can create interesting small compositions:

d1 $ fast "0.75" $ n "d3? d4*2 e3 a3 f3 c3?" # s "supersaw" # speed "0.3 0.2" # shape 0.6 # room 0.8 # delay 0.4
d2 $ fast "0.25" $ n "a2 ~ a1 ~ c2" # s "supersaw" # speed "-0.1" # room 0.9 # shape 0.9 # gain 0.9
d3 $ fast "1.5" $ n "d7 a7" # s "supersaw" # speed "-0.9" # room 0.2 # shape 0.9 # gain 0.5 # delay 0.8

I played with Tidal during first two weeks of October, going through Tidal tutorial and Tidal patterns documents.

Log of my experiments is available online: szymonkaliski/haskell-playground/tidal.

Tidal, although extremely interesting as stand-alone project, didn't make me feel that I was gaining any Haskell knowledge. It has its own set of functions and uses quite small part of Haskell language.

This is not something good nor bad, I feel that Tidal can be extremely productive and is quick to build up complex patterns of sounds, but after two weeks I wanted to explore more of Haskell itself.

Diagrams is Haskell's DSL for creating vector graphics. I followed the quick start tutorial, and tried to build at least a simple graphic every day, first by making simple shapes, then patterns, and later using noise, and finishing the month with L-System implementation.

One thing I really enjoyed, is how terse Haskell can be. To build this shape:

I only needed this:

blackCircle = circle 0.1 # fc black
circleEdges w = atPoints (trailVertices $ regPoly w 1) $ repeat blackCircle

diagram :: Diagram B
diagram = foldr1 mappend $ map circleEdges [3,6..30]

I wouldn't say that terseness of a language signifies in any way how powerful it is, or that the amount of letters that have to be typed influences productivity in any way but, for me, shorter functions with little syntax, feel easier to read.

Last part of my two weeks with Diagrams was spent building simple L-System from scratch. I started with basic rule rewriting code:

rules = Map.fromList [('F', "FF+[+F-F-F]-[-F+F+F]")]

getRule axiom = case Map.lookup axiom rules of
                     Just result -> result
                     Nothing -> [axiom]

genGeneration axiom = concat $ map getRule axiom

genGenerations :: Int -> [Char] -> [Char]
genGenerations 0 axiom = axiom
genGenerations n axiom = genGeneration (genGenerations (n - 1) axiom)
  • rules is a Map from axiom to some rule, getRule queries that Map and returns matching rule or input axiom if no rule is matching
  • genGeneration (no idea what would be a good name for it) generates new axiom based on current one, by concatenating results of getRule
  • genGenerations (even worse name) runs genGeneration recursively n times (using awesome Haskell pattern matching)

All this code can be tested in ghci:

*Main> genGeneration "F"

*Main> genGenerations 2 "F"

The next thing that I needed was a turtle, following my L-System rules:

  • F - move forward and draw a line
  • G - move forward without drawing a line
  • + - turn right
  • - - turn left
  • [ - save current position
  • ] - load last saved position

I started by creating some types:

type Angle = Double
type Position = (Double, Double)
data Turtle = Turtle Position Angle deriving Show
type TurtleStatus = (Turtle, [Diagram B], Stack Turtle)
  • Angle and Position should be self-explanatory - one is just an alias for Double, another is a tuple of two Double: x/y position
  • Turtle contains Position and Angle
  • TurtleStatus (another name that could be improved) contains current turtle information, array of Diagrams that it created, and Stack of Turtle informations (for saving and loading positions)

For [ and ] L-System rules I've created simple Stack implementation, so turtle information can be pushed and popped when needed:

data Stack a = Stack [a] deriving Show

push :: a -> Stack a -> Stack a
push x (Stack xs) = Stack (x:xs)

pop :: Stack a -> (a, Stack a)
pop (Stack (x:xs)) = (x, Stack xs)

This way I could execute the turtle over all of the L-System rules, gather Diagrams that it created, and generate graphics in the last step. To move the turtle forward I'm calculating an end point using current turtle position and angle (notice how drawLineAndMoveForward returns a Diagram):

moveForward :: TurtleStatus -> TurtleStatus
moveForward ((Turtle (x, y) angle), ds, st) = ((Turtle (nx, ny) angle), ds, st)
  where nx = (x + (sin (angle / 360) * pi) * stepDistance)
        ny = (y + (cos (angle / 360) * pi) * stepDistance)

drawLineAndMoveForward :: TurtleStatus -> TurtleStatus
drawLineAndMoveForward (t, ds, st) = (nt, d:ds, nst)
  where (nt, _, nst) = moveForward (t, ds, st)
        d = fromVertices $ map p2 [(getPosition t), (getPosition nt)]

Next step is to get turtle function for given rule, and execute set of rules:

getRuleFn :: Char -> (TurtleStatus -> TurtleStatus)
getRuleFn r = case r of
                   'F' -> drawLineAndMoveForward
                   'G' -> moveForward
                   '+' -> turnRight
                   '-' -> turnLeft
                   '[' -> saveLocation
                   ']' -> restoreLocation

executeTurtleRules :: [Char] -> TurtleStatus -> TurtleStatus
executeTurtleRules (r:rs) t = executeTurtleRules rs $ getRuleFn r t
executeTurtleRules [] t = t

To get the final diagram, we have to execute turtle rules, get the diagrams part of TurtleStatus and fold them into single diagram:

getDiagrams :: TurtleStatus -> [Diagram B]
getDiagrams (_, ds, _) = ds

diagram :: Diagram B
diagram = foldr1 mappend $ getDiagrams $ executeTurtleRules rs t
  where rs = genGenerations numGenerations "F"
        t = ((Turtle (0,0) 0), [], Stack [])

By modifying L-System rules, as well as stepDistance, turnAngle and numGenerations constants we can get a variety of different structures.

I'm pretty happy with my progress in Haskell, especially with the last mini-project of building functioning L-System.

After two months, I can say that Haskell doesn't feel like a good creative-coding environment for me, strong types look like a great thing for refactoring and maintaining big applications, but when exploring, I felt that it was slowing me down a little. Having to think about type of a thing, when in early stages of exploring the thing is not a very useful thing for me.

I stared less at a blank screen caused by runtime error, and more at compiler output.

Usually, once the compiler was happy, the application was running without issues, so it feels like trading runtime errors for compile-time errors, with the overhead of needing to have more things figured out upfront.

I can imagine using Haskell for building server-side code, or backend for installation, and I hope I'll get relaxed enough timeline on one of my client projects to try it out.

For now, all the code is published online: szymonkaliski/haskell-playground, and I'm moving forward to November's project.

Szymon Kaliski © 2022