In the last post We wrote a music function that took an arbitrary music expression and applied an arbitrary color to it. The problem we immediately saw is that we manually had to override all properties - and that we still didn't manage to catch them all. In general terms, a function that redundantly calls \override
for each new property isn't exactly elegantly programmed. So today we will do better and at the same time learn something about reusing code by refactoring and by processing lists.
This was the function as we wrote it originally:
{% lilypond %} \version "2.18.0"
colorMusic = #(define-music-function (parser location my-color music) (color? ly:music?) #{ \temporary \override NoteHead.color = #my-color \temporary \override Stem.color = #my-color \temporary \override Flag.color = #my-color \temporary \override Beam.color = #my-color \temporary \override Rest.color = #my-color \temporary \override Slur.color = #my-color \temporary \override PhrasingSlur.color = #my-color \temporary \override Tie.color = #my-color \temporary \override Script.color = #my-color \temporary \override Dots.color = #my-color
#music
\revert NoteHead.color
\revert Stem.color
\revert Flag.color
\revert Beam.color
\revert Rest.color
\revert Slur.color
\revert PhrasingSlur.color
\revert Tie.color
\revert Script.color
\revert Dots.color
#}) {% endlilypond %}
For each notation element in question we called \temporary override NNNN.color = #my-color
, so the first thing we'll do is factoring out this common functionality in a dedicated function. This will be a helper function usually not called directly. Each use of \override
could be enclosed in a music expression so we'll write a new music function that returns such a music expression.
{% lilypond %} colorGrob = #(define-music-function (parser location my-grob my-color) (symbol? color?) #{ \temporary \override #my-grob #'color = #my-color #}) {% endlilypond %}
As you can see this function takes two arguments: a grob name (GRaphicalOBject) and a color. The color has the type we already know for it (color?) while the grobname has to be given as a symbol?. In the function body the \temporary \override
is applied to the given grob with the given color. So now we can call the function with \colorGrob NoteHead #red
, and as we already have the color as the argument to our entry function \colorMusic
we can use \colorGrob NoteHead #my-color
in our main function.
For uncoloring the music we write a corresponding function \uncolorMusic
that uses \revert
instead of \override
. Our modified main function and its two new helper functions now look like this:
{% lilypond %} \version "2.18.0"
colorGrob = #(define-music-function (parser location my-grob my-color) (symbol? color?) #{ \temporary \override #my-grob #'color = #my-color #})
uncolorGrob = #(define-music-function (parser location my-grob) (symbol?) #{ \revert #my-grob #'color #})
colorMusic = #(define-music-function (parser location my-color music) (color? ly:music?) #{ \colorGrob NoteHead #my-color \colorGrob Stem #my-color \colorGrob Flag #my-color \colorGrob Beam #my-color \colorGrob Rest #my-color \colorGrob Slur #my-color \colorGrob PhrasingSlur #my-color \colorGrob Tie #my-color \colorGrob Script #my-color \colorGrob Dots #my-color
#music
\uncolorGrob NoteHead
\uncolorGrob Stem
\uncolorGrob Flag
\uncolorGrob Beam
\uncolorGrob Rest
\uncolorGrob Slur
\uncolorGrob PhrasingSlur
\uncolorGrob Tie
\uncolorGrob Script
\uncolorGrob Dots
#})
music = \relative c' { c4. d8 e16 d r cis( d4) ~ | d1 \fermata }
\relative c' { \colorMusic #blue \music \colorMusic #red { c4 c } d \colorMusic #green e\f \colorMusic #magenta \music } {% endlilypond %}
which gives the same result as in the last post: [caption id="attachment_2544" align="aligncenter" width="625"] (Click to enlarge)[/caption]
OK, this doesn't make our entry function that much shorter, but we have already achieved a significant first step: avoiding the repetition of the same code. This makes it possible to maintain this code in one place. For example, I told you that \temporary
is a rather new command and that you may just skip that command if you want to use LilyPond 2.16 for some reason. Now that we have factored out the command into a function you can simply update this function once and have the modified code for all instances of the function. Or imagine that you want to temporarily disable the function completely, then you can simply comment out the line in the function, and everything will remain black. Nevertheless this has only been the first step ...
We haven't yet arrived at the goal because we still need to call our helper functions for each grob type individually. The goal would be to use only a single function call, and that can be achieved by passing the grob names as a list. So we want to write a function with the following signature:
{% lilypond %} colorGrobs = #(define-music-function (parser location my-grob-list my-color) (symbol-list? color?) #{
#}) {% endlilypond %}
This function takes a list of grob names and the color and will then iterate over this list to apply the overrides (using the helper functions we have already written). We'd call that function like this from our main function:
{% lilypond %} \colorGrobs #'(NoteHead Stem Flag Beam Rest Slur PhrasingSlur Tie Script Dots DynamicText Accidental) #my-color {% endlilypond %}
This will make our main function much more concise and maintainable because adding a newly noticed grob simply requires adding its name to the my-grob-list
list.
While writing the signature of the \colorGrobs
function was really easy iterating over that list of grob names is something that really can drive you crazy if you're not yet really familiar with Scheme. And I have to admit it drove me crazy while writing this post - which you can take as an indication that I won't be able to continue this series too much further ;-)
A straightforward way to iterate over a list that is comprehensible for people coming from other programming languages is the map
operation. It takes a function and a list of values and applies the function to each of the values one by one. It returns a new list of modified values.
{% lilypond %} (map colorGrob my-grob-list) {% endlilypond %}
will walk over our list of grob names and pass each one to the colorGrob
function. While this looks quite concise it doesn't take the color argument into account, so we have to look further.
Seemingly the solution to this issue would be the lambda
construct which creates an ad-hoc procedure (basically an unnamed function) that can use any number of arguments.
{% lilypond %} ((lambda (arg) (colorGrob arg my-color)) NoteHead) {% endlilypond %}
will create a function with the body:
{% lilypond %} (colorGrob arg my-color) {% endlilypond %}
arg
is defined to be the argument passed into the function, and NoteHead
is passed into it, so colorGrob
will actually be called:
{% lilypond %} (colorGrob NoteHead my-color) {% endlilypond %}
This example is quite useless of course and only there to show how lambda
can work with hard-coded and variable arguments, but can be made fruitful together with map
.
{% lilypond %} (map (lambda (arg) (colorGrob arg my-color)) my-grob-list) {% endlilypond %}
will walk over my-grob-list
and pass the items one by one to the ad-hoc (lambda) function. This way we iterate over the list of grob names passing them each to colorGrob
, accompanied by the static color argument (my-color).
Unfortunately, our journey isn't at its end. As said map
applies the elements of a list to a given function. This function evaluates to a single value (which is what each Scheme expression or function does), and map
creates a new list of all these values. This is very useful for manipulating lists in general, but in our case this is a problem: we have to return a music expression, but map
creates a list of music expressions. So unfortunately we can't go that way and have to apply real recursion. (The Guile manual documents map and lambda, and is a helpful resource for Scheme in general.)
Today we seemingly didn't achieve too much: we factored out some code and then defined the goal where we want to go next. But on the way to that goal we arrived at a dead end. But of course I described this on purpose because I hope discussing these things at such a slow pace will give you an opportunity to get familiar with some of the peculiarities of how Scheme works. While map
and lambda
don't help us with the current issue they are indispensable tools for your career as a LilyPond-Scheme programmer, and it's good to get to grips with them as early as possible.
We will complete this exercise in the next post and learn about a very fundamental concept of Scheme: recursion.
{% credits %}{% endcredits %}