Learning Haskell part 2
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
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
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.
L-System
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)
rulesis aMapfrom axiom to some rule,getRulequeries thatMapand returns matching rule or input axiom if no rule is matchinggenGeneration(no idea what would be a good name for it) generates new axiom based on current one, by concatenating results ofgetRulegenGenerations(even worse name) runsgenGenerationrecursivelyntimes (using awesome Haskell pattern matching)
All this code can be tested in ghci:
*Main> genGeneration "F"
"FF+[+F-F-F]-[-F+F+F]"
*Main> genGenerations 2 "F"
"FF+[+F-F-F]-[-F+F+F]FF+[+F-F-F]-[-F+F+F]+[+FF+[+F-F-F]-[-F+F+F]-FF+[+F-F-F]-[-F+F+F]-FF+[+F-F-F]-[-F+F+F]]-[-FF+[+F-F-F]-[-F+F+F]+FF+[+F-F-F]-[-F+F+F]+FF+[+F-F-F]-[-F+F+F]]"
The next thing that I needed was a turtle ↗, following my L-System rules:
F- move forward and draw a lineG- 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)
AngleandPositionshould be self-explanatory - one is just an alias forDouble, another is a tuple of twoDouble:x/ypositionTurtlecontainsPositionandAngleTurtleStatus(another name that could be improved) contains current turtle information, array of Diagrams that it created, and Stack ofTurtleinformations (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.
Fin.
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.