Thoughts on yield

N

Nolan J. Darilek

I've begun working on a music-related ruby project, and recently I've
been pondering the idea of a music composition environment somewhat
similar to cm, clm and friends for lisp, but in ruby instead.

Today I was thinking about how ruby blocks make constructing
domain-specific languages a snap. I've been reading Lisp as a Second
Language at http://www.ircam.fr/equipes/repmus/LispSecondLanguage/, a
Lisp tutorial for musicians, and was considering using a list-based
structure for storing music. I sorta envisioned the language as
looking something like:

clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

In this example, a-g are methods returning note objects.

What I'd like is to have that return a list like so:

["c", "d", "e", "f", "g", "a", "b", "c", ["c", "e", "g"]]

But, as it stands, each expression is evaluated and discarded. I'd
like to avoid having to include actual lists in the blocks; while
arbitrary ruby expressions would be possible, I'm aiming to hide much
of that from the casual user.

So, my question. Is there any way to achieve something like that using
blocks? Is there any way to have yield do something other than run the
given code?

Assuming not, I was wondering how practical/horrible it would be to
have yield accept an optional block which, when given one parameter,
sets that parameter to the value of every top-level expression run
within the block? So, for instance, if I wanted to evaluate all of the
above expressions and concatenate their results into a list, I could
do:

score = []
yield { |x| score += [x] }

Of course, I realize that probably breaks the principle of least
surprise in a few instances, as many would expect the contents of a
block to be executed and only the result of all code returned, but in
my rather humble and untrained opinion, this would be useful for
implementing domain-specific languages.

Or is there a better way to accomplish what I'm trying?
 
P

Phil Tomson

I've begun working on a music-related ruby project, and recently I've
been pondering the idea of a music composition environment somewhat
similar to cm, clm and friends for lisp, but in ruby instead.

Today I was thinking about how ruby blocks make constructing
domain-specific languages a snap. I've been reading Lisp as a Second
Language at http://www.ircam.fr/equipes/repmus/LispSecondLanguage/, a
Lisp tutorial for musicians, and was considering using a list-based
structure for storing music. I sorta envisioned the language as
looking something like:

clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

In this example, a-g are methods returning note objects.

What I'd like is to have that return a list like so:

["c", "d", "e", "f", "g", "a", "b", "c", ["c", "e", "g"]]


I played with this problem for a few minutes and this is what I came up
with:

module Music

def a(list=@notes)
list << "a"
end

def b(list=@notes)
list << "b"
end

def c(list=@notes)
list << "c"
end

def d(list=@notes)
list << "d"
end

def e(list=@notes)
list << "e"
end

def f(list=@notes)
list << "f"
end

def g(list=@notes)
list << "g"
end

def clef
puts "...In Clef..."
yield
return @notes
end

def measure(time)
puts "...In Measure..."
yield
end

def chord
puts "...In Chord..."
yield @chord_notes
@notes << @chord_notes
end

def notes
@notes
end
end

class Song
include Music
def initialize
@notes = []
@chord_notes=[]
end
end

class MySong < Song
include Music
def initialize
super
clef do
measure("4/4") do
c; d; e; f; g; a; b; c;
chord do |chrd|
c(chrd); d(chrd); g(chrd);
end
end
end
end
end

mysong = MySong.new
p mysong.notes

Of course, the ugly bit is the part in the chord block: you have to
pass in 'chrd' to all of the a-g methods to get those notes to be added to
the correct list (@chord_list) which then gets appended to the @notes
list. So it's still not quite what you want (I understand that you want
people to see very little of the 'wizard behind the curtain' [Ruby] and
having to pass 'chrd' sort of negates that). Hopefully, someone else
will offer a suggestion for getting rid of that requirement.

....you may not like the Song class bit I added either...

Phil
 
P

Phil Tomson

I've begun working on a music-related ruby project, and recently I've
been pondering the idea of a music composition environment somewhat
similar to cm, clm and friends for lisp, but in ruby instead.

Today I was thinking about how ruby blocks make constructing
domain-specific languages a snap. I've been reading Lisp as a Second
Language at http://www.ircam.fr/equipes/repmus/LispSecondLanguage/, a
Lisp tutorial for musicians, and was considering using a list-based
structure for storing music. I sorta envisioned the language as
looking something like:

clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

In this example, a-g are methods returning note objects.

What I'd like is to have that return a list like so:

