-
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
Allow parametrized mixins as detached rulesets to form 'lambdas' #2270
Comments
Just in case this is not ugly hack but well defined language facility. Well, either way the idea itself is quite self-revealing and the only problem is ambiguous syntax, notice:
In other languages with such facility this problem is solved with some specific keyword/operators preceding such lambda (e.g. |
When I mention it being a 'hack', I specifically mean that it seems hacked into the existing language desgin (to facilitate return values as a later addition), rather than something that was thought about up front. I don't mean the actual code is of low quality. Actually; the LESS parser and AST are quite well done. (Tip of the hat for that.) However, the fact that it's well-defined and well-engineered doesn't make it less ugly when used. It's a language feature that is completely counter to intuition for most, as no other mainstream language does this.
May I suggest And if you really wanted to have a list with an explicit |
(Though we're quickly going offtopic - Yes, it was not something thought of the earlier Less days but today it still remains the only way to "return" something from something in Less (since the "function" facility is still somewhat neglected, though actually the whole thing is not limited to returning variables). So I insist :) that "parent scope leak" has nothing to do with lambdas, after all lambdas do not return anything at all. And the rest is another unrelated story).
|
Per the discussion in #1943, and also related to passing mixins by reference (issue # ?), I would suggest this syntax:
...equal to... .mixin(@args) {
box-shadow+: @args black;
}
@list: 10px 10px, -20px -20px;
a {
each(@list, .mixin);
} ...equal to... @list: 10px 10px, -20px -20px;
a {
each(@list, (@args) {
box-shadow+: @args black;
});
} However, there's no reason that you shouldn't be able to use the same construct for user-defined mixins: To be accurate, this is an extension of mixins / parameters, and not detached rulesets, and passing mixins by reference has been discussed elsewhere (although I couldn't find it with a brief search). |
It was mentioned in #965 (comment) and then in more details discussed #1648 (comment) and following comments. |
Per separate discussion with |
Also, this is strangely relevant to #1505 which I just commented on, because we're demonstrating passing a CSS list to a custom function. |
Note that currently you can neither pass identifiers like |
Oh. Damn. Welll.... wouldn't it seem like a good candidate for adding more flexibility for plugins? What I mean is, it's a narrow use case, and I think we had talked about narrow use cases as candidates for user-defined plugins. OR optional core plugins (which isn't defined yet). So, to me, if this went in, it's either expanding core in one way or another. One would be specific to this use case, and one would be expanding plugins to support scenarios like this use case. |
|
Yes, but in your example, it seems you're only doing a partial solution with the plugin, and then doing the other part in a recursive mixin. My question was basically if a plugin could do this (from one of my examples above: @list: 10px 10px, -20px -20px;
a {
@plugin "awesome-list-functions-which-contains-each";
each(@list, (@args) {
box-shadow+: @args black;
});
} Based on your gist, it looks like you're agreeing with @seven-phases-max. No? |
I could just as well put the loop construct into the actual function. I chose not to, to illustrate that you can infact have full lambda delegate functionaity just by binding variables into the detached ruleset scope. Infact; check the same gist again. I just updated it with named variables support...
The argument was that you could neither pass identifiers like a mixin to a function (so you could pass parameters to the mixin serving as a lambda delegate) nor execute a function without assigning its output to a variable or property (preventing you from using it to make a lambda that can emit CSS). In my case, I use the function to wrap a second detached ruleset around the original, which injects the lambda's 'parameters' (very similar to how This wrapping layer solves both problems. |
What you show there is a mixin that is not different from for example this (there you just simplify Less loop via some So, no, to cancel:
You need to show
And you still can't. DR is not a mixin! :) (it can't have parameters for example). E.g. as a user I want my code to have lambda args named |
Did you miss the part where the parameter names are custom bindable? My example has them fixed, but you could have them passed in as an additional argument to whatever mixin is going to process the lambda ruleset. Using my gist as an example: .each(@list, @params, @ruleset) {
@length : length(@list);
.iterate(1);
.iterate(@index) when (@index =< length) {
@item : extract(@list, @index);
@out : lambda(@item, @index, @params, @ruleset);
@out();
.iterate(@index+1);
}
})
.each(foo bar baz, v i, {
value-@{i} : @v;
}); That's not very DRY though and it puts burden on mixins offering callback semantics to set this kind of parameter configuration up. A better way would be to split the lambda function itself out into two components:
At that point a caller has complete control over parameter names: .each(foo bar baz, bind(v, i, {
value-@{i} : @v;
}); and internally the mixin would not have to worry about them, and simply call .each(@list, @ruleset) {
@length : length(@list);
.iterate(1);
.iterate(@index) when (@index =< length) {
@item : extract(@list, @index);
@out : pass(@ruleset, @item, @index);
@out();
.iterate(@index+1);
}
}) Sure; it's still not a real native lambda, but it gets awfully close, doesn't it? ;-) Ofcourse, it's all still a hack that would hopefully be replaced with native language support at some point in time. But until then it's a nice semantically (and almost syntactically) compatible 'polyfill'. |
In that context the three-parens-overkill is capable of more magical things:
(impl.). |
@seven-phases-max You can make it work in the simple case with a 'catch-all' signature based on a rest parameter (so that the Anyway, I think I just had an epiphany on setting this up with some nicer syntax. I'm going to hack on it a bit more and see if it will work. ^_^ |
Yep, that's why I stick to my DR-free |
So; some news. I kind of figured out how to get a reduce operation to work: .test {
@list : 1 2 3;
@init : 0;
@step : { @return : @previous-item + @item; }
.reduce(@list, @init, @step, {
value : @result;
});
} .test {
value: 6;
} (Yes; I do have a |
Just to share an idea. I have some plugin .function-foo(@x) {
return: @x * 5;
}
usage {
width: foo(100px); // -> 500px
} Obviously this requires a few dirty hacks from within the plugin (mostly related to getting the proper scope context for the called mixin inside its function-wrapper) since that's not really something plugins are supposed to do at all. But the point is: probably you could consider something in this direction too? (to get rid of too deep DR dependencies/hacks). As I see now the method you use to make all these things is quite curvy, i.e.:
|
It's actually surprisingly clean, moreso now that I've evicted some more of the cruft. ;-) Anyway, wouldn't mixins give you a few problems when used as lambdas? One I can think of off the bat is that a single mixin call may match multiple definitions and thus can result in multiple return values. In which case; which one do you take? |
Just the same way as anywhere else: LDW. |
Ah, and again just in case: https://github.com/seven-phases-max/less-plugin-lists (I'm doing last minute make-ups before publishing). This thing is strictly orthogonal to what you're crafting (primitive functions vs. iterator algorithms), but it would be curious to see how both will affect various use-cases (key-value look-ups in particular). |
I actually have primitive functions as well; both for lists and maps. |
👍 Okay, so are we closer to consensus on making this a built-in If we're closer, I'd like to post this question: Given that someone may have key/value lists, or want an index, which is more the "Less way" -> each(@list, (@value) { }); // and...
each(@list, (@key, @value) { });
// or
each(@list, (@value) { }); // and...
each(@list, (@value, @key) { }); Unlike JavaScript, in Less the number of arguments is significant. So there's no programmatic reason to always put value first, because key won't be set (passed as a parameter) unless there's a matching definition. So I'm inclined to say |
I'd actually go for It also has parity with |
That's fine with me. I'm good with the argument that |
I like where this is! Yeah, I’m for the plain function. each(@list, (@item, @i) {}); Plain and simple. |
Incidentally, I did a version of this with no parsing changes a little over two years ago. See the commits on this branch: https://github.com/matthew-dean/less.js/tree/feature/each The way I did it with no parsing changes is that the second param could be a list, as in: The thing is, if we want
Basically, that form would make a ruleset more like a mixin, which is okay, since we want to make one "thing". But then a single set of rules needs to be defined. So, we could resolve by not allowing this "shape" of value anywhere else, and special-casing the built-in So the easiest, which can be implemented today, is basically:
Unless we special-case / parse each, but special-casing functions and a new rule for each args takes time. Thoughts? |
Injection of the two parameters seems fine as a first step to get the feature out and working, and then revisit to add proper user-configurable parameter names once the detached ruleset vis-a-vis mixins issues are cleared up. |
That's fine by me. |
If we're only going to loop over lists, I'd use |
That's exactly what I was thinking; that it would support looping over lists or rulesets-as-maps. |
@rjgotten Actually, we may want to do do |
@matthew-dean |
See: #3263 |
This makes me wonder what @map: {
foo: bar;
baz: qux;
};
each(@map, {
-less-log: extract(@map, @index); // → ???
// Or would this mean anything? (Certainly after a unification of access and extract that I've seen floated.)
-less-log: @map[@index]; // → ???
}) |
So, in the implementation of this, I ran into an issue of nested That is to say, two points:
Maybe something more like this in the future: @dr: #(@arg1, @arg2) { };
@dr(1, 2);
each(@list, #(@value, @key) { }); So, the thing is, if you ever WANT nesting, then maybe injecting vars is the wrong approach. And if you need named vars, then just a plain Or maybe this is the wrong approach to support |
I was thinking about some specifier to make it clear you're defining a mixin. I'd think In other words, conceptually, we're just expanding the current mixin form to include no char after the |
I don't see any inconsistencies produced there. You can change your example to something that works with the current PR. @map: {
foo: bar;
baz: qux;
};
.box {
each(@map, {
-less-log: @map[$@key];
})
} outputs: .box {
-less-log: bar;
-less-log: qux;
} You can also do: @list: a b c d;
.box {
each(@list, {
-less-log: extract(@list, @index);
})
} Which outputs: .box {
-less-log: a;
-less-log: b;
-less-log: c;
-less-log: d;
} Or are you simply pointing out that you can't actually use |
Probably could be author-callable eventually, but not-callable could work for this first use-case? I wondered which to go with: |
|
Ha, yeah I slipped it in bug fixes before I released 3.5. ^_^ |
I'd think "anonymous" would suggest "not author-callable", but maybe "anonymous" isn't the word. Maybe this is something different. Also (and I edited the other comment with this thought, too, but I'll put it here so it isn't as hidden), I think the major difference is that each(@list1; .(@value, @key, @i) {})
each(@list2; .(@value, @key, @i) {}) // this mixin probably shouldn't overload, right? And that would mean authors would have no specific way to reference and call a specified |
Wouldn't we eventually do this? @mixin: #(@op1, @op2) {
value: @op1 + @op2;
}
@mixin(2px, 1px); The reason being what you said: i.e. vars are not overloadable. So you could essentially "replace" mixins by reassigning the variable. @mixin: #(@op1, @op2) {
value: @op1 + @op2;
}
& {
@mixin: #(@op1, @op2) {
value: @op1 - @op2;
}
@mixin(2px, 1px);
} Seems like a fairly useful pattern. You could also pass around mixin definitions to other mixins, which you can't do now. (In 3.5 you can pass in mixin calls i.e. the resulting ruleset to a mixin, but not definitions.) That's where I was going with it; that is, the reason why a prefix is needed. If detached rulesets are useful to pass around and call, then it makes sense that rulesets w/ args would be just as useful/powerful, if not more so. That's why I would want this pattern, if used for |
The "anonymous" in "anonymous function" comes from the fact that it is constructed without its own named reference. Without its own named reference, it cannot be directly accessed through name by code at other locations. In that sense, such a function is "not directly author-callable," but is still callable if the author assigns the original function reference to a variable. So yes; "anonymous mixin" is definitely the correct terminology here. |
@rjgotten Thanks, that's a great explanation. What do you think about the syntax tweak / PR? |
I think the added
It literally reads as: "here I declare a mixin" ( the leading The only alternative syntax that would make sense is imho to clone the arrow-function syntax from JS, C#, etc. and make it like: each(@list; (@value, @key, @index) => { }) That would work as well, but is probably much harder to implement as it requires a lot of look-ahead through the arguments list to find the |
I mean this is my main reason; |
Fixes #2270 - Adds each() function to Less functions
It seems that currently LESS only supports 'pure' rulesets to be passed along as mixin arguments or stored in variables as detached rulesets.
I suggest to extend this support to incorporate parametrized mixins, essentially giving LESS the capability to work with lambdas.
E.g.
One would be able to write
where
.forEach
is defined asLambda mixin support would also neatly resolve recurring issues with function return arguments and the ugly hack where variables 'bubble up' to parent scope if said variables are as of yet undefined in said parent scope.
The suggested practice could become to adopt continuation style programming; passing 'return values' along into a lambda mixin to continue down the scope chain. This kind of mechanism is more transparent to users, less brittle by avoiding issues with potential variable name collisions and just fits in better with the overall functional programming paradigms that the LESS syntax is built on.
[EDIT]
Having just had a look at the way detached rulesets and calls are implemented in the AST, I think very little needs to happen to make this work. Even on the parser side of things, it seems fairly simple to just parse an optional block of
mixin.args
beforeblockRuleset
in thedetachedRuleset
parser function and pass the arguments along to thetree.DetachedRuleset
node instance. (Thetree.DetachedRuleset
would need to be extended with the params evaluation fromtree.mixin.Definition
, ofcourse.)The text was updated successfully, but these errors were encountered: