-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Discussion: Variable visibility of mixin call/definition scope and caller scope to each other #2767
Comments
Any particular examples? Currently (starting from v1.4.2) I can't recall any leaking issues. (The only known var scope issue is not really solvable conflict between orthogonal parent and caller scopes - e.g #2435 and related stuff like #1306). The mentioned comment was about earlier 1.4.x version problems fixed in 1.4.2 (+/-). |
Yes, it would. |
Just a brief remark... (only to ensure you both are talking of the same thing).
Subjective - it's just because you think of a mixin like a function. But it's not - the very purpose of the mixin feature (in Less) is to expand the contents of some block scope into another block scope (thus effectively merging both). Obviously this does not automatically mean it should expand everything, but notice a mixin call must expand at least its CSS properties to the calling scope - now considering #2654 - disabling variables would not make any difference (just because as soon as #2654 merged, properties become "variables" too, and we simply can't disable these "variables" to leak into the calling scope). In other words, speaking of counter/pro-intuitiveness, try to think of mixins like of expandable namespaces, then a mixin call becomes just something similar to "using namespace X" and voila. Regardless of above - Personally I would not mind disabling variable expanding into the caller scope (if Less has real functions and "using namespace" (typical use-case) facilities), but I believe there must be some reason to do this beyond subjective "feelings". Notice in every issue (after 1.4.2) this idea pops up - the problems have very few to do with "leaking" itself (#2436, #2543 etc.) So yet again, particular examples are welcome (example where leaking harms and example where disabling it would actually solve something). |
@seven-phases-max Mixins do not really fully work as "merging content in". Calling mixin and writing the code directly can lead to different results (like here). Those who did not grep up on JavaScript are used to think about blocky syntax as about closed non-leaking scope. Especially when value returned by mixin overrides something deep down. Everything is copied into caller rule is unusual. That being said, I do not think we can just disable return values. We can only make it disabled by default, e.g. add some new syntax would allow mixins to return values and assign them to callers variables. However, what I would be keen on removing is the "mixins sees callers variables" thing. There is no reason for that since mixins have parameters. I do not like it much because:
It is ok to see outer scope variable and return values are something that is too useful. Mixin sees callers variables is something that should be deprecated. |
Either way has its merits. I'm thinking about it in terms of simplifying evaluation and re-evaluation by establishing clear dependency chains, similar to @SomMeri's second point. If both are true: mixins can see caller's vars and caller can see mixins vars, then you have a circular dependency. I know it's not the only case where things currently need to be evaluated more than once. I've never used "return vars" so I don't know the use cases there. However, I have extensively used inheritance of vars into mixins. There are Less patterns that are impossible or at least difficult or much more verbose without that inheritance. (I actually have a Less lib I'm working on that pretty much entirely relies on this feature.) So, it may be that we can't really change this behavior without some sort of feature expansion on one side or another. |
This I will agree with 100%. It makes no sense to expose caller scope like that and it already has a suitable, predictible alternative available in the form of argument/parameter passing. |
@rjgotten Yes, it has an alternative, but it's only suitable under simple circumstances. In some cases, a mixin may expand a tree of other mixins, and some of those mixins can rely on an expansion of vars at the top of the tree. You could pass said set of vars into the mixin, but what if you're dealing with 100 variables? 100 arguments into every mixin? You could call the mixin within each individual mixin that expands the custom variables, but that's all of the sudden making a bunch of copy/paste statements. Whether or not that made sense, making that change would basically kill my project. It relies on custom variables being propagated within specific scopes without explicit calls by a user or plugin author. There is no alternative to that, because it's explicitly relying on variable inheritance into mixin scope. Simply put, variable inheritance allows a much more DRY approach when there's multiple (sometimes dozens of) mixins inheriting a parent's scope. Less currently doesn't allow extending mixins, so there is no way to do simple inheritance other than constructing parent scopes. |
That's what is I'm talking about. We can't remove a feature "just because I don't like it" w/o offering something in exchange to use to implement stuff "a bad feature" is usually used for. |
But is that a reliance on caller scope, or a reliance on declaration scope? & {
@foo : "foo";
.bar();
.baz();
}
.bar() {
.baz() {
value : @foo;
}
} or declaration scope: & {
.bar("foo");
.baz();
}
.bar(@foo) {
.baz() {
value : @foo;
}
} I cannot really see how the first would ever be a workable, maintainable code pattern. |
I haven't posted code examples to this point because my involvement with Less is not to advertise for myself, but it looks like real examples are needed. Granola has, in fact, the exact pattern of caller scope given in your example. As in: & {
// Import vars into an isolated scope (don't leak)
#granola();
#vars();
#granola._make(@defaults, @all, @type, @layout, @forms, @nav, @buttons, @icons, @tables, @panels);
} ( https://github.com/Crunch/Granola/blob/master/lib/granola.less#L17 ) By calling the vars mixin, and then calling the entire library "render" sequence, it allows reference of these vars through the library. And by calling a vars mixin within the scope, it allows the variables to be simply named, yet to not override any local vars an author might have. So it provides code isolation. On the library end, the Less code uses a lot of tricks, but for the intended end user, it makes their job extremely easy and yes, maintainable. // styles.less
@import "granola";
// Change custom vars
#granola {
.vars() {
@borders__scale: 0.5;
}
}
// This is what actually renders CSS
#granola.make(); IMO variable isolation and namespacing is a better pattern than something like Bootstrap's completely flat global variable pattern. And, as an example of one mixin: #granola {
// Mixins to help with UI rendering
.ui {
// Draw a border based on math!
.border(@width, @style, @color) {
border: ceil(@width*@borders__scale+(@borders__base - 1)) @style @color;
}
}
...
} There are utility classes which do not demand that the end user pass in every variable that might be required, since some are essentially constants within the library. You could, of course, call I call this pattern an "OOLESS" pattern (similar to the OOCSS pattern). However, to my knowledge, this is the only OOLESS library that exists. But that doesn't mean that this pattern isn't being used elsewhere, or that using it makes something unmaintainable or unworkable. In my case, this turned out to be the best solution. |
Actually this #granola {
.vars() {
@borders__scale: 0.5;
}
} is what "return vars" is. Above I named this feature |
Oh, good point. In that case, it turns out I'm using both features. Lol oops. So, yes, that's true, I'm expanding vars into a scope (via a mixin), and then inheriting those in both mixin calls and mixin declarations. Which doesn't mean this isn't a problem. But for me, there really isn't a better solution. At least in Less right now. The ability to do both is what makes this pattern really powerful. |
@matthew-dean Theoretically, your use case would not require "the mixin sees caller scope" if we returned values from detached rulesets too. E.g.: @allParametersDetachedRuleset: {
@granola();
@vars();
//own modifications
};
#granola._make(@allParametersDetachedRuleset); Incidentally, since we are returning mixins from detached rulesets and both mixins and variables from mixins, returning variables from detached rulesets would make sense. |
Found workaround to "detached rulesets don't return values" - just nest one more mixin into it: #use-place {
//here we define all variables needed by _make
@newDetachedRuleset: {
//we need nested mixin, because detached ruleset
//does not return variables
.workaround() {
//default values
#granola();
#vars();
//local modifications
@override: overriden;
}
};
//pass all variables to _make mixin
//it assumes variables are defined in @newDetachedRuleset
#granola._make(@newDetachedRuleset);
}
//granola library
#granola() {
._make(@parametersPack) {
//return all needed variables
@parametersPack();
.workaround();
//use variables
making: made;
configuration: @configuration;
override: @override;
}
}
//default configuration of granola library
#vars() {
@configuration: configured;
@override: default;
} compiles into: #use-place {
making: made;
configuration: configured;
override: overriden;
} Edited to add: if we want to avoid the hard to maintain thing, it is possible to send the mixin to #use-place {
//here we define all variables needed by _make
@newDetachedRuleset: {
//we need nested mixin, because detached ruleset
//does not return variables
.workaround() {
//default values
#vars();
//local modifications
@override: overriden;
}
};
//pass all variables to granola library
//it assumes variables are defined in @newDetachedRuleset
#granola(@newDetachedRuleset);
//use ._make from granola
._make();
}
//granola library
#granola(@parametersPack) {
//return all needed variables
@parametersPack();
.workaround();
._make() {
//use variables
making: made;
configuration: @configuration;
override: @override;
}
}
//default configuration of granola library
#vars() {
@configuration: configured;
@override: default;
} @lukeapage The less-preview thing is simply awesome. Ability to link live preview of code is really neat addition. |
@SomMeri Your first example wouldn't work because the What that does is effectively emulate namespaced variables (#1848), which IMO is ready to implement. If that was done, then I would be doing things like:
So, I'm not sure return vars from detached rulesets would help me, since I'd always have to pass the detached ruleset into every mixin all the way down the line. However, if I could directly pierce a namespace and reference a variable, then I wouldn't need to expose variables in a parent scope. Essentially, I'm importing a scope into the current scope rather than referencing that scope directly (since the latter is not yet possible). |
@matthew-dean You can unpack it in library root. Every mixin defined inside the library will see that scope and variables. Like this: //granola library
#granola(@parametersPack) {
//return all needed variables
@parametersPack();
.workaround();
//library mixins
._make() {
//use variables
making: made;
configuration: @configuration;
override: @override;
}
//default configuration of granola library
#vars() {
@configuration: configured;
@override: default;
}
}
I do not see which the mixin is added twice? |
Ooh! Now that is clever. |
Yes (#2004). I did not mentioned this issue because it would be weird to say so in a topic that propose exactly the opposite :). |
@SomMeri Isn't this what I'm already doing? I'm basically expanding variables within each
Except that in the documentation it explicitly says that a DR has private vars. But that may be more a description than a design strategy? You'd have to ask @lukeapage on that one. I think he designed and implemented it. But, yes, the syntax is so similar to mixins that I'd prefer if "detached rulesets" were actually "variable mixins", as in, mixins assigned to a variable. Like: .i-am-a-mixin(@var) when (@var = true) {
stuff: yo;
}
@i-am-a-variable-mixin(@var) when (@var = true) {
stuff: yo;
} To me, this syntax of DRs has always felt wrong: @i-am-a-variable-mixin: {
stuff: yo;
}; I get that it makes sense if you're just considering Less variables, but it's completely inconsistent with CSS block definitions, which require neither a colon to assign to a keyword, nor termination of a block with a semi-colon. I don't think discussing DRs are completely off-topic, since it sounds like we're saying that we can't change behavior of mixin scope without filling in the gaps with something, which may include DR functionality. In fact, I'll tweak this issue heading a bit. |
Yes, the documentation was written to reflect the current state which never was considered finished (as was remarked in every DR-related PR). I would probably complain (as well as I would be against using the internal codename "DR" in the public docs) - but some documentation is better then nothing. |
Btw., don't forget DRs were implemented specifically to target "passing rulesets to mixins" problem - that is, their main and the primary use is: .mixin({ /* ta-da */ }); The variable assignment syntax is just naturally raises from above, since mixins parameters are represented as variables, it automatically turns into: .mixin(@a, @b) {}
.mixin(1, {/* 2 */}); // -> @a: 1; @b: {/* 2 */}; Nothing more than that. |
I recognize that, and yet their declaration is still at odds with other parts of Less/CSS. At the least, I would change it so that the final semi-colon is optional. It both makes logical sense and yet looks completely weird next to other blocks. It's not like we don't know when a detached ruleset has ended. The semi-colon is redundant. Beyond that bit, even though, yes, they were intended for passing to mixins, they do turn out to have other uses, and they do share some commonalities with other Less features that at least raises the question of how those features could better align. For instance, one of the ways I've used detached rulesets is where they are acting effectively as a mixin, but I want that mixin to have the ability to be completely overwritten. Rather than using some intricate system of guards and vars, an end user can set the DR to a new value. Of course, this is a hack. What I'm trying to do is allow mixins to be either replaced or appended to, and hacking DRs is the only way I've found to solve this.
Yes, this is true. Ironically, I've been in discussions where someone said, "But the docs say..." and I've said, "Okay, well the docs are wrong then. That's not what was intended." I think the only cautionary piece then is that if we are implementing a feature, we either write the docs for that feature, or at the least, review it as soon as possible. I think you said this best:
So, far, we've individually only been able to provide examples of why mixins need to have access to caller scope and why the caller needs access to the vars from the mixins. But none of us have been able to demonstrate, to @seven-phases-max's point, why limiting the scope might fix any particular problem, except to say it might make some optimizations in the compiler theoretically easier, which doesn't seem like that much of a pressing concern. While this is a great discussion, I wonder if we shouldn't just close this at this point, since there doesn't seem anything actionable that isn't tracked somewhere else. Agree? |
@matthew-dean I still do not think mixins needs to see callers scope :) One issue with scoping is that it has subtle bugs that are very hard to fix (some of #2038, #2093, #996, #1316 from those opened by me - there are others). It is buggy on edges, once you deal with details and there is no easy way to fix it all. I have fixed one or two of those issues in less4j, but it required datastructures I would not dare to implement in javascript. As of now, it is pretty much impossible to think of all possible cases that should be tested when adding a new feature, because there is too much variants and subtlety around. So basically, fragility. On optimizations: I had to give up on some in less4j (which someone used on unexpected large huge sheets), because it would be too much work compared to if scoping would be simpler. As of now, everything sees everywhere. Of course that is less4j problem and somewhat edge problem. This ties to variables in import statements and similar features too - we can sort of half make them, but it is very hard to define them consistently, implement in mostly bug free way and reasonable estimate is that they will never be fully fixed. We basically do the part that is easy to implement and the rest is left as open issue forever and the reason is that the wall to fully-works is too big to climb. I tend to think about detached rulesests as sort of lambdas - pieces of code defined at will and passed around randomly. I agree that it should work as similarly to mixins as possible. Imo, fragility, how easy it is to learn it and how many special cases to think about influence a lot whether people want to continue using something a lot or not. Then again, backward compatibility is important too and from this discussion maybe this is not the right place where to cut. And yet again, it seem to me that we are not able to define all the feature we want in simultaneously right now. |
@SomMeri Okay, that helps. Then there is some evidence that current scoping rules cause current problems. I can think of a few things that would likely solve the need to see caller's scope for me:
I think all of this relates to a general issue where Less is designed with a certain "flatness" that doesn't scale well into larger projects. It has the concept of scope, but when called or defined, that scope tends to be further flattened. Or as @SomMeri puts it: "everything sees everywhere". A lot of these problems are systemic and architectural, which is why I put up less/less-meta#10. I don't think they are huge in terms of workload, but IMO I think we need to rethink Less from the bottom up. That is: given Less's current feature-set, if you were design those features today, from scratch, what would it look like? Is Less's feature-set appropriate for the web in 2016? It reminds me of the old adage: "If you find yourself headed in the wrong direction, stop going that way." |
@matthew-dean Is there a reason not to have DR behaving the same way as mixins? It seem to me that it could be optimal for both users (easier to learn and simultaneously more powerful) and code base (single code path for both things). "A lot of these problems are systemic and architectural, which is why I put up less/less-meta#10. I don't think they are huge in terms of workload, but IMO I think we need to rethink Less from the bottom up." I agree with that. Roadmap Proposal is a good thing, I think that we need to create a vision of where we want to go and decide which features/changes matter the most. |
I don't think so? Not sure if you were saying I said something like that. If they do behave more like mixins, though, then the syntax shouldn't be as different as it is. We could even say that a detached ruleset is an anonymous mixin assigned to a variable. I think thinking of it as a "lambda" is correct. If you can write:
... then you should be able to write:
Of course, a fascinating thing about that example is that Don't you think it would be easier for people to think about these blocks in the same way? That is: & { } // anonymous mixin (DR) immediately evaluated, like ```(function() { })();``` in JavaScript
@lam: { } // anonymous mixin assigned to a variable
.mixin { } // named mixin Since the beginning of Less, selectors have been mixins. Less didn't try to differentiate the two (except that certain selector types were excluded). Why not just make everything a mixin, and align the behavior? Rather than splitting the behavior depending on how the block is defined? Currently those 3 types all behave differently. We already added guards to Was thinking about it more, and |
Further thoughts: If we align DRs and mixins, something like this should be valid: @my-lambda: (@a, @b) when (@b = 1) {
do: stuff;
} However, anonymous mixins couldn't be "overloaded" like named mixins for obvious reasons: // This makes no sense. The second variable declaration replaces the first, and doesn't overload it
// And "default()" should be invalid for anonymous mixins
@my-lambda: (@a, @b) when (@b = 1) {
do: stuff;
}
@my-lambda: (@a, @b) when (default()) {
do: stuff;
} Buuuut... a variable could be overloaded like: .mixin (@a, @b) when (@b = 1) {
do: stuff;
}
.mixin (@a, @b) when (default()) {
do: stuff;
}
@my-lambda: [.mixin]; // pretend mixin reference syntax |
Yes, here we've discussed why
Wait, oh ... (further discussed after #1648 (comment)) I mean you again forget the DR (in its current state) is nothing but a quick |
If your final question is essentially: should mixins and DRs align in behavior? I think we're all saying yes. Yes? |
@matthew-dean Yes, that was the question and I agree with yes too. |
@seven-phases-max (or @SomMeri) Would you be willing to write-up to write up a proposal in less-meta that includes examples for DRs and mixins and the changes in their scoping behavior? That would probably need to include comparisons to current behavior to illustrate the differences. As well, it would be good to demonstrate how someone would replicate any deprecated behavior using some other means. |
@matthew-dean I will think about it, but wont be able to produce anything big for next two months or so. |
Closing in favor of discussion in less/less-meta#16 |
This was mentioned here by @lukeapage: #637 (comment), but was not the point of the main thread, so I'm assuming this is still an issue. (If not, close.)
It would be a cleaner architecture (and more consistent, and less error-prone) to only have variable inheritance one-way. Outer scope -> inner scope. Caller scope -> mixin scope.
This would potentially be a breaking change (3.x).
The text was updated successfully, but these errors were encountered: