1

For user convenience and more clean code I would like to write a class that can be used like this:

Encoder::Theora.encode do
  infile = "path/to/infile"
  outfile = "path/to/outfile"
  passes = 2
  # ... more params
end

The challenge now is, to have that parameters available in my encode method.

module Encoder
  class Theora
    def self.encode(&proc)
      proc.call
      # do some fancy encoding stuff here
      # using the parameters from the proc
    end
  end
end

This approach does not work. When the Proc is called, the variables are not evaluated in the context of the Theora class. Usually I would like to use method_missing to put every parameter into a class variable of class Theora, but I do not find the right way for an entry.

Can anyone point me into the right direction?

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
GeorgieF
  • 2,687
  • 5
  • 29
  • 43
  • 1
    You may want to consider changing the title of the question, it does not have very much to do with procs, but all to do with DSLs. I'd suggest changing the `proc` tag to `dsl` too. – Theo Jan 21 '11 at 08:33

4 Answers4

3

I'm not sure it's possible to get the DSL to use assignment, I think the Ruby interpreter will always assume that infile in infile = 'path/to/something' is a local variable in that context (but self.infile = 'path/to/something' can be made to work). However, if you can live without that particular detail, you can implement your DSL like this:

module Encoder
  class Theora
    def self.encode(&block)
      instance = new
      instance.instance_eval(&block)
      instance
    end

    def infile(path=nil)
      @infile = path if path
      @infile
    end
  end
end

and use it like this:

Encoder::Theora.encode do
  infile 'path/somewhere'
end

(implement the other properties similarily).

Theo
  • 131,503
  • 21
  • 160
  • 205
  • Quite unsure if it's beauty can compete with pmdboi's approach. Will play with it. Thx. – GeorgieF Jan 21 '11 at 09:43
  • I agree, pmdboi's solution is the more common way to implement a configuration DSL. Some, like Capistrano and Rails 3's routes (among others), uses styles similar to my answer, though. – Theo Jan 21 '11 at 13:19
1

It can't be done the way you've written it, AFAIK. The body of the proc has its own scope, and variables that are created within that scope are not visible outside it.

The idiomatic approach is to create a configuration object and pass it into the block, which describes the work to be done using methods or attributes of that object. Then those settings are read when doing the work. This is the approach taken by create_table in ActiveRecord migrations, for example.

So you can do something like this:

module Encoder
  class Theora
    Config = Struct.new(:infile, :outfile, :passes)

    def self.encode(&proc)
      config = Config.new
      proc.call(config)
      # use the config settings here
      fp = File.open(config.infile)       # for example
      # ...
    end
  end
end

# then use the method like this:
Encoder::Theora.encode do |config|
  config.infile = "path/to/infile"
  config.outfile = "path/to/outfile"
  config.passes = 2
  # ...
end
pmdboi
  • 534
  • 2
  • 4
  • yielding the config would be even more idiomatic, and save the runtime from having to pack the block into a proc. – Theo Jan 21 '11 at 08:31
  • That sounds absolutely like something that I was looking for. Clean, simple, selfexplanatory. Thank you. – GeorgieF Jan 21 '11 at 09:39
0

In playing around with this I arrived at the following, which I don't necessarily recommend, and which doesn't quite fit the required syntax, but which does allow you to use assignment (sort of). So peruse in the spirit of completeness:

module Encoder
  class Theora
    def self.encode(&proc)
      infile = nil
      outfile = nil
      yield binding
    end
  end
end

Encoder::Theora.encode do |b|
  b.eval <<-ruby
    infile = "path/to/infile"
    outfile = "path/to/outfile"
  ruby
end

I believe Binding.eval only works in Ruby 1.9. Also, it seems the local variables need to be declared before yielding or it won't work -- anyone know why?

zetetic
  • 47,184
  • 10
  • 111
  • 119
0

OK, first I must say that pmdboi's answer is very elegant and almost certainly the right one.

Still, just in case you want a super cut-down DSL like

Encoder::Theora.encode do
  infile "path/to/infile"
  outfile "path/to/outfile"
  passes 2
end

You can do something ugly like this:

require 'blockenspiel'
module Encoder
  class Theora
    # this replaces pmdboi's elegant Struct
    class Config
      include Blockenspiel::DSL
      def method_missing(method_id, *args, &blk)
        if args.length == 1
          instance_variable_set :"@#{method_id}", args[0]
        else
          instance_variable_get :"@#{method_id}"
        end
      end
    end

    def self.encode(&blk)
      config = Config.new
      Blockenspiel.invoke blk, config
      # now you can do things like
      puts config.infile
      puts config.outfile
      puts config.passes
    end
  end
end
Seamus Abshere
  • 8,326
  • 4
  • 44
  • 61
  • I just realized how similar my answer was to Theo's. Sorry for not recognizing that before. The difference is that I rely on Blockenspiel instead of trying to do the instance_eval stuff myself (which I don't enjoy). – Seamus Abshere Jan 27 '11 at 16:42