1

I have a ruby code that currently look like this:

grades.sum{|g| g.grade * g.weight}

For a user maintained DSL, I would like to implement something like this:

grades.sum('grade' * 'weight')

Is this possible?

sawa
  • 165,429
  • 45
  • 277
  • 381
f0i
  • 167
  • 3
  • 9

3 Answers3

6

No, it's not possible unless you monkeypatch the class String to add your application logic (please don't do that). But this -which even looks beter- is possible if you play a little bit with instance_eval:

grades.sum { grade * weight }

For example, let's assume grades is an array:

module Enumerable
  def sum(&block)
    inject(0) { |acc, x| acc + x.instance_eval(&block) }
  end
end
tokland
  • 66,169
  • 13
  • 144
  • 170
  • I like this better than my solution. If the curly braces are acceptable instead of parentheses, this reads much more nicely, IMO. – Chris Heald Nov 03 '12 at 15:33
  • +1, your answer is getting close to also what I need, and what has been solved on SO already, and what I had no time to investigate until now. But I have an impression that there was just something a bit more to it... I'll be back after some searching. – Boris Stitnicky Nov 03 '12 at 18:19
  • Got it. It's [this SO question](http://stackoverflow.com/questions/10058996) and [this tool by Niklas B.](https://github.com/niklasb/ruby-dynamic-binding) that fully treat this problem. – Boris Stitnicky Nov 03 '12 at 19:56
1

Strictly speaking, no, because the Ruby interpreter will attempt to multiply the strings "grade" and "weight" together before passing them as a parameter to "sum". You could achieve it by monkeypatching String to override all the operations you want to permit, but that's an awful idea and will be a maintenance nightmare.

Now, that said, you have a couple of ways that you could achieve this. If you require a full string as the parameter:

grades.sum('grade * weight')

Then, you can use instance_eval to eval that code in the context of the value you're passing to #sum. The implementation would look something like:

class Grade
  def weight
    0.5
  end

  def grade
    75
  end
end

class Array
  def sum(code = nil)
    if block_given?
      inject(0) {|s, v| s + yield(v) }
    else
      sum {|v| v.instance_eval code }
    end
  end
end

grades = Array.new(5, Grade.new)
puts grades.sum("weight * grade")

# Expected output: 5 * 75 * 0.5 = 187.5
#
# Actual output:
# % ruby grades.rb
# 187.5

This also preserves your original block form:

puts grades.sum {|g| g.weight * g.grade } # => 187.5
Chris Heald
  • 61,439
  • 10
  • 123
  • 137
0

You might want to take a look at this SO question and this code by Niklas B.

Community
  • 1
  • 1
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74