mustache / spec

The Mustache spec.
MIT License
361 stars 71 forks source link

Why "Lambdas used for inverted sections should be considered truthy"? #128

Closed determin1st closed 8 months ago

determin1st commented 3 years ago

shouldn't it be (for inverted)

plus (maybe)

jgonggrijp commented 3 years ago

No.

There is a subtle difference in the treatment of context methods depending on whether the lambdas extension is implemented. Suppose that we have the following template (with a non-inverted section, but bear with me for a moment):

{{#meaning}}42{{/meaning}}

and that the context is the following JavaScript object:

{
    meaning: function(sectionText) {
        return '<b>' + sectionText + '</b>';
    }
}

then a Mustache engine that doesn't implement the lambdas extension will render 42, while an engine that does will render <b>42</b>. The reason for this is that without the lambdas extension, the return value of a context method is considered just another value, like any other value that you might resolve from the context. With the lambdas extension, on the other hand, the return value of a context method is interpreted as template text that replaces the original content of the section.

(The distinguishing feature of the lambdas extension is not that lambdas/functions/methods are invoked in the first place. The basic interpolation, sections and inverted specs already require that they are invoked and that their return values are used. The real distinguishing feature is that lambdas/functions/methods gain additional influence over what is rendered. I didn't realize this myself until I read the specs a few times.)

Inverted sections obviously need to behave opposite to regular sections. According to the lambdas extension, a section will invoke a context method in order to establish the inner template text to be rendered. So an inverted section will NOT invoke a context method, let alone render whatever template text it might return.

determin1st commented 3 years ago

The basic interpolation, sections and inverted specs already require that they are invoked and that their return values are used.

where? there is no lambdas in interpolation and inverted test files (only variables there). lambdas are in lambdas testfile. i don't get what you mean by that.

So an inverted section will NOT invoke a context method..

if it doesn't support lambdas, sure, it will not.. same logic may be applied to normal section (why not?), #section will not invoke anything and will give 42 result (just because it's a truthy variable) - according to the lambda extension? i'm not getting this logic..

there is no point in compatibility with lambda and non-lambda engines by the means of making

if lambda and lambda() => content
if not lambda => content

because still, it will give different results (42 without and <b>42<b> with) it may be solved by implementing an option of compatibility in the engine with lambdas, like lambdas: false, then it will not invoke lambdas at all:

if lambda => content
if not lambda => content

and will be variables first compatible.. otherwise

if lambda and lambda(text) => content
if not lambda or not lambda() => content

is more appropriate (with lambdas).. maybe i miss something.. but it appears unlikely..

jgonggrijp commented 3 years ago

You are right to point out that there is no test for lambdas in the sections spec. However, the overview of the sections spec does prescribe that lambdas must be invoked and that their return values must be used for name resolution:

https://github.com/mustache/spec/blob/bb63070e701e6e5ba9fa0b4adaa259a0ef8115be/specs/sections.yml#L18-L22

You find exactly the same prescription in the inverted spec:

https://github.com/mustache/spec/blob/bb63070e701e6e5ba9fa0b4adaa259a0ef8115be/specs/inverted.yml#L19-L23

As I wrote before, that is already the specified behavior without even including the lambdas extension. It is important to realize that this takes place as a step during name resolution. Name resolution must be fully complete before the engine even starts to determine what will be rendered. So in all cases (variables, sections and inverted sections, whether the lambdas extension is implemented or not), taking inspiration from your notation, the following is what happens during name resolution:

value := name from context
if value is lambda => use value() as value

Without the lambdas extension, section rendering proceeds as follows:

if value is not array =>
    if value is truthy => use [value] as value
    if value is falsy => use [] as value
render original section template once for each element of value

The inverted section applies the same conditions, except that it only renders the original section template if value is empty.

With the lambdas extension, there is an additional condition before section rendering starts:

if value was obtained by invoking lambda =>
    use value as raw template text that replaces original section content
    render replaced section once with current context
    stop here
else continue as regular section

You find that prescribed over here:

https://github.com/mustache/spec/blob/bb63070e701e6e5ba9fa0b4adaa259a0ef8115be/specs/%7Elambdas.yml#L10-L13

While it's left somewhat implicit, I think the appropriate opposite behavior for inverted section is as follows:

if value was obtained by invoking lambda =>
    use value as raw template text that replaces original section content
    render replaced section zero times with current context
    stop here
else continue as regular inverted section

and as far as I can tell, the particular test that you're asking about agrees with this interpretation. Note that since "once" becomes "zero times", the inverted section effectively never renders a lambda, so the Mustache engine can optimize the lambda invocation away.

determin1st commented 3 years ago

now i get it, i missed the object.method call.. let me examine the logic.. will return to it today..

determin1st commented 3 years ago

Ooke, let's clarify some wording..

lambda is an anonymous function, called without specified object/context. method is a function called within specified object/context.

Take a look at this JavaScript object (last lambdas test):

data:
  static: 'static'
  lambda: function(txt) {return false}

when the name lambda is resolved, as described in the spec, the data.lambda(text) should be invoked because it's a property of the data object. This objects your statement about not doing a call.

Same goes with JavaScript arrays, they are considered objects. Which is false for PHP.

My opinion is that the name resolution should be separated from the knowledge of variable/section/inverted. This way, no object.method call will be made, only traversal. When the object.method is found as the last name (for example {{#lastName}} or {{#here.goes.lastName}}), it should be wrapped with it's context and passed as a result value. Later, it may be called or not called.

Why not call?

It was already prescribed to call them. The only reason i see for no-call is the section check first:

value := resolveFromContext(name)
if invertedSection
  if value
    => render empty
  otherwise
    => render non-empty
otherwise
  => handle normal section

vs falsy check first:

if not value := resolveFromContext(name)
  if invertedSection => render non-empty
  otherwise => render empty

if isFunction(value)
  if isWrapped(value)
    value := unwrap(value)(text)
  else
    value := value(text)

if invertedSection
  # ...

im really lost with this spec :)

jgonggrijp commented 3 years ago

Please excuse me for quote-sniping you a little.

lambda is an anonymous function, called without specified object/context. method is a function called within specified object/context.

Depending on the language, a lambda might be a closure, in which case it might actually be bound to a particular object/context. I've been carefully avoiding the word "lambda" alone when referring to callable functions, because of the wild variety of possible interpretations. With the Mustache spec in mind, "lambda" should be mostly thought of as a feature of the Mustache templating language, rather than a feature of a programming language.

As you probably already realized, a lambda in Mustache will be a method in the underlying programming language most of the time.

Take a look at this JavaScript object (last lambdas test):

data:
  static: 'static'
  lambda: function(txt) {return false}

when the name lambda is resolved, as described in the spec, the data.lambda(text) should be invoked because it's a property of the data object.

So far, I think I understand what you mean, ...

This objects your statement about not doing a call.

but here, I'm losing you. Do you mean that there is a conflict with what I've written so far? I'm not seeing it.

Same goes with JavaScript arrays, they are considered objects. Which is false for PHP.

I fail to see how this is relevant.

My opinion is that the name resolution should be separated from the knowledge of variable/section/inverted. This way, no object.method call will be made, only traversal. When the object.method is found as the last name (for example {{#lastName}} or {{#here.goes.lastName}}), it should be wrapped with it's context and passed as a result value. Later, it may be called or not called.

In that case, I think the spec agrees with you. Methods earlier in the chain still need to be invoked at this stage, though. In your example above, if here.goes is a method, then it must be invoked in order to obtain an object that has lastName as a member.

Why not call?

It was already prescribed to call them.

Here is the full algorithm that the spec prescribes (as far as sections and inverted sections are concerned):

frame, value := resolveFromContext(startingContext, name)

if isFunction(value)
    if lambdasImplemented
        finalValue := startingContext
        if invertedSection => renderedSectionTemplate := ""
        otherwise => renderedSectionTemplate := frame.value(originalSectionTemplate)
    otherwise
        finalValue := frame.value(originalSectionTemplate)
        renderedSectionTemplate := originalSectionTemplate
otherwise
    finalValue := value
    renderedSectionTemplate := originalSectionTemplate

if isArray(finalValue) => contextList := finalValue
otherwise
    if truthy(finalValue) => contextList := [finalValue]
    otherwise => contextList := []

if invertedSection
    if contextList empty => render renderedSectionTemplate with startingContext
    otherwise => render empty
otherwise
    for renderedContext in contextList
        render renderedSectionTemplate with renderedContext
determin1st commented 3 years ago

according to your code, when isFunction and lambdasImplemented the assignment if invertedSection => renderedSectionTemplate := "" and later isArray check with context manipulation don't do any good, it will always be render empty .. because template set empty.

take a look at my interpretation: https://github.com/determin1st/sm-mustache/blob/99776b748b0b49c8c1dc0b3d4ac28c31233f7f85/mustache.php#L537-L551

un-commenting the block (after the falsy check) will comply with the lambda spec (this issue), but, it means that functionality will be removed, not added.

as i understand, the main reason why "lambdas" was made - is to replace/substitute section's content, which inverted section can't do by means of common sense, what they can, is to specify a flag (as a call result), with this test, the flag is not respected.

so the question "why" is about the flag being needed or not.

jgonggrijp commented 3 years ago

according to your code, when isFunction and lambdasImplemented the assignment if invertedSection => renderedSectionTemplate := "" and later isArray check with context manipulation don't do any good, it will always be render empty .. because template set empty.

True. As I wrote before, there is a lot that can be optimized away. My own implementation does that, too. I was just representing the un-optimized algorithm as it is described in the spec.

take a look at my interpretation: https://github.com/determin1st/sm-mustache/blob/99776b748b0b49c8c1dc0b3d4ac28c31233f7f85/mustache.php#L537-L551

un-commenting the block (after the falsy check) will comply with the lambda spec (this issue), but, it means that functionality will be removed, not added.

I can't really comment on your implementation without giving it a lot of study first, but I will take your word for it.

as i understand, the main reason why "lambdas" was made - is to replace/substitute section's content, which inverted section can't do by means of common sense, what they can, is to specify a flag (as a call result), with this test, the flag is not respected.

so the question "why" is about the flag being needed or not.

I wasn't around when the spec was written. With that out of the way, there are lots of subtle variations possible when defining the semantics for a formal language (as you illustrated with your comments). The authors need to take care to pick a combination of variations that is (a) useful, (b) practically implementable and (c) consistent.

The alternative that you are suggesting, where inverted sections are basically exempt from the lambdas extension, is arguably useful. As you wrote, doing so would allow inverted sections to make a yes/no rendering decision based on what the named function returns. However, that wouldn't be consistent. Consider the following example:

{{#myLambda}}yes{{/myLambda}}{{^myLambda}}no{{/myLambda}}

First consider the case that myLambda is a function that returns a new template. In that case, the inverted section will obviously never render unless the returned template is the empty string. What do you believe should be rendered when the returned template is, in fact, the empty string?

Next, consider the case that myLambda is a function that returns a boolean. According to what you are suggesting, the template will render "no" if it returns false. However, if the function returns true, then what do you believe should be rendered?

Finally, consider the case that myLambda doesn't exist. In that case, at least, we can probably agree that "no" should be rendered.

The algorithm in the spec avoids the contradictions that I described above. If myLambda is a function, then it basically counts as a single truthy value, so we always render the normal section with whatever it returns as a template. Otherwise, the lambdas extension doesn't apply, so we can follow the other rules on whether to apply the normal section or the inverted section. It's also useful; we can use inverted sections as a "fallback option" for when a lambda we wanted to use isn't available. I think the authors made a pretty good choice at the time.

determin1st commented 3 years ago
{{#myLambda}}yes{{/myLambda}}{{^myLambda}}no{{/myLambda}}

0) myLambda returns 'something new' string => 'something new' (first is replaced, second is truthy negated, empty) 1) myLambda returns '' empty string => no (first section is replaced with empty, second is falsy negated, rendered) 2) myLambda returns true boolean => yes (first is true, rendered, second is true negated, empty) 2) myLambda returns false boolean => no (first is false, empty, second is false negated, rendered) 3) myLambda is null => no (first is falsy, empty, second is falsy negated, rendered)

what is wrong/contradicts here?

jgonggrijp commented 3 years ago
  1. myLambda returns 'something new' string => 'something new' (first is replaced, second is truthy negated, empty)

  2. myLambda returns '' empty string => no (first section is replaced with empty, second is falsy negated, rendered)

This is contradictory. In the first case, you are interpreting the returned string as a new template. In the second case, you are effectively interpreting the string as a boolean. In order for these two conditions to be consistent with each other, you'd either have to render yes in the first case or the empty string in the second case.

determin1st commented 3 years ago

yes, because the nature of {{^section}} is the if not expression, it will interpret any returned value as a flag. with current spec it will always be true (empty):

  1. 'something new' => 'something new'
  2. '' => ''

better?

means that template {{#myLambda}}yes{{/myLambda}}{{^myLambda}}no{{/myLambda}} equals to {{#myLambda}}yes{{/myLambda}} - this is a contradiction to my taste.

jgonggrijp commented 3 years ago
  1. 'something new' => 'something new'
  2. '' => ''

better?

These two cases are no longer contradictory, but now case 1 is contradictory with both case 2 (true used as a boolean instead of as a replacing template) and case 3 (falsy return value leading to "no" being rendered in one case but not in the other). Case 0 is also contradictory with case 2.

means that template {{#myLambda}}yes{{/myLambda}}{{^myLambda}}no{{/myLambda}} equals to {{#myLambda}}yes{{/myLambda}} - this is a contradiction to my taste.

Those templates are not equivalent; they still give different results if myLambda is undefined.