问题
For "fun", and to learn functional programming, I'm developing a program in Clojure that does algorithmic composition using ideas from this theory of music called "Westergaardian Theory". It generates lines of music (where a line is just a single staff consisting of a sequence of notes, each with pitches and durations). It basically works like this:
- Start with a line consisting of three notes (the specifics of how these are chosen are not important).
- Randomly perform one of several "operations" on this line. The operation picks randomly from all pairs of adjacent notes that meet a certain criteria (for each pair, the criteria only depends on the pair and is independent of the other notes in the line). It inserts 1 or several notes (depending on the operation) between the chosen pair. Each operation has its own, unique criteria.
- Continue randomly performing these operations on the line until the line is the desired length.
The issue I've run into is that my implementation of this is quite slow, and I suspect it could be made faster. I'm new to Clojure and functional programming in general (though I'm experienced with OO), so I'm hoping someone with more experience can point out if I'm not thinking in a functional paradigm or missing out on some FP technique.
My current implementation is that each line is a vector containing maps. Each map has a :note and a :dur. :note's value is a keyword representing a musical note like :A4 or :C#3. :dur's value is a fraction, representing the duration of the note (1 is a whole note, 1/4 is a quarter note, etc...). So, for example, a line representing the C major scale starting on C3 would look like this:
[
{:note :C3 :dur 1}
{:note :D3 :dur 1}
{:note :E3 :dur 1}
{:note :F3 :dur 1}
{:note :G3 :dur 1}
{:note :A4 :dur 1}
{:note :B4 :dur 1}
]
Let's ignore the problem of using a vector (that's covered in a different question). I want to focus on improving the speed of figuring out which pairs of notes pass the criteria for a given operation.
The problem is, each operation has to see which adjacent note pairs meet its criteria. Currently, this is O(n) where n is the size of the line (it simply steps through the entire line and looks at each pair), which means even if I fix the issue of using a vector it would still be slow. However, the nice thing about the criteria check is that it only has to re-evaluate notes whose neighbors have changed (since the criteria for each pair is independent of the other notes in the line). I'm not sure how I would keep track of this in Clojure, so that the algorithm would track which pairs were dirty and which were clean, and be able to only re-evaluate note pairs that were dirty. Would it be okay, paradigm-wise, to keep track of that using a map to enclose the line, and adding metadata? For example, I would have a structure like this:
{
:line [<the elements of the line>]
:dirty [<indexes of notes that need to be re-checked>]
:valid {
operation1 [<indexes of notes that operation 1 can be performed on>]
operation2 [<indexes of notes that operation 2 can be performed on>]
...
}
}
How could I do something like that but make it a nice abstraction? Like, so I wouldn't have to remember how the thing was structured every time I wanted to do stuff to it. Is there some Clojure language feature that would be useful for abstracting this? Or is this not a good way to do it? I'm thinking about how easy it is in OO to abstract away implementation details like this (for example, if I want to say that a given note is now dirty I can do something like .setDirty(index) instead of accessing the vector directly). I'm sure there's ways to do this in Clojure/functional programming.
Does this seem like a decent way to track "dirty" notes? How might I code this in Clojure to make it easy to use (making a nice abstraction so I don't have to remember how the map is structured).
回答1:
Clojure protocols and records somewhat resemble object-oriented interfaces and classes. In this case you could define a protocol to abstract the essential methods on a line:
(defprotocol LineProtocol
(extend [this])
(note-seq [this])
(length [this]))
where extend
produces a new line by applying random operation to a random applicable note pair, length
returns the number of notes in the line, and note-seq
returns the sequence of notes in the line.
Then given an initial line you could get a line of the desired length by:
(->> line
(iterate extend)
(drop-while #(< (length %) desired-length))
first
note-seq)
The abstraction of the implementation you have in mind might look like
(defrecord Line [notes valid dirty]
LineProtocol
(extend [this] (->Line ...)) ;calc new line from this line's state
(length [this] (count notes))
(note-seq [this] (seq notes)))
if notes
is a vector. (->Line
is a factory method generated by the defrecord
call.)
Your implementation seems on the right track to me, though I think you have to evaluate all the dirty notes on each call to extend
.
回答2:
I'm also curious about what @schaueho is asking -- do you really need to track the "dirty" notes, or would it be sufficient to iteratively pick elements of the vector at random, examine them and the surrounding elements, see if the criteria are met, and if so, make the changes, otherwise do nothing, and move on, until the line is at the desired length?
Assuming that you do need to track "dirty" notes, you could actually store {:dirty true}
as metadata on the note itself, like so:
(with-meta {:note :C3 :dur 1} {:dirty true})
This would be kind of like calling note.setDirty()
, in OOP parlance.
Then determining whether a note is "dirty" is as easy as:
(defn dirty? [note]
(:dirty (meta note)))
So your algorithm might look something like this:
(loop [notes [{:note :C4 :dur 1} {:note :E4 :dur 1} {:note :G4 :dur 1}]]
(if (>= (count notes) desired-length)
notes
(recur (let [note (rand-nth notes)]
(if (dirty? note)
notes
(update-notes-and-include-dirty-metadata-on-affected-notes))))))
The issue of how exactly to update the array at a specific index has to do with your other question. I thought about it some too and I think A. Malloy is probably on the right track with finger trees. Unless you're dealing with hugely long melodies, I would think that a finger tree could handle this sort of thing well.
来源:https://stackoverflow.com/questions/24132238/how-can-i-provide-a-nice-easy-to-use-abstraction-for-tracking-dirty-elements