Closed lukeapage closed 6 years ago
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 :).
So would &
end up being something akin to JavaScript's this
? 'Cause that seems rad.
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://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
Very into .mixin() { & when(…) {…} }
.
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.
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:
when
? (suffering from typos though)if
why not if
(or some derivative)? no hypocrisy! :) (this will make [5] much more dramatic though).@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.
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,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.
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?
Side note: spreadsheet functions are also a good example for declarative programming functions.
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.
@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
).
@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 :).
@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
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.
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).
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://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.
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).
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:
or
in guards.Following statement:
@variable: if(@x=@y, @arg1, @arg2) // assume @arg1, @arg2 boolean
This could be read in three ways:
@variable
is supposed to hold the value of @x=@y or @arg1 or @arg2
. @x
is supposed to be compared to comma separated list with three values @y
, @arg1
and @arg2
.@arg1
otherwise @arg2
.The consequence is that we need to use either ;
as arguments separator or new operator for or
in conditions or both.
@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.
<
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
. ,
doubling as or
and lists are inside []
so it is always clear where they start and end. 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:
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.
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);
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...
@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?
@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.
Side note: I'm glad you guys are involved with Less. You've got good brains.
@SomMeri Yes, space delimiter too.
Btw. speaking of or
- #2149 might be important to reference (illustrating that current guard syntax has certain defects and needs some changes anyway) so maybe just adding or
could be the least disturbing change.
And.. oh.. actually it is in CSS already: http://www.w3.org/TR/css3-conditional/#at-supports (while Less's =
isn't).
@seven-phases-max Both of those seem relevant. If Less is a superset of CSS, and CSS has added or
, then Less should too (as an alias of ,
). Likewise, the point about "not" applying to the whole conditional is a good one. Those notes should probably be added to #2149.
@matthew-dean
@select {
when (@b = a), (@c = @j) {
@a: 10px;
}
when not (@b = 1) and (@red = red) {
@a: 5px;
}
when (default()) {
@a: 2px;
}
}
that way it will be almost the same as:
.-();
.-() when (@b = a), (@c = @j) {
@a: 10px;
}
.-() when not (@b = 1) and (@red = red) {
@a: 5px;
}
.-() when (default()) {
@a: 2px;
}
except that the mixin variant, while being probably more ugly looking with all its extra parens, also allows you to use pattern-matching so for not so artificial use-cases it actually may end in more compact code (for example). So to be honest I'd rather admit that we actually in bad need of a good more detailed tutorial/how-to for guards/mixins/scope thing rather than a new syntax for a large conditional structure :)
while being probably more ugly looking with all its extra parens
Yeah, exactly. They're ghastly. The other thing is that they're not equivalent, because you also have to call the mixin using said ugly parens. So they're a pile of mess. So can we clean them up with a more supported (not ugly and hackish-looking) solution?
I like the @when
solution overall, but there are two (probably minor) buts:
{}
opens news scope everywhere else, maybe new symbol instead? ([]
)?Second thing is something I did not through out in full yet, so it is sort of half baked idea.
Basically, as a first step @when cond { ... }
== .-(); .-() when cond { ... }
Can you explain:
it does not make it possible to put boolean into variable
As far as:
{}
opens news scope everywhere else
Mostly, but calling a mixin imports that scope, even though it's in a separate {}
. So this would be similar, no?
@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?
@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.
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
(https://github.com/less/less-docs/issues/80#issuecomment-32127901), 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?
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.
@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 :scream:).
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.
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.
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.)
Also, another consideration could be a guard condition on the select statement.
@select when (cond) {
@when (cond) {
}
@when (default()) {
}
}
@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.
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.
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 &
).
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: https://github.com/less/less.js/pull/2852 & https://github.com/less/less-meta/issues/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.
Some other possibilities:
when
as a Less "flag" (e.g. `!important).something (@a, @b) {
@x: 1;
!when (@a=1) {
@x: 2;
}
}
or
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.
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?
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.
In this case you get a syntax error from less. I propose it should work.
this case I am not sure about.
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
and maybe thats just clearer?