["c", "d", "e", "f", "g", "a", "b", "c", ["c", "e", "g"]]

I did a little more work on this; now I don't have to pass in a list to
the notes from within the chord block:

module Music

def a(list=@tmp_notes)
list << "a"
end

def b(list=@tmp_notes)
list << "b"
end

def c(list=@tmp_notes)
list << "c"
end

def d(list=@tmp_notes)
list << "d"
end

def e(list=@tmp_notes)
list << "e"
end

def f(list=@tmp_notes)
list << "f"
end

def g(list=@tmp_notes)
list << "g"
end

def clef
puts "...In Clef..."
yield
@notes = @tmp_notes
@notes << @chord_notes unless @chord_notes.empty?
return @notes
end

def measure(time)
puts "...In Measure..."
yield
end

def chord
#make a local copy of @tmp_notes
tmpnotes = @tmp_notes.dup
@tmp_notes.clear
puts "...In Chord..."
yield @chord_notes
@chord_notes = @tmp_notes
#restore @tmp_notes
@tmp_notes = tmpnotes
end

def notes
@notes
end
end

class Song
include Music
def initialize
@notes = []
@chord_notes=[]
@tmp_notes = []
end
end

class MySong < Song
include Music
def initialize
super
clef do
measure("4/4") do
c; d; e; f; g; a; b; c;
chord do
c; d; g;
end
end
end
end
end

mysong = MySong.new
p mysong.notes


So that gets rid of some of the ugliness that the user of your music
language has to know (you want them to not have to know Ruby at all), but
the super is still there.

Perhaps you could do something like this instead of subclassing Song, you
could do (untested):

Defined by you:
class Song
include Music
def initialize
@notes = []
@chord_notes=[]
@tmp_notes = []
yield
end
end


Defined by user in a different file:

my_song= Song.new {
clef do
measure("4/4") do
c; d; e; f; g; a; b; c;
chord do
c; d; g;
end
end
end
}

The advantage being that you can eliminate the need for the user to have
to define a class and call super. You could even add more methods to
either Song or Music so that you could do:

my_song.play

....but I'm not sure that will work; the block being passed to Song.new has
to know about all of the methods we're using in it (a-f refer to
@temp_list which doesn't have a context in this block). There's probably
some way of doing this...

Phil
 
J

Jim Freeze

clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

In this example, a-g are methods returning note objects.

What I'd like is to have that return a list like so:

["c", "d", "e", "f", "g", "a", "b", "c", ["c", "e", "g"]]

But, as it stands, each expression is evaluated and discarded. I'd
like to avoid having to include actual lists in the blocks; while
arbitrary ruby expressions would be possible, I'm aiming to hide much
of that from the casual user.

At the basic level do you want

chord { c e g }

to return ["c", "e", "g"]?

It seems that you want to generate a list
using a lisp syntax. And to do that inside
a block, you probably need a hook to catch
the return value of each statment.
I'm not sure that would be advisable.

Have you considered parsing a string and using eval?
For example, do:

chord %{ c e g }


Of course, some smarter people than me may provide
the answer you are looking for.
 
J

Joel VanderWerf

Phil said:
Of course, the ugly bit is the part in the chord block: you have to
pass in 'chrd' to all of the a-g methods to get those notes to be added to
the correct list (@chord_list) which then gets appended to the @notes
list. So it's still not quite what you want (I understand that you want
people to see very little of the 'wizard behind the curtain' [Ruby] and
having to pass 'chrd' sort of negates that). Hopefully, someone else
will offer a suggestion for getting rid of that requirement.

I suggest our old friend instance_eval.

--

diff -u music-orig.rb music.rb
--- music-orig.rb 2003-09-27 22:04:11.000000000 -0700
+++ music.rb 2003-09-27 22:03:43.000000000 -0700
@@ -41,8 +41,9 @@

def chord
puts "...In Chord..."
- yield @chord_notes
- @notes << @chord_notes
+ @chord_notes.instance_eval &Proc.new
+ @notes << @chord_notes.notes.dup
+ @chord_notes.clear
end

def notes
@@ -50,23 +51,32 @@
end
end

+class Chord
+ include Music
+ def initialize
+ @notes = []
+ end
+ def clear
+ @notes.clear
+ end
+end
+
class Song
include Music
def initialize
@notes = []
- @chord_notes=[]
+ @chord_notes=Chord.new
end
end

