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

Imperative LESS (a.k.a. recursive variable definition is actually possible) #1678

Closed
seven-phases-max opened this issue Nov 26, 2013 · 8 comments

Comments

@seven-phases-max
Copy link
Member

Recently there was a disscussion (two questions at once actually) at the stackoverflow, one of those boring "how to increment a variable?" (i.e. #1545) So I came up with this useless (as I thought first) snippet:

// implementation:

.counter() {
    .counter-redefine();
    .counter-wrapper((@counter + 1));
}

.counter-wrapper(@new: 0) {
    .counter-redefine() {
        @counter: @new;
    }
} .counter-wrapper();

// usage:

a {
    .counter();
    counter: @counter;
}

b {
    .counter();
    counter: @counter;
}

c {
    .counter();
    counter: @counter;
}

d {
    .counter();
    counter: @counter;
}

Result:

a {
  counter: 0;
}
b {
  counter: 1;
}
c {
  counter: 2;
}
d {
  counter: 3;
}

As far as I understand it's even two issues (or at least "side effects") abused here:

  1. .counter-redefine called inside .counter is the one expanded to the global namespace by initial .counter-wrapper call in the global scope and not the one expanded by the next .counter-wrapper local scope call (the latter is not visible yet, see Use Dynamically Generated Classes as Mixins #1399 (comment) second example).
  2. .counter-wrapper call does not actually define new or modify old @counter variable so LESS does not detect any Recursive variable definition for @counter at the .counter-wrapper(...) stage, what .counter-wrapper does modify is the @new argument variable which seems to behave somewhat like a "singleton" variable regardless of the .counter-wrapper call scope. So next .counter()->.counter-redefine() call defines new @counter variable initialized with the previous @new argument value. This might be considered not an issue at all (or a very minor one at least), but when it's combined with the first one... hmm...

In other words, what we have here is simply:

@new: 0;
@counter: @new;
@new: @counter + 1;
@counter: @new;
@new: @counter + 1;
@counter: @new;
@new: @counter + 1;
// etc.

And LESS does not detect any 'recursive variable definition' just because of recursive mixins and argument variable evaluation black magic.
I doubt LESS should really detect the `recursive variable definition' in the initial snippet, but the "issue" (2) looks interesting on its own, notice the following example:

// Mixin call in .a's scope changes behaviour of a mixin unlocked in any other scope.

.Person(@gender) {
    .sayGender() {
        output: @gender;
    }
}

.Person('Male');

a {
    .Person('Female');
}

b {
    .sayGender();
}

I.e. the b scope knows nothing of the a scope, nevertheless it still can use the foreign scope value.

@scottgit
Copy link

Regarding your final example

We can see how order is important in this case of LESS calculations. In your example, b generates output: 'Female' as defined by a, but if b is moved before a, the output is output: 'Male'. So it is not that a "changes behaviour of a mixin unlocked in any other scope," but rather, it changes the behavior of all mixins used in scopes following it.

Regarding your counter idea

You actually don't even need the extra init code, as this works too:

.counter() {
    .counter-redefine();
    .counter-wrapper((@counter + 1));
}

.counter-wrapper(@new) {
    .counter-redefine() {
        @counter: @new;
    }
} .counter-wrapper(0); //init

You can actually make the counter mixin more generic, and capable of handing multiple counter routines through some creative parametric mixins and variable variable calls (tested on LESS 1.4.1):

// implementation:
@counter1: 0; //init value 
@counter2: 9; //init value 
.increment(@varName; @add: 1) { //this iterates any counter by amount given (1 is default)
  .setVar();
  .iterateVar(@varName, (@@varName + @add));
}

.iterateVar(counter1, @new) { //define counter1
  .setVar() {
    @counter1: @new;
  }
} .iterateVar(counter1, @counter1); //initialize
.iterateVar(counter2, @new) { //define counter2
  .setVar() {
    @counter2: @new;
  }
} .iterateVar(counter2, @counter2); //initialize

// usage:

a {
   counter1: @counter1;
  .increment(counter1);
}

b {
  counter1: @counter1;
  .increment(counter1);
}

c {
  counter1: @counter1;
  counter2: @counter2;
  .increment(counter1);
  .increment(counter2, 3);
}

d {
  counter1: @counter1;
  counter2: @counter2;
  .increment(counter1);
  .increment(counter2, 3);
}

Output is the following. Note how each counter is independent, though using the same increment() mixin to iterate.

a {
  counter1: 0;
}
b {
  counter1: 1;
}
c {
  counter1: 2;
  counter2: 9;
}
d {
  counter1: 3;
  counter2: 12;
}

It becomes vital if one wants to maintain a proper count to increment at the usage point. Using the above incrementing code, note the differences here:

a {
   counter1: @counter1;
  .increment(counter1);
  .increment(counter1);
  c {
    counter1: @counter1;
  }
}

b {
  counter1: @counter1;
  .increment(counter1);
}

Output:

a {
  counter1: 0;
}
a c {
  counter1: 0;
}
b {
  counter1: 2;
}

Versus this:

a {
   counter1: @counter1;
  .increment(counter1);
  c {
    counter1: @counter1;
    .increment(counter1);
  }
}

b {
  counter1: @counter1;
  .increment(counter1);
}

Outputting this:

a {
  counter1: 0;
}
a c {
  counter1: 1;
}
b {
  counter1: 2;
}

Incrementing the counter in the same selector block is only possible by duplicating the selector. Consider this which will not work to iterate the count for the next call:

a {
   counter1: @counter1;
  .increment(counter1);
   counter1_2: @counter1;
  .increment(counter1);
}

Output:

a {
  counter1: 0;
  counter1_2: 0;
}

Versus this which is needed:

a {
   counter1: @counter1;
  .increment(counter1);
  & { 
    counter1_2: @counter1;
    .increment(counter1);
  }
}

Output is duplicated css selector:

a {
  counter1: 0;
}
a {
  counter1_2: 1;
}

@seven-phases-max
Copy link
Member Author

You actually don't even need the extra init code,

Good point!

So it is not that a "changes behaviour of a mixin unlocked in any other scope," but rather, it changes the behavior of all mixins used in scopes following it.

Yes, thanks for correction. I guess I was just more concerned of the scope ignorance rather than the call order.

@scottgit
Copy link

You can use this counter in a loop, but it has a one item "lag" that would have to be accounted for. Using the code given above, this works:

.build-a() when (@counter1 < 4) {
  a-@{counter1} {
     counter1: @counter1;
  }
  .increment(counter1);
  .build-a(); //next 
} .build-a(); //start it

EXCEPT it does output a 4 class:

a-0 {
  counter1: 0;
}
a-1 {
  counter1: 1;
}
a-2 {
  counter1: 2;
}
a-3 {
  counter1: 3;
}
a-4 { //unexpected
  counter1: 4;
}

There are various ways that could be overcome, but it is something to be aware of if one were to want to loop this way.

@lukeapage
Copy link
Member

And LESS does not detect any 'recursive variable definition' just because of recursive mixins and argument variable evaluation black magic.

its kind of magic.. but also not. when you call a mixin it evaluates arguments - so it always gets converted to a dimension inside the mixin, not a variable reference.

@lukeapage
Copy link
Member

is there anything left in this issue? is it a bug or feature request or can it be closed?

@seven-phases-max
Copy link
Member Author

Personally I think of [2] of the first post as a bug, but assuming that it's:

  • questionable
  • really rare thing (yet at least)
  • the discussion is wider than just [2]

I guess it's OK to just close this (maybe later someone opens a dedicated issue report exactly for [2]).

@seven-phases-max
Copy link
Member Author

maybe later someone opens a dedicated issue report exactly for [2]

Actually there's one already: #1291, the example there is a bit different so it's not that simple to see the same problem but it is the same.

@matthew-dean
Copy link
Member

CSS is declarative. And order is important. Later things override (are evaluated after) earlier things.

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