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

Allow variable redefinition inside guarded self running blocks #2072

Closed
lukeapage opened this issue Jun 25, 2014 · 48 comments
Closed

Allow variable redefinition inside guarded self running blocks #2072

lukeapage opened this issue Jun 25, 2014 · 48 comments

Comments

@lukeapage
Copy link
Member

.a {
 @b: a;
 & when (@b = a) {
   @a: 10px;
 }
 b: @a;
}

In this case you get a syntax error from less. I propose it should work.

.a {
 @b: a;
 @a: 2px;
 & when (@b = a) {
   @a: 10px;
 }
 b: @a;
}

this case I am not sure about.

.a {
 @b: a;
 &.a when (@b = a) {
   @a: 5px;
 }
 b: @a;
}

this should stay as an error.

@seven-phases-max and @SomMeri what do you think?

I was trying to make code that redefined a couple of variables based on an option.

I guess the other way of doing it would be this

.a(@b) when (@b = true) {
   .a(12px);
}
.a(@b) when (@b = false) {
   .a(5px);
}
.a(@a) when (default() = true) {
 b: @a;
}

and maybe thats just clearer?

@seven-phases-max
Copy link
Member

That's a difficult one to decide. Personally I get accustomed to think of & as of just ordinal CSS selector (which it actually is). So for me the current behaviour is less or more consistent:

a {
    b {
        @c: 10px;
    }
    // no @c here
}

a {
    & {
        @c: 10px;
    }
    // no @c here as well
}

I can see the benefits of turning the & block into a sort of implicitly self-calling mixin, but I'm quite concerned of the implementation side. Notice that unlike mixins (which are expanded all at once at the point of their call), selectors (incl. &) are expanded at the point of their definition - so it may be quite problematic (if not impossible) to mimic all the (positive) side effects of a mixin expansion with &.
In particular:


[1].

.mixin() {@a: 1}
.mixin() {@a: 2}
.mixin() {@a: 3}

test {
    .mixin(); 
    a: @a; // -> 3
}

(typical use-case)
Similar code with & won't work w/o special handling since later & expansions don't override variables of prev. expansions.


[2].

test {
   .mixin(); // guard is evaluated here (so default() knows of all the mixin definitions)

    // but:
    & when (default()) {@a: 1} // guard is evaluated right here but we have no idea of any other `&` definitions at this point :(

    // ...

    & {@a: 2}
}

W/o above features the & will hang somewhere in between mixins and ordinal selectors and create another "set of expansion rules" to remember (even if documented :).

@calvinjuarez
Copy link
Member

So would & end up being something akin to JavaScript's this? 'Cause that seems rad.

@bassjobsen
Copy link
Contributor

i think (vote) that & when should have a special meaning; short for writing two mixins in once.

The result of .mixin() { & when() {} } should be the same as .mixin() followed by .mixin when() where mixins evalute top bottom, having their own scope and leak variables as used with mixins as functions.

In the current situation:

.mixin(@a:0) {
    p: first;
    @v: 0;

    & when(@a = 1) {
    p: last;
    @v: 1;
    }
}

p {
    .mixin(1);
    v: @v;
}

compiles into:

p {
  p: first;
  p: last;
  v: 0;
}

and:

.mixin(@a:0) {
    p: first;
    @v: 0;        
}
.mixin(@a:0) when (@a = 1) {
    p: last;
    @v: 1;
}

p {
    .mixin(1);
    v: @v;
}

into:

p {
  p: first;
  p: last;
  v: 1;
}

I think / expect both situation should have the same result:

p {
  p: first;
  p: last;
  v: 1;
}

Also see: http:https://stackoverflow.com/questions/29496933/less-conditional-variable-change-inside-mixin/29497626

** some update **
in fact the second mixing should possible read as:

.mixin(@a:0) when (@a = 1) {
    p: first;
    @v: 0;    
    p: last;
    @v: 1;
}

But still expected @v = 1 due to last declaration wins

@calvinjuarez
Copy link
Member

Very into .mixin() { & when(…) {…} }.

@SomMeri
Copy link
Member

SomMeri commented Apr 8, 2015

I prefer @seven-phases-max version, where & is considered just another selector. It seem more intuitive to me and also easier to explain/remember to users: this thing is just another example of selector, they behave the same way.

Plus, I like that & allows us to sneak in 'private' variables that are sure not to overwrite caller scope.

@seven-phases-max
Copy link
Member

seven-phases-max commented Apr 8, 2015

Btw. my opinion about this feature shifted from somewhat neutral (as I expressed above) to "strictly no". This happened as I stepped into a few related Qs here and there at the SO and one issue ticket at the tracker. Now when I better see why exactly people tend to use it this way and how exactly they are going to use it... I can find more concerning pitfalls it will bring in (+ a few problems I missed earlier).

So in summary:

If it's going to be an if substitute we should not use & for it.

---

In details.

[1] Let's start with the following example:

.a {
    result: @a;
    @a: 2px;
    & when (true) {
        @a: 10px;
    }
}

As you can see in above comments and in the linked SO question (actually there're a lot of exactly the same snippets at the SO) they clearly expect this to result in result: 10px. And a code like this will be the most often seen use-case for it. So for this change to actually work for those who vote it up, it has to override outer scope no matter what, basically breaking all scoping rules previously defined for rulesets and mixins. (So no, it's not going to be "just a shorthand for self-expanding mixin", as such mixin would result in 2px). Now accepting this as the base requirement, here're possible consequences and all sort of troubles it brings in:

[2] Evading current scoping rules isn't a problem itself, but for sure it becomes a nice example of those "nonsensical and non-orthogonal/non-uniform scoping rules" they're talking about (falsely by now, but much harder to argue after this change). So when it will come to explaining/documenting this stuff it will be really hard to find any rationale for "why & when works this way but &.b when or even body when works that way" (yup, "just because" :).

[3] (Probably the most important, strangely I missed this earlier) As noticed by @SomMeri above (and earlier in #1877) this also will break the pattern of using & {...} to isolate scopes. And while it probably never was "an official" usage thing, this pattern is a natural corollary of the normal ruleset scoping rules. So, considering backward compatibility and the fact that this pattern went relatively wide-spread (at least I estimate so) by now, this change most likely will instantly break a lot of code out there. (Even not counting backward compatibility, we'll then have to invent another straight-forward and "clean" method to use to put those various "scope-leaking side-effects" under predictable control when nothing else can help).

[4] Blurring & semantics. By now, & does not really have any special scoping rules, except that, indeed, since v1.6.2 the CSS properties it generates are expanded directly into the outer scope. But this was made only for the sake of the output CSS optimization (simply to eliminate redundant duplicated selectors) and wasn't really meant to alter any Less code itself (obviously CSS is flat and does not have any scope, but Less has). So & {...} still can be considered as an ordinal ruleset (after all this is why it is allowed to use & when syntax at all: only because it is a ruleset too) and & is still just what "Parent Selectors" says it to be (technically just "an alias for a selector of the current ruleset"). This change will turn & into "a (magical) Less ruleset scope cracker", thus loading it with a brand new semantics that has almost nothing to do with its original meaning ("selector shorthand keyword" vs. "ruleset auto-expansion directive"), thus making the whole "Parent Selectors" + "Scope Cracker" combo more ambiguous to define and understand. In other words, next time you see & in code it will require much more attention to find if it's used as a "parent selector" or as a "scope cracker" (or both, with the latter being yet more unpredictable/unclear in "how-to-use" since (as of [2]) it no longer follows normal scope rules, which brings us to [5]).

[5] We all know that the lazy-loading stuff (and the "declarativeness" of Less in general) is probably the last thing most new users (at least those with PHP/JavaScript/"name-your-scripting-language" background) become aware of. So we'll have to be prepared for all sorts of weird code like:

div {
    @a: 5;
    a {
        & when (@a > 5) {
            @a: 2;
        }
        & when (@a < 3) {
            @a: 10;
        }
        & when (@a > 6) and not(something-else) {
            @a: boo!;
        }

        result: @a;
    }
}

to jump in, all marked as Less bugs. "WTF is this lazy-loading ur talking about? I have exactly the same code working perfect in PHP!" :). Really, just take any arbitrary if snippet out there, replace if with & when (that's what they usually do) and see what happens with lazy-loading. So this also becomes a maintenance/support nightmare as it encourages brainless copy-pastes from "name-your-favorite-language" (for now they at least have to notion Less basic principles earlier when doing their search for "conditional code in Less" or so). This of course is no way an issue of this feature or a reason to not enable it, but if there's some (other syntax?) way we could target this earlier (or at least minimize possible recursive dependencies vs. lazy-loading conflicts) we're better to try to search for such way before making the changes.

---

So for the ending summary:

While I still sympathize this (quite big new as it turns out to be actually) feature in general, I believe it should be <something-else> when instead of & when (and & when itself to stay as-is, assuming fixed #1877 then).
Even if that would require a new keyword/directive it will be definitely worth by cutting [3], [4] and partially [2] things off.
[5] remains, but it's another big story to think of so I have no real ideas currently (earlier this was mentioned regarding mixin guards that can also suffer from such recursion in certain cases and the possibility to detect such stuff (to throw a error when unsolvable) was estimated as too complicated).

Speaking of <something-else>, also no good ideas yet but in sketching mode:

  • just when? (suffering from typos though)
  • finally, if it's if why not if (or some derivative)? no hypocrisy! :) (this will make [5] much more dramatic though).

@bassjobsen
Copy link
Contributor

@seven-phases-max thanks for your extended answer in the first place. I do really appreciate that. I will have to think about all this. For now it seems you says everything about a & should act the same, and i possible argue that & when and & .selector are different cause the first is a guard and the second a selector.

A new ... for & when possible solve that indeed. A new & when possible introduce new issues. Till now i did not found any issues with the current & when except when using it for mixins as functions.

update
okay i understand now when & when becomes a self-expanding mixin then also & {} should become such a mixin. The latest will break many other things.

@SomMeri
Copy link
Member

SomMeri commented Apr 9, 2015

In general I would be favorable to syntactic sugar around often used cases that are currently either long or hard to read. Things that are just syntactic sugar tend to be easy to implement if the syntax is designed right, so why not if they add real value to the user.

The rules less respects so far and we might not want to break without good reason:

  • {} creates new scope everywhere else,
  • less is declarative language not procedural one.

When user need to declare single variable, a simple if or when function or ? : operator could already help:

//ternary operator is standard
@variable: when(condition, ifTrue, ifFalse);
@variable: if(condition, ifTrue, ifFalse);
@variable: ?(condition, ifTrue, ifFalse); 

When multiple variables depend on the same condition, we could use new symbol for non-scope creating blocks. For example [block]:

if (condition) [ 
  // variables defined here override local scope
] elif (condition) [
] else [
]

It could be just a syntactic sugar for conditional mixins + slight different handling of return values.

@matthew-dean
Copy link
Member

XSLT is a good reference for a declarative language that is still very functionally expressive. I'm not a fan of introducing big logic blocks. BUUUT single line evaluations would seem to fit, because we already use them in part for things like the contrast function:

value: contrast(#222222, #101010, #dddddd, 30%);

This is basically the same as this pseudocode:

value: if(lightness(#222222) > 30%, #101010, #dddddd);

They both evaluate against a value, and return a result depending on the test. And, in truth, the second is probably easier to logically follow than the contrast function itself (it's self-documenting), and allows more expressiveness.

So, to go back to @lukeapage's example:

.a {
 @b: a;
 @a: 2px;
 & when (@b = a) {
   @a: 10px;
 }
 b: @a;
}

This becomes:

.a {
 @b: a;
 @a: if(@b = a, 10px, 2px);
 b: @a;
}

It's a hell of a lot easier to read, and we don't have to dick around with & when and block scoping stuff. We already use "if-like" functions, so why not just an if function?

@matthew-dean
Copy link
Member

Side note: spreadsheet functions are also a good example for declarative programming functions.

@matthew-dean
Copy link
Member

Oh, and second side note: I just realized in the contrast example that I'm not sure if it's less than 30% or greater than 30% that results in the first value being returned, which emphasizes my point. I would use an if function to test for light / dark rather than contrast, because while it's more verbose, it would be more maintainable.

@seven-phases-max
Copy link
Member

@a: if(@b = a, 10px, 2px);

Which is basically #1894.

Btw., XSLT has if statement and it works exactly like Less * when() {} now - it can't redefine values and variables in outer blocks....

P.S. Curously xsl:when is (sort of) the function that we would need for stuff like @a: if(@b = a, 10px, 2px); to be possible (currently you can't use logical operators in Less anywhere except after its when), i.e. in the end this whole stuff would look something like that: @a: select(when(x = y), 10px, 2px); (such function would need select or choose (hello XSLT again :) name instead of if for obvious reasons (and when to be a special pseudo-function like url).

@SomMeri
Copy link
Member

SomMeri commented Apr 10, 2015

@seven-phases-max What would select do if not combined with when? I do not mind select(when(x = y), 10px, 2px) working, but if(x = y, 10px, 2px) would be nice short syntactic sugar for that :).

@seven-phases-max
Copy link
Member

@SomMeri

if(x = y, 10px, 2px)

It can't because this would break filter: alpha(opacity=50); (resuting in filter: alpha(false); then).
I would also personally prefer a shorter variant, but even if we decide to break MS-specific stuff (by always requiring ~"" there), it still would suffer from all sort of yet unsolved things similar to #2481, e.g.:

@var: 2 > 1; // ?

@{var} { // ouch!
   color: whatever(@var, 20, 30); // OK, true
}

In other words it's still *(<true or otherwise>, 10px, 2px) but when is needed only to tell the parser that a conditional expression is coming. But I guess this is already mentioned in #1894.


Speaking of the function name, I guess it does not really that important but certainly not if() (just because it is a complete ternary operation and not just a condition part (-> less confusion for new users)).
?() is good (but it will require further parser changes again, though I guess not that dramatic since we have %() already).
I would not mind even cmov() :P

@matthew-dean
Copy link
Member

Which is basically #1894.

Reading that. I don't agree with the conclusions that were made from the example. A simple variable conditional assignment is less verbose than a guarded mixin to simply assign a variable. But yes, the description of the need is the same, as is the rationale.

It can't because this would break filter: alpha(opacity=50); (resuting in filter: alpha(false); then).

That's silly. There's no reason it would have to break alpha. They're not even the same keywords.

In other words it's still *(, 10px, 2px) but when is needed only to tell the parser that a conditional expression is coming.

It isn't, actually?

Speaking of the function name, I guess it does not really that important but certainly not if() (just because it is a complete ternary operation and not just a condition part (-> less confusion for new users)).

Or... certainly if? Since that's an understood keyword for new users, as is the fact that the second argument is for true and the third is for else. That's pretty consistent in multiple environments.

?() is good

This would be my "certainly not". This breaks CSS and LESS conventions of functions being defined by actual names, not symbols. This isn't an operation. Is that how you're reading this? This is a LESS function with three arguments. It's better compared to contrast than to & when syntax. Which, again, let's remember that contrast is a function which does part of a conditional operation (without being able to specify the operator), and has basically an if / then / else pattern as far as a returned value. I don't get all these arguments that say the parser would have to be rewritten and everything would break and MS functions would flail wildly. You can't argue that if would never work or would muddy variable assignment or selector interpolation if no other LESS function that returns a value, sometimes conditionally, has that problem.

What would select do if not combined with when? I do not mind select(when(x = y), 10px, 2px) working, but if(x = y, 10px, 2px) would be nice short syntactic sugar for that :).

Exactly. The metaphor to XSLT doesn't even make sense, since XSLT's choose / when would allow multiple whens, so that actually operates like mixin guards do now. LESS's mixin guard / default operator are almost an exact parallel to choose / when. But, let's not dive into how like / unlike it is to XSLT. That's not really relevant. This is just an if / then / else function. As @SomMeri says, extra keywords are not needed, if they don't distinguish any behavior from omitting them, nor do they clarify the function at all. And to use when here actually confuses when, which is a guard for a block.

I would not mind even cmov()

Now you're just being ridiculous.

@seven-phases-max
Copy link
Member

That's silly. There's no reason it would have to break alpha. They're not even the same keywords.

It's not the alpha or select that will need to evaluate 2 > 1 there but the parser itself before passing an evaluated result of such expression to a function (so @var: 2 > 1 and any derivative will result in true and opacity=50 would result in false). We may hardcode select itself in the parser to be special like url (like when in my variant would) but that way the parser will also need to evaluate the rest of its arguments too[1] and then you won't be able to write your own function or a mixin to receive similar boolean expression args.

What I'm trying to suggest is some practical solution which is less or more possible to build into existing compiler. This no way means that others can't keep polishing a magical syntax to appear in Less 5 ;)

if(x = y, 10px, 2px)

Oh, common... if (x is equal to y and/or 10px and/or 2px) then what?

P.S. to clarify [1]: more strictly speaking the parser will have to build a dedicated tree value of If type (just like url for example) to be evaluated later to make all this possible (and the rest is the same). Contrary same "special" when (or whatever we name it) function may be stored in the tree as already existing Condition with minimal changes).

@matthew-dean
Copy link
Member

I don't understand. If the parser needs a special pass for functions, how is this a different case? How do plugins work if you can't send any value to a plugin, or define any function name in a plugin? Why would defining a new function affect current parsing rules (and if so, is that a known limitation of plugin custom functions)?

Oh, common... if (x is equal to y and/or 10px and/or 2px) then what?

Wut? Are you reading it that way or suggesting someone would read it that way? Because...
https://support.google.com/docs/answer/3093364?hl=en
https://msdn.microsoft.com/en-us/library/27ydhh0d%28v=vs.90%29.aspx
https://msdn.microsoft.com/en-CA/library/hh213574.aspx
https://dev.mysql.com/doc/refman/5.0/en/control-flow-functions.html#function_if
https://support.office.com/en-in/article/IF-function-a918d97a-251e-4af5-bd15-09b12b8742bb
http:https://www.filemaker.com/help/html/func_ref3.33.11.html
https://mariadb.com/kb/en/mariadb/if-function/
https://www.gnu.org/software/make/manual/html_node/Conditional-Functions.html
https://reference.wolfram.com/language/ref/If.html

A 3-argument (or optional 2-argument) if function is a common construct across many different languages, both in terms of naming and the order of the condition, then, and else arguments. So I'm not sure if you're trying to purposefully mangle the meaning or make the concept seem foreign, but if so, I'm not sure what you'd be drawing that from. Microsoft's languages sometimes named it iif to distinguish from an IF block, but other languages that don't have blocks name it IF for simplicity.

It's fine to say that it's difficult to implement with the current parser. That makes sense. But I think to say the naming of the function or its behavior is inherently confusing to new users vs. any other alternative is disingenuous. A single statement IF / THEN / ELSE evaluation should not be foreign to anyone.

@seven-phases-max
Copy link
Member

seven-phases-max commented Apr 10, 2015

All right. Excel, filemaker, maridb, wolfram - I'll buy it, won't argue anymore...
^sarcasm (from the above list I consider only Wolfram as something to respect (the rest are just phew, sorry...) - but considering Wolfram's totally alienated syntax if compared to either of mainstream langs, its syntax is not an good source of ideas).

@SomMeri
Copy link
Member

SomMeri commented Apr 11, 2015

1.) If we add conditional assignent, we need also to add a way how to assign boolean value to variable.

This is preferable:

@condition: when(x<5);
@variable1: when(@condition, ifTrue, ifFalse);
@variable2: when(@condition, ifTrue, ifFalse);
@variable3: when(@condition, ifTrue, ifFalse);

over repeating the condition here:

@variable1: when(x<5, ifTrue, ifFalse);
@variable2: when(x<5, ifTrue, ifFalse);
@variable3: when(x<5, ifTrue, ifFalse);

The point being we need both one parameter version and three parameters version. Another consequence is that variables need to be able to hold also boolean values.

2.) Less uses comma , in three different ways:

  • as parameter separator,
  • as list members separator,
  • as or in guards.

Following statement:

@variable: if(@x=@y, @arg1, @arg2) // assume @arg1, @arg2 boolean

This could be read in three ways:

  • Someone who learned about guards would reasonably assume that it means the @variable is supposed to hold the value of @x=@y or @arg1 or @arg2.
  • Alternatively, it could also mean that @x is supposed to be compared to comma separated list with three values @y, @arg1 and @arg2.
  • Finally, it could be that if x is equal y use value @arg1 otherwise @arg2.

The consequence is that we need to use either ; as arguments separator or new operator for or in conditions or both.

@SomMeri
Copy link
Member

SomMeri commented Apr 11, 2015

@matthew-dean One difference between languages you linked and less is that those languages have overall more consistent syntax - mostly because they are not supposed to be addition over css.

  • When Mathematica (Wolfram) parser sees < or = then the parser knows the token is part of condition. Basically, opacity=50 has always boolean value (assignment uses :=). Less have < inside selectors and = in functions like alpha.
  • Mathematica does not have list separator , doubling as or and lists are inside [] so it is always clear where they start and end.
  • Less also does not have true keywords, when is both guard and perfectly valid identifier.

Optimally, well designed grammar would not require parser to know what kind of function it is parsing, the abstract syntax tree generated from alpha(opacity=50) would be the same as the one from when(@x=50). I think that once it is getting inconsistent this way:

  • It is hard to predict where everywhere it causes ambiguities and you risk getting into all kind of complications in edge cases. The consequence is that while first working code looks simple, we risk to have to patch it for many special cases ending up with something much more complicated to maintain and explai.
  • It makes it harder to switch to parser generator, cause those tend to assume consistent well designed grammar.
  • Generated parser will be slower - for example antlr uses optimizations and predictions that makes parser faster, but those do not work that well if you are forced to use certain tricks.

Wolfram but also others on that list are mostly well designed grammars that follow most known best practices, do not have ambiguities etc. The if construct does not conflict with anything, = is always equality operator, operator for or is always or and there is no alpha(opacity=@v).

Basically, less have all kind of heritage that cause similar construct to be more problematic.

@seven-phases-max
Copy link
Member

1.) If we add conditional assignment, we need also to add a way how to assign boolean value to variable.

Yes, in my variant above it was considered to be like:

@c: when(2 > 1);
@x: select(@c, a, b);
@y: select(@c, c, d);
@z: select(@c, x, y);

@seven-phases-max
Copy link
Member

Btw., https://www.gnu.org/software/make/manual/html_node/Conditional-Functions.html (yes it's actually the two function variant similar to what I proposed not a three-args-if ;) gives an interesting idea of omitting parens for the when itself... e.g. could be select(when 2 > 1, then-value, else-value), but , as or spoils it. W/o or I'm afraid it seems to have to be something scary like when((2 > 1), not(false)) or a sort of (since ; is also not allowed as a function arguments delimiter)).. Hmm.. though I'm not sure maybe there's way to simplify this somehow...

@SomMeri
Copy link
Member

SomMeri commented Apr 11, 2015

@seven-phases-max The space is list separator too, some constructs may and up ambiguous @variable: when @a1 <- should it be a list or should it convert @A1 into boolean?

@matthew-dean
Copy link
Member

@SomMeri - Your arguments make sense. The ambiguity problem isn't so much in terms of similarity to functions but consistency with other conditionals. Thanks for clarifying. And that helps me get what you were saying, @seven-phases-max

So in other words, we already have this kind of conditional syntax that we'd want to support:

.mixin (@a) when (isnumber(@a)) and (@a > 0) { ... }
.mixin (@a) when (@a > 10), (@a < -10) { ... }
.mixin (@b) when not (@b > 0) { ... }

So, now I'm seeing the cope of the problem. Since a "when" guard or Less conditional, however you want to phrase it, can be verbose, it can be a bit of an issue.

What if we flip this problem around, and go back to original source. What if we just make "guard blocks" that would, by definition, exist in the parent scope. Trying to stuff this into function syntax is awkward, as you noted with the use of commas. Therefore:

.a {
 @b: a;
 @when (@b = a) {
   @a: 10px;
 }
 b: @a;
}

and for multiple whens:

.a {
  @b: a;
  @select {
    when  (@b = a), (@c = @j) {
      @a: 10px;    
    }
    when  not (@b = 1) and (@red = red) {
      @a: 5px;    
    }
    when  (default()) {
      @a: 2px;    
    }
 }
 b: @a;
}

It doesn't have the benefit of being as slim of a declaration of a single function statement, but since that clearly won't work anyway, this would be consistent with current when guards and @media syntax (which when is inspired from). Plus, it would avoid the ambiguity or muddying of the & when syntax.

That's kind of back-peddling on my part, but I think you've successfully demonstrated that a condensed syntax is flawed.

@matthew-dean
Copy link
Member

Side note: I'm glad you guys are involved with Less. You've got good brains.

@SomMeri
Copy link
Member

SomMeri commented Apr 11, 2015

@matthew-dean

1.) I meant that it is not possible to create a variable that would hold guard value, e.g. something like this:

@condition: when(x<5);
@variable1: when(@condition, ifTrue, ifFalse);
@variable2: when(@condition, ifTrue, ifFalse);
@variable3: when(@condition, ifTrue, ifFalse);

But since you can put multiple things into that block, it probably does not matter much.

2.) You are right. Maybe it could be just syntactic sugar for mixin call?

@matthew-dean
Copy link
Member

@SomMeri - Ah. Well, as I said, based on your examples and noting the use of a comma, I don't think the "function" form will work, so as a result, there's no need to put a guard value in a variable, which is an awkward idea anyway.

Maybe it could be just syntactic sugar for mixin call?

I wasn't suggesting literally a mixin call. I think there's value in a dedicated @when construct. I just meant it's functionally equivalent to an arbitrarily named mixin (or an anonymous mixin), with a guard, that is called automatically within that scope. So, coding-wise, the logic path shouldn't be too difficult. But I'd like to get rid of .-() when cond { ... } .-(); in example code if we could. It just isn't friendly to newbies.

@seven-phases-max
Copy link
Member

I actually liked the "function thing" because it does not suffer from [5]...


Btw., I guess it's also would be important to remind that mixin-based stuff works more like switch/case thing rather than if/else (less/less-docs#80 (comment)), and if we're designing some replacement we should take into account unrelated conditional blocks in the same scope (e.g. multiple if/else vs. if/elseif vs. switch/case etc. use-cases).
For example:

.something(@a, @b) {

    .1(@a);
    .1(@c) {@x: @c}
    .1(2)  {@x: (@a - @b) / 2}

    .2();
    .2() when (@x < 0)    {@y: @b + @a}
    .2() when (default()) {@y: @b}

    result: @x @y;
}

div {
    .something(2, 3);
}

how @when would translate this?

@SomMeri
Copy link
Member

SomMeri commented Apr 12, 2015

On if/else and case: what about something like this?

@when (@b = a) {
   @a: 10px;
} @elwhen (@b = c) {
   @a: 20px;
} @elwhen (@b = d) {
   @a: 30px;
} @else {
   @a: 40px;
}

The advantage is that user will have less need to use the some condition in multiple conditional blocks - less repetition is always good.

On @seven-phases-max example: I do not think when should be in-place equivalent of everything mixins are able to do. And anyway, arent patterns deprecated and not to be promoted too much :) ?

This is how I would rewrite it:

.something(@a, @b) {
  //mixin 1
  @x: @a;
  @when (@a=2) {
    @x: (@a - @b) / 2;
  }

  //mixin 2
  @when (@x < 0) {
    @y: @b + @a;
  } @else { // here would be condition repetition if when does not have @else
    @y: @b;
  }
}
div {
    .something(2, 3);
}

without else (but I prefer with else):

.something(@a, @b) {
  //mixin 1
  @x: @a;
  @when (@a=2) {
    @x: (@a - @b) / 2;
  }

  //mixin 2
  @when (@x < 0) {
    @y: @b + @a;
  } 
  @when not (@x < 0) {
    @y: @b;
  }
}
div {
    .something(2, 3);
}

Edit: edited typo in last code.

@seven-phases-max
Copy link
Member

@SomMeri

And anyway, arent patterns deprecated and not to be promoted too much :) ?

Do you mean Pattern-matching? If, yes, then no, they are not (that would be 😱).

@matthew-dean
Copy link
Member

I guess it's also would be important to remind that mixin-based stuff works more like switch/case thing rather than if/else

Sooort of, but not really. A switch / case has single values, but I know what you mean. An if / then / else has a binary outcome. So yes, I was suggesting that, instead of using if / then / else, using the when pattern that mixins use. Meaning: two different @when blocks can evaluate to true. But at least then it would be a consistent and familiar pattern. And an if/then/else would still be a possible construction by using select/when/default.

I actually liked the "function thing"

If we did that, we would have to change the way we write Less conditionals, not just add a method that evaluates those conditionals.

@SomMeri An issue with your when / else is that your else is an independent statement. There is nothing that declaratively unites those two blocks. Even though you placed @else after the closing }, that still doesn't change the fact that semantically, they're too different independent statements in that form. In other words, remember that, scoping-wise, an "else" after the "when" would evaluate the same as the same "else" at the top of the mixin.