class MySong < Song
- include Music
def initialize
super
clef do
measure("4/4") do
c; d; e; f; g; a; b; c;
- chord do |chrd|
- c(chrd); d(chrd); g(chrd);
+ chord do
+ c; d; g;
end
end
end
 
J

Joel VanderWerf

What about an algebraic approach?

Instead of

clef do
measure("4/4") do
c; d; e; f; g; a; b; c;
chord do
c; d; g;
end
end
end

maybe

clef do
measure("4/4") do
c + d + e + f + g + a + b + c + c*d*g
end
end

You could denote quarter notes with b/4 etc.
 
P

Phil Tomson

Here's take 3. This one seems to work:

module Music
def initialize
@notes = []
@chord_notes=[]
@tmp_notes = []
end

def a(list=@tmp_notes)
list << "a"
end

def b(list=@tmp_notes)
list << "b"
end

def c(list=@tmp_notes)
list << "c"
end

def d(list=@tmp_notes)
list << "d"
end

def e(list=@tmp_notes)
list << "e"
end

def f(list=@tmp_notes)
list << "f"
end

def g(list=@tmp_notes)
list << "g"
end

def clef
puts "...In Clef..."
yield
@notes = @tmp_notes
@notes << @chord_notes unless @chord_notes.empty?
return @notes
end

def measure(time)
puts "...In Measure..."
yield
end

def chord
tmpnotes = @tmp_notes.dup
@tmp_notes.clear
puts "...In Chord..."
yield @chord_notes
@chord_notes = @tmp_notes
@tmp_notes = tmpnotes
end

def notes
@notes
end
end

class Song
include Music

def define_song(&b)
instance_eval &b
end
end

mysong= Song.new
mysong.define_song {
clef do
measure("4/4") do
c; d; e; f; g; a; b; c;
chord do
c; d; g;
end
end
end
}
p mysong.notes


This way the user doesn't even have to define a class, call super and all
those Ruby details.


Thanks to Joel for the instance_eval suggestion.

Phil
 
D

daz

Jim Freeze said:
Have you considered parsing a string ...
For example, do:

chord %{ c e g }


Like Jim, my feeling is that some parsing will be
required. There's no C# method in Ruby.

Allows for sharps, flats and naturals.

My effort was also helped by Joel's input.

Unique Note objects are created for no reason other
than folks (and my) amusement.


#====================

module Music

class Clef
def initialize
@notes = []
end

def time_sig(beats, beatval)
@beats = beats
@beatval = beatval
end

def chord(c)
@notes << c.split.map {|ce| Note[ce] }
end

def notes(n=nil)
if n
@notes << n.split.map {|ne| Note[ne] }
else
@notes
end
end

def inspect
@n
end
end

class Note
private_class_method :new
@@note_h = Hash[]

