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

Mixin overloads from detached ruleset caller scope aren't applied #2556

Closed
SLaks opened this issue Apr 17, 2015 · 17 comments
Closed

Mixin overloads from detached ruleset caller scope aren't applied #2556

SLaks opened this issue Apr 17, 2015 · 17 comments

Comments

@SLaks
Copy link
Contributor

SLaks commented Apr 17, 2015

Source:

.Method() when (default()) {
  content: 'Not inverted';
}

.CallInverted(@callback) {
  @invert: true;
  @callback();

  .Method() when (@invert) {
    content: 'Inverted';
  }
}

.Call(@callback) {
  @invert: false;
  @callback();
}


a {
  .CallInverted({ .Method(); });
}

b {
  .Call({ .Method(); });
}

c {
  .Method();
}

Current Output:

a {
  content: 'Not inverted';
}
b {
  content: 'Not inverted';
}
c {
  content: 'Not inverted';
}

Desired Output:

a {
  content: 'Inverted';
}
b {
  content: 'Not inverted';
}
c {
  content: 'Not inverted';
}

Context: I want to make a set of mixins that are sometimes called in callbacks, and I want to make each one use a different definition from some callsites of the callbacks (the .CallInverted() mixin).

I can't find any other way to do this, because the guard variable must be defined when called in global scope, and I can't override its value when called from a mixin.

@seven-phases-max
Copy link
Member

