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.
In Tidal, music is based around patterns — there's nine channels (named
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
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)
Mapfrom axiom to some rule,
Mapand 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
genGenerations(even worse name) runs
ntimes (using awesome Haskell pattern matching)
All this code can be tested in
*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 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)
Positionshould be self-explanatory - one is just an alias for
Double, another is a tuple of two
TurtleStatus(another name that could be improved) contains current turtle information, array of Diagrams that it created, and Stack of
Turtleinformations (for saving and loading positions)
] 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
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
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
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.