def Note.[](n)
puts "Bum Note - #{n}" if n !~ /[a-g][#_@]?/i
nn = @@note_h[n] || @@note_h[n] = new(n)
end

def initialize(n)
@n = n
end

def inspect
@n
end
end

def clef(&b)
clf = Clef.new
clf.instance_eval(&b)
r = clf.notes
p r
r
end
end

#====================

include Music

clef do
time_sig(4,4)
notes %{d e f# g a b c# d}
chord %{c d g}
end

clef do
time_sig(3,4)
notes %{a_ b@ c# d}
chord %{d k}
end

#====================

#-> [[d, e, f#, g, a, b, c#, d], [c, d, g]]
#-> Bum Note - k
#-> [[a_, b@, c#, d], [d, k]]


daz
 
D

daz

I said:
def chord(c)
@notes << c.split.map {|ce| Note[ce] } # OK
end

def notes(n=nil)
if n
@notes << n.split.map {|ne| Note[ne] } # Oops !!
# ^^

@notes += n.split.map {|ne| Note[ne] }
else
@notes
end
end

#====================

Oops, I pushed the note array instead of joining.
Don't want people to think I don't care ;-)

#-> [d, e, f#, g, a, b, c#, d, [c, d, g]]


daz
 
R

Ryan Pavlik

Like Jim, my feeling is that some parsing will be
required. There's no C# method in Ruby.

Allows for sharps, flats and naturals.

There are however unary +, -, and ~ operators, which might translate
appropriately.

However there are a lot of intricacies to music in general, and I'm
not sure any of the given methods quite handle them all. Some
everyday things to think about:

* Tuples
* Multiple voices (not just simple chords)
* Note length tweaks (for slurs and the like)
* Rests (big one)
* Tempo changes
* Dynamics

Arrays might help some things, as well as combining voices on the fly,
but it may not be as straightforward as desired.

Hmm, a little method_missing magic and we could have a vaguely
track-like view perhaps:

part {
bar 16 # Set each _ to be a 16th, so ____ is 1/4

v1 a____, r_, ...
v2 r_, b__, +c__, ... # r is for rest

v1 ...
v2 ...
}

In this case, you could chop off the _'s and count them in
method_missing to produce a length, call the method with that as a
parameter, returning a Note modified by the current key signature, and
then the +/-/~ would take over. Each vN function would be a "voice"
function that was later collected and combined by part(), so you could
handle multiple voice movement without a lot of crazy arrays and
stuff. Of course, you'd likely still want to allow [a_, +c_, e_] to
give you a chord in a particular voice for convenience.

Anyhow, just a thought, still a number of issues to be dealt with I'm
sure.
 
S

Sabby and Tabby

I sorta envisioned the language as looking something like:

clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

In this example, a-g are methods returning note objects.

What I'd like is to have that return a list like so:

["c", "d", "e", "f", "g", "a", "b", "c", ["c", "e", "g"]]

Possible. But not for long.

#!/usr/bin/ruby -w

def clef
yield
end

def measure(m)
@lines = []
yield
@lines.inject {|a,b| a.push(*b)}
end

def chord
@lines[-1] = [yield]
end

('a'..'g').each {|x| eval <<""}
def #{x}(*a)
@lines << [] if a.empty?
@lines[-1].unshift('#{x}')
end

notes = clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

p notes

__END__
../gem:25: warning: parenthesize argument(s) for future version
../gem:25: warning: parenthesize argument(s) for future version
../gem:25: warning: parenthesize argument(s) for future version
../gem:25: warning: parenthesize argument(s) for future version
../gem:25: warning: parenthesize argument(s) for future version
../gem:25: warning: parenthesize argument(s) for future version
../gem:27: warning: parenthesize argument(s) for future version
["c", "d", "e", "f", "g", "a", "b", "c", ["c", "e", "g"]]
 
D

Dan Doel

Sabby said:
('a'..'g').each {|x| eval <<""}
def #{x}(*a)
@lines << [] if a.empty?
@lines[-1].unshift('#{x}')
end
I didn't realize that the first line didn't count for here documents, so you
could close blocks and such. I thought you would have needed:

('a'..'g').each {|x| eval <<""
def #{x}(*a)
@lines << [] if a.empty?
@lines[-1].unshift('#{x}')
end

}

You learn something new every day, I guess.

- Dan
 
H

Hal Fulton

Dan said:
Sabby said:
('a'..'g').each {|x| eval <<""}
def #{x}(*a)
@lines << [] if a.empty?
@lines[-1].unshift('#{x}')
end
I didn't realize that the first line didn't count for here documents, so
you
could close blocks and such. I thought you would have needed:

('a'..'g').each {|x| eval <<""
def #{x}(*a)
@lines << [] if a.empty?
@lines[-1].unshift('#{x}')
end

}

You learn something new every day, I guess.

What? I'm confused. Are you saying that there are
two blank lines required at the end of this doc?

Personally I've never used "" as a terminator
anyway.

Hal
 
D

Dan Doel

Hal said:
What? I'm confused. Are you saying that there are
two blank lines required at the end of this doc?

Personally I've never used "" as a terminator
anyway.

No, just one. Anything more is Thunderbird making it hard for me to format
messages precisely.

- Dan
 
M

Martin DeMello

Sabby and Tabby said:
Possible. But not for long.

#!/usr/bin/ruby -w

def clef
yield
end

def measure(m)
@lines = []
yield
@lines.inject {|a,b| a.push(*b)}
end

def chord
@lines[-1] = [yield]
end

('a'..'g').each {|x| eval <<""}
def #{x}(*a)
@lines << [] if a.empty?
@lines[-1].unshift('#{x}')
end

notes = clef do
measure("4/4") do
c d e f g a b c
chord do
c e g
end
end
end

p notes

Beautiful. Pity it's going away.

martin
 
H

Hal Fulton

Martin said:
Beautiful. Pity it's going away.

I agree. But "poetry mode" is not totally going away,
is it? Don't we just get the warning in certain
circumstances?

What are the facts here?

Hal
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,598
Members
45,147
Latest member
CarenSchni
Top