The result is expected, since within a DR, the parent scope (incl. global) has higher precedence than the caller scope. Just the same way as it is with ordinal rulesets and parametric mixins (yes, it's the same #1316, #2435 etc. story), e.g.:

@foo: global;

@dr: {
    dr: @foo;
};

.ruleset {
    ruleset: @foo;
}

.mixin() {
    mixin: @foo;
}

div {
    @foo: caller;
    @dr();      // global
    .ruleset(); // global
    .mixin();   // global
}

I.e. in you example .Method() when (@invert) is simply never visible inside @callback because the global .Method() is matched earlier. Mixins from different scopes/namespaces can stack/cascade only when these scopes are merged before the corresponding call (or in other words only if they become a part of the same scope).

Actually from your example it looks like you tried to overcome "parent > caller" thing for the variable (@invert) by mixin overloading, did you? But missed the fact that it's the same scope story for mixins too. Maybe this minimal example will better illustrate what went wrong:

.Method() {
    content: 'global';
}

.Foo() {
    .Method();
}

.Call() {

    .Method() {
        content: 'caller';
    }

    .Method(); // -> 'caller' only (local  > global)
    .Foo();    // -> 'global' only (global > caller)
    // but in both cases only one definition is used
}

div {
    .Call();
}

@seven-phases-max
Copy link
Member

I'm marking this as "consider closing" since this is currently expected behaviour and possible improvements to be a subject for another ticket.

Feel free to enlighten more details about your specific use-case though - maybe we could come up with a workaround. I would offer something but the example is too abstract to try to approach it with different method. For instance it's not so evident why @invert is actually necessary there, e.g. .CallInverted could work w/o such flag and w/o any DR at all, but the code becomes to be too strange to look like something you had in mind:

.Method() {
  content: 'Not inverted';
}

.CallInverted() {
  .Method();
  .Method() {
    content: 'Inverted';
  }
}

.Call(@callback) {
  @callback();
}


a {
  .CallInverted();
}

b {
  .Call({.Method});
}

c {
  .Method();
}

SLaks added a commit to SLaks/Silon that referenced this issue Apr 19, 2015
This doesn't work because invertCombinator
doesn't pass through to the inner callback.
See less/less.js#2556
@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

Here is I'm actually trying to do.
Callsite, Demo (this issue breaks the lower XOR)

I'm implementing logic gates in CSS, using LESS mixins to generate CSS selectors for chained operations.
I use a detached ruleset to pass expressions like .op(#a:checked, and, #b:checked) as parameters to other .op() calls, and I use the #private.call() mixins to expand these parameters, whether they're DRs or simple selectors (strings).
This all works perfectly.

The problem is that implementing XOR requires that I invert each argument ((A && !B) || (!A && B)). Therefore, if I pass a .op(...) call to XOR, I need to generate a selector for the negation of that call. I'm trying to do that by setting @invertCombinator from the callsite of the DR, to tell the .op() call within the DR to negate itself. This is why I can't use your solution; the DR passed to .CallInverted() may be arbitrarily complicated.

I worked around the initial problem described in this issue (making the first level invert) by using a separate overload for the top-level call (the one that doesn't have @invertCombinator defined at all).
However, this still doesn't work for nested calls; it looks like a variable defined in the callsite of one DR applies to DRs defined within the first DR and overrides variables defined in the callsite of the inner DR (even it isn't in the inner DR's lexical scope?)

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

Here is a minimal example of what I'm currently stuck at:

.Method(root) {
  @invert: false;
  .Method();
}
.Method() when not (@invert) {
  content: 'Not inverted';
}
.Method() when (@invert) {
  content: 'Inverted';
}

.CallInverted(@callback) {
  @invert: true;
  @callback();
}
.Call(@callback) {
  @invert: false;
  @callback();
}


a {
  .CallInverted({ 
    .Method(); 
    inner {
      .Call ({ .Method(); });
    }
  });
}

b {
  .Call({ 
    .Method(); 
    inner {
      .CallInverted({ .Method(); });
    }
  });
}

c {
  .Method(root);
}

Actual:

a {
  content: 'Inverted';
}
a inner {
  content: 'Inverted';
}
b {
  content: 'Not inverted';
}
b inner {
  content: 'Not inverted';
}
c {
  content: 'Not inverted';
}

Desired:

a {
  content: 'Inverted';
}
a inner {
  content: 'Not inverted';
}
b {
  content: 'Not inverted';
}
b inner {
  content: 'Inverted';
}
c {
  content: 'Not inverted';
}

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

I suppose I could work around this by passing a nesting level to each .op() call and concatenating it into a variable variable to create an "array" of @invertCombinator values for the current call stack, but I'd strongly prefer to avoid polluting the callsite like that.

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

Here is a simpler interesting impossible example that would help:

.StartChain(@inner) {
  @index: 0;
  @inner();
}
.Chain(@inner, @i: @index) {
  @index: @i + 1;
  @inner();
}

a {
  .StartChain({
    i: @index;
    b {
      .Chain({
        i: @index;
          c {
            .Chain({
              i: @index;
            });
          }
      });
    }
  });
} 

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

A completely different approach I thought of for my actual problem is to pass nested lists instead of DRs:

.op(
  (#a:checked, xor, #b:checked), xor, (#c:checked, and, #d:checked));
  { #result { ... } }
);

The implementation would look like this:

#private {
  .call(@combinator, @selector, @content) when (isList(@selector)) {
  // isList() doesn't exist?
    &@{combinator} {
      .op(extract(@selector, 1), extract(@selector, 2), extract(@selector, 3));
      // I wish I could just expand the list into the arguments like JS apply()
    }
  }
}

This would essentially create a DSL.

However, I can't actually create nested lists like that. (?)

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

Also, if you make DRs support parameters (#2270), this entire problem will be instantly solved.

@seven-phases-max
Copy link
Member

I see. Well, yes, that's pretty complicated stuff to suggest something w/o understanding all aspects of it.

Also, if you make DRs support parameters (#2270), this entire problem will be instantly solved.

Sure, but notice #2270 (comment).
I.e. when/if #2270 is implemented, the "chain counter" example above will have to look something like:

.StartChain(@inner) {
  @inner(0);
}
.Chain(@inner, @i: @index) {
  @inner(@i + 1);
}

a {
  .StartChain(???(@index) {
    i: @index;
    b {
      .Chain(???(@index) {
        i: @index;
          c {
            .Chain(???(@index) {
              i: @index;
            });
          }
      });
    }
  });
}

Now taking the mentioned trick (that has essentially equal-to-2270-feature syntax except extra redundant pair of {}) - the same "chain counter" that works currently becomes:

.StartChain(@inner) {
    @inner();
    .func(0);
}

.Chain(@inner) {
    @inner();
    .func(@i + 1);
}

a {
    .StartChain({.func(@i) {
        i: @i;
        b {
            .Chain({.func(@i) {
                i: @i;
                c {
                    .Chain({.func(@i) {
                        i: @i;
                    }});
                }
            }});
        }
    }});
}

@seven-phases-max
Copy link
Member

A completely different approach I thought of for my actual problem is to pass nested lists instead of DRs:

.op(
  (#a:checked, xor, #b:checked), xor, (#c:checked, and, #d:checked));
  { #result { ... } }
);

You can actually, in simple case you can use whitespace separated lists, see for example.
Assuming you have to use ~"" hack to pass these selectors to mixin anyway, it should be no conflict between whitespace as a selector delimiter and whitespace as a list delimiter, e.g.:

.op(
  ~'#a:checked' xor ~'#b:checked', xor, ~'#c:checked' and ~'#d:checked';
  { #result { ... } }
);

Alternatively you can use something like this to be able to define an arbitrary-level nested lists with a single statement.

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

That's (adding more {}) a great idea; thanks!

The problem is that I will probably add a second variable passed in this fashion, and I don't want to have to modify every callsite each time I add a variable.

I'd like to just pass a stack depth, and use variable variables to form a call stack with "array"s of each variable I use.

However, it appears to be impossible to set a variable variable?

@invertCombinator: %("invertCombinator%s", @i);  // Syntax error :(
@@invertCombinator: true;
...
when (@@invertCombinator)

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

Re: Your second comment, I have more than two layers of nesting, and I cannot easily use plugins.

@seven-phases-max
Copy link
Member

However, it appears to be impossible to set a variable variable?

No, strictly speaking, because of Less's declarative nature, variables can't be set at all. They are all constants actually, and each time you write @var: value you define new variable (that simply overrides any previous definition).
And no, you can't define a new variable using an arbitrary string as its name (i.e. that's what @@invertCombinator: true; would do), and while this is technically possible (there were just no worths use-case offered for this), such statement would still just define new variable in the current scope and not modify such variable in outer scopes...

In other words, if there's a need for some counter it should use anything but the variable name...

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

I know; that's exactly what I want. I want to define a new constant (which must have a unique name) for each level of the call stack.

@seven-phases-max
Copy link
Member

and I cannot easily use plugins.

Sad... because after looking at all that code I started to think that doing all this via plain classical functions would be just enormously more clean and simple than all these DRs...
(e.g. xor(a, b), xor(a, and(b, c)) etc...).
(this would not be necessary that yet unfinished plugin but for example a few functions implemented in JS via @plugin or so).

@SLaks
Copy link
Contributor Author

SLaks commented Apr 19, 2015

I finished implementing the workaround; everything works now.
It'd still be nice to have better support for this (ideally, this entire issue should be transparent to the caller of the .op() mixin).

@seven-phases-max
Copy link
Member

OK, closing since the initial problem is an expected behaviour and the ticket for related improvements is #2270 (as well as other tied features in other threads).

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

2 participants