Therefore: I think that's when you would need a select or choose, and then you could use a consistent default (consistent with mixins).

.something(@a, @b) {
  //mixin 1
  @x: @a;
  @when (@a=2) {
    @x: (@a - @b) / 2;
  }

  //mixin 2
  @select {
    when (@x < 0) {   // or we always write "@when"?
      @y: @b + @a;
    }
    when (default()) {
      @y: @b;
    }
  }
}
div {
    .something(2, 3);
}

Another way to illustrate that the "default" statement is independent is that you should be able to place the default statement like this (just as you can with mixins):

@select {
    when (default()) {
      @y: @b;
    }
    when (@x < 0) {   
      @y: @b + @a;
    }
  }

Each when evaluates independently. And when in a @select, a default() only applies to that @select statement.

@SomMeri
Copy link
Member

SomMeri commented Apr 12, 2015

1.)

In other words, remember that, scoping-wise, an "else" after the "when" would evaluate the same as the same "else" at the top of the mixin.

I am not sure what you mean by that.

Even though you placed @else after the closing }, that still doesn't change the fact that semantically, they're too different independent statements in that form

If we define the statement as a statement with multiple blocks tied through keywords, then they will not be independent. If in python works that way:

if expression1:
   statement(s)
elif expression2:
   statement(s)
elif expression3:
   statement(s)
else:
   statement(s)

Java supports only two blocks if and else, but conditions can be chained like this:

if (x==5) {
} else if (x==4) {
} else {
}

2.) If we do not give them else if they will have to do this when they will need more cases:

@when (@x<10) {
  @variable: 10px;
  property-1: value-1;
}
@when (default()) {
  @when (@x<20) {
    @variable: 5px;
    property-2: value-2;
  }
  @when (default()) {
    @variable: 55px;
    property-3: value-3;
    @when (@x<40) {
      @variable: auto;
      property-4: value-4;
    }
    @when (default()) {
      @variable: hi;
      property-5: value-5;
      //could nest further
    }
  }
}

3.) If we claim that @when are independent and people should use default() to achieve negation, then we may run into chains like this:

Default being used before condition, because they are not tied in syntax - so I can:

// sort of else
@when (default()) {
}
//things in between 
property: value;
.mixin();
//condition
@when (condition) {
}

Default being between two conditions, what now?:

//first condition
@when (condition1) {
}
//things in between 
property: value;
.mixin();
//else could belong to upper condition or to lower condition or be valid only if both are false
@when (default()) { 
}
//things in between 
another-property: another-value;
.mixin();
//second condition
@when (condition2) {
}

If we say that @when (default()) belongs to both conditions, then it will be impossible to have two conditional statements with two different else without condition repetition or longer workarounds.

Alternatively, we could put some code into compiler to make it clear which @when wins, but it seem to me easier/better fit to to solve this kind of conflicts in syntax itself.

@matthew-dean
Copy link
Member

The examples you give are imperative patterns, which tell the compiler a precise control flow. If we are extending when from mixin syntax, it doesn't make sense to have a different control flow than used with mixins, or to suddenly include an imperative flow in a declarative language. To follow Less's model, you'd want to think of when and else (or default()) as guards you are "binding" to something, which will then be evaluated as a group by the compiler. The reason why mixin guards work without a @select group is because all of those statements are bound to the same mixin name, so the "default" applies to that mixin group.

Similarly, all the examples of possible failures would apply to anywhere else where we currently use when guards, which no one has really complained about. In other words, I think you're describing a complicated issue which could exist, but there's no reason to think it does it already. And if in an edge case, someone wants to nest when statements, there's nothing more complicated to that than having a mixin call inside another mixin call, where both have guards. So, if these problematic patterns were to exist, they already exist, and this syntax wouldn't be introducing those problems. I don't think it's worth changing the nature of the Less language and declarative blocks in order to solve patterns people haven't asked about yet.

In essence, what I'm describing is something like "anonymous guards". They're not bound to any selector block or mixin definition. They evaluate and return to their current scoped block. This would be a familiar addition to Less authors that are already using guards on mixins, which also has an advantage over the "functional" syntax discussed earlier. The pattern is already used extensively in Less. (However, it's possible that we could still also introduce a simple binary if/then/else function for simpler conditions and use cases. The problem would be how to write those conditions.)

@matthew-dean
Copy link
Member

Also, another consideration could be a guard condition on the select statement.

@select when (cond) {
  @when (cond) {
  }
  @when (default()) {
  } 
}

@SomMeri
Copy link
Member

SomMeri commented Apr 23, 2015

@matthew-dean Mixins are named, so you can have one default() for one mixin and entirely different default() for another mixin. E.g. the border() mixin is not forced to share default implementation with transformation() mixin.

The examples are cases where you might want multiple different when within the same block, but can not because they share the same else clause by language design. Basically, if we do not add else to when, then we are limiting what is possible to do with the language - you can have only one full conditional statement within the same block.

I do see how it changes the nature of less language itself. It just changes how the feature is implemented. We can not straightforwardly convert all when statements within the same block into nameless mixin - we would had to give everyone different name. But that is just relatively small implementation issue, not a language one.

@seven-phases-max
Copy link
Member

seven-phases-max commented Aug 7, 2015

A bit of offtopic, but somewhat related. I've just occasionally remembered an old trick that I think would be fun to share here (from "Branch-free based programming in Less", Chapter 6: "Table-Lookups").
Gist (and Codepen demo for the same thing). It's actually two tricks in one: there the @alt-color is actually a function (trick one) that uses a table-lookup (trick two) to emulate contrast function w/o any conditional stuff or guards or whatever.

@seven-phases-max
Copy link
Member

seven-phases-max commented Apr 30, 2016

Btw. (inspired by recent topics referenced above), since I'm not a great fan of the idea of bringing in @custom directive syntax for Less specific features, I would like to also mention an alternative syntax:

    :when (...)

(with or without optional &).

@matthew-dean
Copy link
Member

matthew-dean commented Apr 30, 2016

The only thing to remember is that & when is not evaluated like @when, and both of those have more predictable allegories; e.g. selector guards and media queries, respectively. :when would not have a predictable behavior (which does not make it worse, but something of note).

That said, even having proposed @when, I too am reluctant about adding @-rules to Less. It's a) kind of Sass-y, and b) muddies var definition syntax, c) muddies existing CSS @-rules, d) potentially conflicts with the existing PR / 3.0 discussion about allowing plugins to define custom @-rules. (Under debate: #2852 & less/less-meta#10)

So, ':when' is an interesting suggestion. I would suggest that as to this:

(with or without optional &).

If we did that, then the same optionality should exist for &:extend, as those would then be somewhat congruous as far as syntax.

@matthew-dean
Copy link
Member

matthew-dean commented May 4, 2016

Some other possibilities:

1. when as a Less "flag" (e.g. `!important)

.something (@a, @b) {
  @x: 1;
  !when (@a=1) {
    @x: 2;
  }
}

or

2. GUARD ALL THE THINGZ

These are all separate ideas.

// guard on a value
.something (@a, @b) {
  @x: 1;
  @x: 2 when (@a=1);
  // or would possibly need the flag at this point
  @x: 2 !when (@a=1);
}
//or guard on an anonymous ruleset
.something-else (@a, @b) {
  @x: 1;
  { @x: 2; } when (@a=1);   // scoping gets confusing here, and confused with `& when` so probably not
}
// or guard on a detached ruleset
.something-else-else (@a, @b) {
  @x: 1;
  @dr: { @x: 2; };
  @dr() when (@a=1);   // requires proposed change to DR var scope
}
// or guard on an evaluated lambda
.something-else-else-else (@a, @b) {
  @x: 1;
  @() { @x: 2 } when (@a=1);   // self-executing lambda (anonymous mixin)?
  // or written possibly as
  @() when (@a=1) { @x: 2 };
}
// or
.something-else-else-else-else (@a, @b) {
  @x: 1;
  @dr: () when (@a=1) { @x: 2 };  // pie-in-the-sky: var assigned to lambda with guard (DR w/ mixin features)
  @dr();  
}

The latter sketches are me just thinking about if we could basically cover "when" cases with two existing Less.js proposals: 1) lambdas and 2) DR/mixin unification.

@calvinjuarez
Copy link
Member

calvinjuarez commented Jun 24, 2016

I'd love to be able to do something like this:

(when (@a = 1): { @x: 2; })();
// OR
@(when (@a = 1): { @x: 2; })();
// optionally without the `:`s

I'm borrowing the JS anon self-calling function syntax, which seems most familiar and explicit to me.

ALSO,

Do the () in a detached ruleset call do anything at the moment, or is it just mixin-like syntax? Is there a plan for those?

@stale
Copy link

stale bot commented Nov 14, 2017

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Nov 14, 2017
@stale stale bot closed this as completed Nov 28, 2017
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

6 participants