Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String interpolation list item text #71

Closed
arvanasse opened this issue Oct 7, 2017 · 6 comments
Closed

String interpolation list item text #71

arvanasse opened this issue Oct 7, 2017 · 6 comments

Comments

@arvanasse
Copy link

Consider the following:

class MyClass
  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  def create_doc
    Caracal::Document.save('my_file.docx') do |document|
      document.h1 'Example'
      document.ul do
        li "Name: #{name}"
        li "Age: #{age}"
      end
    end
  end
end

MyClass.new('john', 30).create_doc
#=> NameError: undefined local variable or method `name' for #<Caracal::Core::Models::ListModel:0x007fafbe38b650>

String interpolation into a list item seems like a pretty general need for anyone attempting to dynamically generate a document. As one possible alternative, I would not mind being able to pass an enumerable data attribute to the document.ul call and let Caracal add the list items from the enumerable (much like #table). This could be done as one alternative to list generation with a fall back to the current, explicit method.

@jdugan
Copy link
Contributor

jdugan commented Oct 12, 2017

Hi, Andy.

Scoping is an fairly gnarly issue for a gem like Caracal, but I suspect there is an existing way around your issue.

What You Actually Asked Me About

First, a small bit of background.

The main difference between a Word document and an HTML document (for example) is that the Word document actually requires several output streams to be written and collected to form the "output". This complicates the rendering process for Caracal because it can't really just "render as it goes" like, say, Prawn can. For better or worse, Caracal was designed to model all DSL instructions first and only when all the instructions are stored does it attempt to produce the various output files and zip them into the final document. The reason this complicates things is because Caracal is fundamentally turning all your instructions into a series of nested model objects as it parses your commands, which by default rapidly destroys the idea of a single context in which the commands can be evaluated.

Having written that, there is generally a way to preserve outer context. In an effort to give programmers some degree of control over how their instructions are interpreted, Caracal's BaseModel implements an arity strategy I discovered in similar gems when I was designing Caracal--it chooses a block evaluation strategy based on the arity of the block. So, if you call a Caracal method without a block argument (i.e., arity < 1), the gem assumes all the variables in the block are local and it uses instance _eval(&block); if you provide a block argument, Caracal assumes the variables are not all local and uses block[self] instead--self here is the caller rather than the receiver.

Consider the following example, which removes Caracal from the equation:

class InnerClass
  attr_reader :birth_year

  def initialize(age)
    @birth_year = 2017 - age
  end

  def format(&block)
    (block.arity < 1) ? instance_eval(&block) : block[self]
  end
end

class OuterClass
  attr_reader :name, :age, :inner

  def initialize(name, age)
    @name  = name
    @age   = age
    @inner = InnerClass.new(age)
  end

  # This evaluates self in the context of the caller 
  # so it explicitly prefixes the value coming from 
  # receiver.
  #
  def print_cool
    inner.format do |ic|
      puts "#{ name } - #{ age } - #{ ic.birth_year }"
    end
  end

  # This evaluates self in the context of the caller  
  # but it fails to identify the birth year's owner. 
  # So, ruby thinks the caller owns birth_year, which 
  # is not true, hence the error.
  #
  def print_notcool
    inner.format do |ic|
      puts "#{ name } - #{ age } - #{ birth_year }"
    end
  end

  # This evaluates self in the context of the receiver, 
  # so the birth year is cool but ruby has no idea what 
  # name and age mean now, hence the error.
  #
  def print_alsonotcool
    inner.format do
      puts "#{ name } - #{ age } - #{ birth_year }"
    end
  end
end

You Didn't Ask, But I'd Appreciate Your Opinion

FWIW, I realise this feature of Caracal is not documented in any way. Probably it should be. But self is a sufficient advanced concept that you're the first person to ask me about scoping in a non-trivial way.

I genuinely don't think there's a great, auto-magical way to spare advanced users from managing scope explicitly, but I'm 100% open to a comprehensively designed alternative to what I've implemented.

I'm also not clear how best to even write up this discussion in a way that an ordinary ruby user would understand. From what I can tell, most Caracal users are building relatively simple, top-down Word documents through Rails, so I typically advise them to localise their values into simple strings/objects and reference those in their blocks. (The instance_eval calls pick these data structures fine.) My sense is if I started delving into scoping and self, I'd lose most of them immediately, you know?

@jdugan
Copy link
Contributor

jdugan commented Jan 3, 2018

A Practical Solution

I'm going to close this issue, so I wanted to follow-up in case you were still interested. (If not, that's also 100% fine.)

I added a link to this discussion in the README to help others with lexical scoping questions. I'm also going to explicitly answer your immediate question so that the issue will include a "solution" per se.

The ruby issue in your example is that the variables are implicitly referencing self.
But by the time the interpreter encounters name and age, self is no longer a instance of MyClass but rather an instance of Caracal ListItem. Caracal's ListItem model has no idea what name and age mean.

To get the example to work, we need to tell the interpreter explicitly which object owns name and age. To do so, we can employ a strategy familiar to javascripters:

class MyClass
    attr_reader :name, :age

    def initialize(name, age)
      @name = name
      @age = age
    end

    def create_doc
      obj = self
      Caracal::Document.save('my_file.docx') do |document|
        document.h1 'Example'
        document.ul do
          li "Name: #{obj.name}"
          li "Age: #{obj.age}"
        end
      end
    end
  end

  MyClass.new('john', 30).create_doc

@Uelb
Copy link

Uelb commented Jul 1, 2019

hey, I had the same problem and had a hard time finding this issue, I believe it would be nice to add that to the readme.

@jdugan
Copy link
Contributor

jdugan commented Jul 4, 2019

Hi, @Uelb. There is a direct link to this issue in the README under a section named Using Variables. Were you recommending something other than that?

@Uelb
Copy link

Uelb commented Jul 5, 2019

Actually I missed that and it may be enough ! Thanks. Maybe a better way would be to directly include the bad and the good example in the readme.

@koutzasnew
Copy link

Hi, this could also work using

@@name = name
@@age = age

inside the initialize method and then using @@name and @@age in your main code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants