Erector Design Issue: Promises, Promises

10 views
Skip to first unread message

Alex Chaffee

unread,
Aug 3, 2011, 3:48:06 PM8/3/11
to erector, Viktor Trón
Issue: Promises

A promise is basically a tag (element method) that's already started
to render itself onto the output stream. But you can tell it to change
its mind and add more attributes to the open tag. This allows
dot-class and dot-id-bang shortcuts.

div.foo "bar" #=> "<div class='foo'>bar</div>"

The first "div" renders-- sorry, emits "<div></div>" and returns a promise.

Calling "foo" on the promise rewinds the output stream and then writes
"<div class='foo'>bar</div>"

Promises also allow you to chain strings and calls together as
parameters to a single method call, like this:

text "I", b("love"), "cheese"

Note that "b" has already been called, and therefore written
"<b>love</b>" to the output. But since the "b" returns a promise, the
text method can rewind and replay the args in the right order, so

"I"
"<b>love</b>"
"cheese"

end up on the output buffer.

(Extra bonus coolness:

text "I", b("love"), "cheese!", :join => " frickin "
#=> "I frickin <b>love</b> frickin cheese!"

)

I think there's something powerful lurking under the surface here.
Like maybe Tags and Elements and Widgets are all really Promises and
we could get the power of returning and emitting simultaneously and
interchangeably... something that seems both functional and OO... both
hierarchical and streaming... or maybe I'm just suffering from
schizophrenic visions. I dunno yet.

see https://github.com/pivotal/erector/blob/master/lib/erector/promise.rb

---

P.S. After I named this class, I learned that the term Promise means
something in Clojure. This has nothing to do with that. It's more like
a Future but it's not really like that either. Maybe it should just be
named Element. Or more generically, Chunk (having nothing to do with
the other 80 meanings of "chunk" in CS and IT).

--
Alex Chaffee - al...@stinky.com - http://alexch.github.com
Stalk me: http://friendfeed.com/alexch | http://twitter.com/alexch |
http://alexch.tumblr.com

Viktor Tron

unread,
Aug 4, 2011, 11:53:04 AM8/4/11
to erector, Alex Chaffee
Hi Alex, my 2 cents below, with constructive suggestions towards the end :)

On Wed, 03 Aug 2011 20:48:06 +0100, Alex Chaffee <al...@stinky.com> wrote:

> Issue: Promises
>
> A promise is basically a tag (element method) that's already started
> to render itself onto the output stream. But you can tell it to change
> its mind and add more attributes to the open tag. This allows
> dot-class and dot-id-bang shortcuts.
>
> div.foo "bar" #=> "<div class='foo'>bar</div>"
>
> The first "div" renders-- sorry, emits "<div></div>" and returns a
> promise.
>
> Calling "foo" on the promise rewinds the output stream and then writes
> "<div class='foo'>bar</div>"

the whole concept of an output buffer is that you can only write to it,
not sure what exact api one can assume but i dont think that we can assume
output buffers in general
to be rewindable (backward mutable) since their flushing cycle is not
controlled by you. think stream.
https://github.com/pivotal/erector/blob/master/lib/erector/output.rb#L107-120

> Promises also allow you to chain strings and calls together as
> parameters to a single method call, like this:
>
> text "I", b("love"), "cheese"

this use actually reinterprets an emit call as a return type call
which is semantically unsound (or at least its context dependence makes it
non-transparent).
it is also painfully non-automatic in similar contexts, for instance i
would expect

text "I", "love", "cheese", :join => b(' - ')

to work but it doesnt, instead it renders:

<b> -
</b>I#&lt;Erector::Promise:0x000001009b8ee8&gt;love#&lt;Erector::Promise:0x000001009b8ee8&gt;cheese

but there is a more fundamental problem: rewinding a promise means
overwriting anything in the buffer that follows the promise's onset mark.
This makes the assumption that the promise object is 'recently' emitted,
but this assumption is wrong.

text 'I'
promise = b("love")
text ' promises and ' # this is gonna disappear
text promise, "cheese"

what do you expect this to render?
well, not sure but deleting stuff i happily emitted to the buffer is
prolly not cutting it:

I<b>love</b>cheese

or this:

promise_div = div "i love promises"
text " actually i dont really like them " # this is gonna disappear
promise_div.id1!

renders:
<div id="id1">i love promises</div>

there are also further anomalies in how text content is overwritten:

promise_div = div "i love promises"
promise_div.cl1.id1! 'not that '

renders:
<div class="cl1" id="id1">not that i love promises</div>

while

promise_div.cl1 "i love promises"
promise_div.id1! 'not that '

renders:

<div class="cl1" id="id1">not that </div>

also
div("hi").id1!.cl1

and

div.id1!("hi").cl1

are not the same, etc

This is because Element (which is just a proxy to promise) handles
arguments differently
ok this is clearly a minor issue compared to the conceptual problem.

> Note that "b" has already been called, and therefore written
> "<b>love</b>" to the output. But since the "b" returns a promise, the
> text method can rewind and replay the args in the right order, so
>
> "I"
> "<b>love</b>"
> "cheese"
>
> end up on the output buffer.
>
> (Extra bonus coolness:
>
> text "I", b("love"), "cheese!", :join => " frickin "
> #=> "I frickin <b>love</b> frickin cheese!"
>
> )
>
> I think there's something powerful lurking under the surface here.
> Like maybe Tags and Elements and Widgets are all really Promises and
> we could get the power of returning and emitting simultaneously and
> interchangeably... something that seems both functional and OO... both
> hierarchical and streaming... or maybe I'm just suffering from
> schizophrenic visions. I dunno yet.
>
> see https://github.com/pivotal/erector/blob/master/lib/erector/promise.rb

all in all, i think emit and return type calls should be kept different
and explicit.
context dependent rewinding blurs this separation, seems inefficient and
non-transparent and even invalid for certain output buffers.

dot-id-bang etc syntax sugar requires too much bang for too little buck,
imho :) (you need options for other tag attributes anyway).

But, and here is the constructive part, if one insists on syntactic sugar
for id/class attributes, there are several ways to go
which avoids rewinding promises

1. use missing_method to match complex tag+attribute combos like

div_id_class

Note that since ! etc cannpt be used in method names, it will prob be
rather arbitrary to parse,
also not too concise

2. use css selector-style args

div '.class1.class2,#id' "hi"

<div class="class1 class2" id="id1">hi</div>

3. use chaining, but have an across-the-board rule that a tag with no
arguments
is not emitting, just return a promise-like thingy that caches the
attributes
and hence is chainable.

div.id1!.class1.class2 "hello"

would only render the finished div once it has an actual argument or block.
this modification is very easy to implement (just make this _render call
conditional
https://github.com/pivotal/erector/blob/master/lib/erector/promise.rb#L113).
It also seems harmless to me although one has to deal with tags used with
no arguments such as 'br'
br without args would not emit anything, so you'd have to use br '' or one
could implement banged versions of all tags to force them to emit


4. like 3, but all tag/element calls would return a promise-like thingy
put on a widget @current_promise which would hold the same promise
sequentially modified by chained calls
any element calls would emit (render) the current_promise before they move
to create their own promise
also after Widget#content is called we need to emit the last promise
surely all methods should emit without delay if they have a block or if
they render an embedded widget (in the latter case passing the output
buffer makes
erector efficient)

this solution - lets call it delayed emission - has the best of all worlds:
- keeps the current api with the chainable id/cl modifiers as is.
- allows the control of emmission and the creation of content maybe a way
to Alex's vision
- br-like argumentless tags behave normally
- semantically totally transparent
- no inefficency due to rewinding etc
- also relatively easy to implement

Note that none of the first 3 can make this work:


text "I", b("love"), "cheese!"

the last could even make text "I", b("love"), "cheese!" work but only
with one promise, with two
text "I", b("love"), b("cheese!") would emit

<b>love</b>I<b>love</b><b>cheese!</b>

since the call to b("cheese!") would emit the current promise which is
b("love") before text method is called
as you see from this, the example can work if we can make sure that
emissions are not done for arguments until
the method renders them into its own promise.
calling a method say emit_safe! would mark the promise as not to emit, so

text "I", b("love").emit_safe!, b("cheese!").emit_safe!

would render correctly.


hth

vik

Alex Chaffee

unread,
Aug 4, 2011, 1:06:27 PM8/4/11
to Viktor Tron, erector
On Thu, Aug 4, 2011 at 8:53 AM, Viktor Tron <vikto...@gmail.com> wrote:
> Hi Alex, my 2 cents below, with constructive suggestions towards the end :)
>
> On Wed, 03 Aug 2011 20:48:06 +0100, Alex Chaffee <al...@stinky.com> wrote:
>
>> Issue: Promises
>>
>> A promise is basically a tag (element method) that's already started
>> to render itself onto the output stream. But you can tell it to change
>> its mind and add more attributes to the open tag. This allows
>> dot-class and dot-id-bang shortcuts.
>>
>>    div.foo "bar" #=> "<div class='foo'>bar</div>"
>>
>> The first "div" renders-- sorry, emits "<div></div>" and returns a
>> promise.
>>
>> Calling "foo" on the promise rewinds the output stream and then writes
>> "<div class='foo'>bar</div>"
>
> the whole concept of an output buffer is that you can only write to it,
> not sure what exact api one can assume but i dont think that we can assume
> output buffers in general
> to be rewindable (backward mutable) since their flushing cycle is not
> controlled by you. think stream.
> https://github.com/pivotal/erector/blob/master/lib/erector/output.rb#L107-120

Currently there are only three types of backing store for the Output
object: String, SafeBuffer, and Array. In theory someone could make an
auto-flushing buffer but nobody has yet (unless Rails allows this and
I haven't noticed). And the block of code you highlighted fails loudly
if the buffer is not rewindable.

It's the old "immutability" debate. Immutable objects are simpler and
more predictable, but less powerful. And since Strings in Ruby are
mutable, why fight it?

>> Promises also allow you to chain strings and calls together as
>> parameters to a single method call, like this:
>>
>>    text "I", b("love"), "cheese"
>
> this use actually reinterprets an emit call as a return type call
> which is semantically unsound (or at least its context dependence makes it
> non-transparent).

I agree that it's not obvious at first glance. But it's so delicious!

And we've had lots of people get confused about the current system,
where they'd try to do things like this:

x = b("love")

and get confused when x is nil.

> it is also painfully non-automatic in similar contexts, for instance i would
> expect
>
>     text "I", "love", "cheese", :join => b(' - ')
>
> to work but it doesnt, instead it renders:
>
> <b> -
> </b>I#&lt;Erector::Promise:0x000001009b8ee8&gt;love#&lt;Erector::Promise:0x000001009b8ee8&gt;cheese

That's a bug. Thanks for finding it!

> but there is a more fundamental problem: rewinding a promise means
> overwriting anything in the buffer that follows the promise's onset mark.
> This makes the assumption that the promise object is 'recently' emitted, but
> this assumption is wrong.
>
>  text 'I'
>  promise = b("love")
>  text ' promises and ' # this is gonna disappear
>  text promise, "cheese"
>
> what do you expect this to render?
>
> well, not sure but deleting stuff i happily emitted to the buffer is prolly
> not cutting it:

Very good point! I might have to rethink this whole thing. :-(

You're right that I intended Promises to work only in the context of a
single call to #text.

> all in all, i think emit and return type calls should be kept different and
> explicit.
> context dependent rewinding blurs this separation, seems inefficient and
> non-transparent and even invalid for certain output buffers.
>
> dot-id-bang etc syntax sugar requires too much bang for too little buck,
> imho :) (you need options for other tag attributes anyway).
>
> But, and here is the constructive part, if one insists on syntactic sugar
> for id/class attributes, there are several ways to go
> which avoids rewinding promises
>
> 1. use missing_method to match complex tag+attribute combos like
>
>   div_id_class
>
> Note that since ! etc cannpt be used in method names, it will prob be rather
> arbitrary to parse,
> also not too concise

Since there can only be one id per element, you could switch the order

div_class_id!
div_class1_class2
div_class1_class2_id!
div_id!

but something smells fishy about using underscore like that, since it
interferes with Ruby's normal snake-case convention.

>
> 2. use css selector-style args
>
>   div '.class1.class2,#id' "hi"
>
> <div class="class1 class2" id="id1">hi</div>

Using strings is wrong, but what about symbols?

div :class1, :class2, :id!, "hi"

It would work, but it doesn't feel right to me. The parallel between
CSS dots and Ruby dots is too strong to ignore.

> 3. use chaining, but have an across-the-board rule that a tag with no
> arguments
> is not emitting, just return a promise-like thingy that caches the
> attributes
> and hence is chainable.
>
> div.id1!.class1.class2 "hello"
>
> would only render the finished div once it has an actual argument or block.
> this modification is very easy to implement (just make this _render call
> conditional
> https://github.com/pivotal/erector/blob/master/lib/erector/promise.rb#L113).

Not bad.

> It also seems harmless to me although one has to deal with tags used with no
> arguments such as 'br'
> br without args would not emit anything, so you'd have to use br '' or one
> could implement banged versions of all tags to force them to emit

There are already ! versions of all tags (that means "raw") but I
don't know if anyone uses them.

> 4. like 3, but all tag/element calls would return a promise-like thingy put
> on a widget @current_promise which would hold the same promise
> sequentially modified by chained calls
> any element calls would emit (render) the current_promise before they move
> to create their own promise
> also after Widget#content is called we need to emit the last promise

This was actually my first idea, but as I developed it test-first, I
found I didn't need to keep track, at least for the use cases I
tested. Clearly, you came up with more cases!

I also wasn't sure I could find all places in the code where we must
flush the promise. I didn't want to get into an unstable situation
where sometimes promised elements disappeared. Fear is the mind
killer!

> surely all methods should emit without delay if they have a block
> or if they
> render an embedded widget (in the latter case passing the output buffer
> makes
> erector efficient)

...and this has been a major sticking point in these designs.

I've considered rebuilding a version of Erector from scratch that
builds a tree of element objects (like, e.g. Nokogiri's builder
http://nokogiri.rubyforge.org/nokogiri/Nokogiri/XML/Builder.html) and
then emitting them all at the end, just to compare performance and see
how much we're really gaining from keeping output streamed vs. using
heap memory for all tags and delaying output.

> this solution - lets call it delayed emission - has the best of all worlds:
> - keeps the current api with the chainable id/cl modifiers as is.
> - allows the control of emmission and the creation of content maybe a way to Alex's vision
> - br-like argumentless tags behave normally
> - semantically totally transparent
> - no inefficency due to rewinding etc
> - also relatively easy to implement
>
> Note that none of the first 3 can make this work:
>  text "I", b("love"), "cheese!"
>
> the last could even make  text "I", b("love"), "cheese!" work but only with
> one promise, with two
> text "I", b("love"), b("cheese!") would emit
>
> <b>love</b>I<b>love</b><b>cheese!</b>
>
> since the call to b("cheese!") would emit the current promise which is
> b("love") before text method is called

That stinks.

> as you see from this, the example can work if we can make sure that
> emissions are not done for arguments until
> the method renders them into its own promise.
> calling a method say emit_safe! would mark the promise as not to emit, so
>
> text "I", b("love").emit_safe!, b("cheese!").emit_safe!
>
> would render correctly.

Also stinky. :-(

> hth

It does help! Thanks for the great debate.

- A

Alex Chaffee

unread,
Aug 4, 2011, 1:47:35 PM8/4/11
to Viktor Tron, erector
On Thu, Aug 4, 2011 at 10:06 AM, Alex Chaffee <al...@stinky.com> wrote:
>> https://github.com/pivotal/erector/blob/master/lib/erector/output.rb#L107-120
>

Another possibility (#5?): change the "rewind" semantics to "cut and
paste" (or "move").

In other words, when a Promise emits, it first removes itself from
where it had emitted before (if any).

That would require a buffer that could splice, not just slice; it
would also mean that existing promises would have to move their
pointers into the buffer showing where they were emitted before.

Reply all
Reply to author
Forward
0 new messages