Repository navigation
String interpolation list item text #71
Description
Activity
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 usesblock[self]instead--selfhere 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
selfis 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_evalcalls pick these data structures fine.) My sense is if I started delving into scoping andself, I'd lose most of them immediately, you know?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 encountersnameandage,selfis no longer a instance of MyClass but rather an instance of Caracal ListItem. Caracal's ListItem model has no idea whatnameandagemean.To get the example to work, we need to tell the interpreter explicitly which object owns
nameandage. 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
Reacted by Goulven Champenoishey, 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.
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?
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.
Hi, this could also work using
@@name = name @@age = ageinside the initialize method and then using
@@nameand@@agein your main code.
Consider the following:
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
dataattribute to thedocument.ulcall 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.