Argument weirdness when defining functions

Okay, this has been bugging me for a while.
With ruby functions, you should be able to specify the name of an argument like this:
mymethod somearg: somevalue
This works for the sonic pi built-in functions such as sample, play, etc. But when I write my own functions, it seems to force me to do them in order, and ignores the names.
Here’s some sample code illustrating the problem:


define :testargs do |arg1=nil, arg2=nil, arg3=nil, arg4=nil|
  puts "arg1:"
  puts arg1
  puts "arg2:"
  puts arg2
  puts "arg3:"
  puts arg3
  puts "arg4:"
  puts arg4
end

puts "args in sequence"
testargs "a", "b", "c", "d"
puts "args by name"
testargs arg4: "d", arg3: "c", arg2: "b", arg1: "a"

Why doesn’t the second call to testargs work correctly? Is there a way to address this in how I’m defining my functions?
Thanks!

I’ve done some testing and found two problems:
The argument given to the function is {:arg1=>"1"} instead of 1
When using Ruby’s native def instead of define and arg1=1 in the function call the argument is given as 1 but the ordering still doesn’t work

def doesn’t work for me, it throws a syntax error. I’m on Version 4.5.1 ‘8oh8’.
I did some reading for how ruby handles args, and there seem to be some differences.
Any ideas how to get my methods to support keyword arguments when calling?
Also, I read about block arguments, where you can pass in a block of code. Does sonic pi support that?
Thanks!

Obviously my example code illustrating the problem is trivial. But I have more ambitious functions, with numerous args, and it would be much more user-friendly, if I wanted to override the default for the 9th argument, to not have to specify args 1-8.

This is how I do it. It does mean you have to use opts[:varname] to refer to your variables inside your function, but I’ve not found that too much of a problem.

define :testargs do | opts = {} |
  defaults = {
    arg1: "foo",
    arg2: "bar",
    arg3: 1,
    arg4: nil
  }
  
  opts = defaults.merge(opts)
  
  puts "arg1 = #{opts[:arg1]} arg2 = #{opts[:arg2]} arg3 = #{opts[:arg3]} arg4 = #{opts[:arg4]}"
end

Hmm. Okay, that’s an improvement, but then we give up supplying the args in order. I can say:


sample :ambi_choir,  amp:0.5

… utilizing an unnamed arg (which fills the first argument’s name) and a named arg, out of its declared order. And I’d like to have the same flexibility in my own methods. Any idea how I can have my cake and eat it too?
Thanks so much for your help!

And I still don’t understand why I can’t use ruby’s builtin “def”, which has all this functionality wired in. Any ideas on that?

With some AI assistance, I managed to produce the following. Is this what you were after?

define :eat_cake do | arg1=nil, arg2=nil, arg3=nil, arg4=nil, **kwargs |
  # Fill in missing named arguments from kwargs if they exist
  arg1 = kwargs[:arg1] if kwargs[:arg1]
  arg2 = kwargs[:arg2] if kwargs[:arg2]
  arg3 = kwargs[:arg3] if kwargs[:arg3]
  arg4 = kwargs[:arg4] if kwargs[:arg4]
  
  # Now you have all arguments, either from positional parameters or from kwargs
  puts "Argument 1: #{arg1}"
  puts "Argument 2: #{arg2}"
  puts "Argument 3: #{arg3}"
  puts "Argument 4: #{arg4}"
end

# Example calls:
eat_cake("h", "a", "v", "e")
eat_cake(arg4: "e", arg3: "k", arg2: "a", arg1: "c")

That works great! Thanks!
All hail our artificial overlords!

Okay, I’ve been working at this to sweeten it a bit, and make it smarter with some metaprogramming.
Here’s what I came up with:


define :overrideargs do |kwargs, params, arglistname="kwargs"|
  kwargs = {} if kwargs == nil
  params.collect! {|x| x=x[1]} #params is an array of arrays, with the name as the 2nd item in each nested array. This strips & flattens
  kwargcmdlist = ""
  kwargs.each do |argname|
    kwargcmdlist += argname[0].to_s + " = " + arglistname.to_s + "[:" + argname[0].to_s + "] if " + arglistname.to_s + "[:" + argname[0].to_s + "]\n" if params.include? argname[0]
  end
  kwargcmdlist
end




define :eat_cake do | arg1=nil, arg2=nil, arg3=nil, arg4=nil, **kwargs |
  
  
  eval overrideargs(kwargs, method(__method__).parameters)
  
  
  # Now you have all arguments, either from positional parameters or from kwargs
  puts "Argument 1: #{arg1}"
  puts "Argument 2: #{arg2}"
  puts "Argument 3: #{arg3}"
  puts "Argument 4: #{arg4}"
end




eat_cake "zip", arg4: "foo", arg2: "bar", garbage: "trash"



Now, all I need to do is add **kwargs as the last argument, and put one line of code at the top of each method. Clean and simple, and smart enough to ignore bad parameter names.

1 Like

One more refinement… I made it optional whether to ignore new arguments, and put in a message to stdout when a bad param is flagged:


define :overridekwargs do |kwargs, params, ignorenewargs=true, arglistname="kwargs"|
  kwargs = {} if kwargs == nil
  params.collect! {|x| x=x[1]} #params is an array of arrays, with the name as the 2nd item in each nested array. This strips & flattens
  kwargcmdlist = ""
  kwargs.each do |argname|
    puts "argname: " + argname[0].to_s
    puts "params include argname? " + (params.include? argname[0]).to_s
    puts "ignore new? " + ignorenewargs.to_s
    if params.include? argname[0] or !ignorenewargs
      kwargcmdlist += argname[0].to_s + " = " + arglistname.to_s + "[:" + argname[0].to_s + "] if " + arglistname.to_s + "[:" + argname[0].to_s + "]\n"
    else
      puts argname[0].to_s + " is not a valid param -- did you make a typo?"
    end
    
  end
  kwargcmdlist
end


Hope others find this useful!

Okay, I ran into a new wrinkle.
I wrote 2 methods. One, called arpeggiate, uses the method described above to pass in extra args, to be used as synth defaults. It works just fine. The second, strum, is basically a convenience wrapper for arpeggiate, to make it easy to do strumming sounds. Here’s the code (which is not working):



define :strum do |thesenotes,strumspeed=0.05,totaltime, **synthdefaults|
  lastnotelength=totaltime - (strumspeed * (thesenotes.length - 1))
  thesedelays= knit(strumspeed, thesenotes.length - 1)
  (thesedelays = thesedelays.to_a) << lastnotelength
  arpeggiate thesenotes, thesedelays, synthdefaults
end
 

The problem is in the last line. When I try to pass in the synthdefaults, it throws an error saying:
“wrong number of arguments (given 3, expected 2) (ArgumentError)”
If I eliminate the synthdefaults arg, the call works fine.
What’s the method to pass through the extra args from a wrapper method to a nested method being called? Any ideas?
Thanks!
BTW, here’s how I’m declaring arpeggiate:

define :arpeggiate do |thesenotes, thesedelays, **synthdefaults|

Try this:

arpeggiate thesenotes, thesedelays, *synthdefaults

This also seems to work, I’m not sure what the difference is:

arpeggiate thesenotes, thesedelays, **synthdefaults

My understanding is that one splat gives an array of unnamed args, so you need to rely on position, where two splats gives you a hash, so each item has a name.

Just tested it, *synthdefaults doesn’t work. It throws “syntax error, unexpected *”. Looks like it needs to be ** to force a hash. Apparently an implicit hash is created when adding arg: value pairs in a method call.

I finally brute-forced it, by building a command string and massaging the **synthargs into individual arguments. A kludge, but it works.
I’d still like a more elegant solution, if anyone has one.

If you can post the code you’ve got, then people could try playing around with it and improving it.

Sure. Here’s the code. The debugprint is a souped-up puts command.

define :passkwargsthrough do |cmd, kwargs|
  
  if kwargs == nil
    debugprint "no args, clean call"
  else
    debugprint "got kwargs, cooking cmd"
    comma = ","
    if !cmd.include? " "
      debugprint "no space, zapping first comma"
      comma = ""
    end
    debugprint "comma: ", comma
    kwargs.each do |key, value|
      debugprint "key: ", key
      debugprint "value: ", value
      if ringorlist value
        debugprint "value is a ring or list"
        value = value.to_a
        debugprint "value: ", value
      end #if ring or list
      cmd += comma + key.to_s + ": " + value.to_s
      comma = ","
    end #each key value pair
    debugprint "cmd: ", cmd
    cmd #return value
  end
  
  
  
end


define :strum do |thesenotes,strumspeed=0.05,totaltime, **synthdefaults|
  debugprint "top of strum"
  thesenotes = cleanchordorscale thesenotes
  debugprint "clean version of thesenotes: ", thesenotes
  
  lastnotelength=totaltime - (strumspeed * (thesenotes.length - 1))
  thesedelays= knit(strumspeed, thesenotes.length - 1)
  (thesedelays = thesedelays.to_a) << lastnotelength
  debugprint "final version of thesedelays: ", thesedelays
  
  cmd = "arpeggiate " + thesenotes.to_s + ", " + thesedelays.to_s
  cmd = passkwargsthrough cmd, synthdefaults
  debugprint "cmd: ", cmd
  eval cmd
end


use_synth :piano

strum (chord :c4, "m7"), 0.05, 2, amp: [0.1, 0.3, 0.5, 1], cutoff: 20


As I said, not elegant, but it’s a kludge that works.
I’m building a library of useful methods that make life easier in sonic pi, including lfos, envelopes, trancegates, easy transposing of pitched samples, etc. But I’m bogged down debugging a huge complex method for arranging multiple voices. Once I got the bugs out, I’ll be putting it on github and sharing it here. Hopefully in a week or so.

OK, I think I see how to do it. You don’t need passkwargsthrough. This seems to work:

define :strum do |thesenotes,strumspeed=0.05,totaltime, **synthdefaults|
  puts "top of strum"
  thesenotes = cleanchordorscale thesenotes
  puts "clean version of thesenotes: ", thesenotes
  
  lastnotelength=totaltime - (strumspeed * (thesenotes.length - 1))
  thesedelays= knit(strumspeed, thesenotes.length - 1)
  (thesedelays = thesedelays.to_a) << lastnotelength
  puts "final version of thesedelays: ", thesedelays
  
  arpeggiate thesenotes, thesedelays, **synthdefaults
end

So simple!
Thanks so much, that is indeed an elegant solution.

1 Like