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 parametrized mixins as detached rulesets to form 'lambdas' #2270

Closed
rjgotten opened this issue Nov 4, 2014 · 118 comments
Closed

Allow parametrized mixins as detached rulesets to form 'lambdas' #2270

rjgotten opened this issue Nov 4, 2014 · 118 comments

Comments

@rjgotten
Copy link
Contributor

rjgotten commented Nov 4, 2014

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

.list {
  .forEach(foo bar baz, (@item, @index) {
    @i : (@index + 1);
    > li:nth-child(@{i}):before {
      content : "@{item}";
    }
  });
}

where .forEach is defined as

.forEach(@list, @lambda) {
  @n : length(@list);

  .for(0)
  .for(@index) {}
  .for(@index) when (@index < @n) {
    @lambda(extract(@list, @index), @index);
    .for(@index + 1);
  }
}

Lambda 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 before blockRuleset in the detachedRuleset parser function and pass the arguments along to the tree.DetachedRuleset node instance. (The tree.DetachedRuleset would need to be extended with the params evaluation from tree.mixin.Definition, ofcourse.)

@seven-phases-max
Copy link
Member

the ugly hack where variables 'bubble up' to parent scope

Just in case this is not ugly hack but well defined language facility.
(I also can't get the idea of solving "up-chain scope issues" with "down-chain parameter pass feature", these do not seem to be related, do they? But well, never mind, the feature remains useful regardless of this anyway)


Well, either way the idea itself is quite self-revealing and the only problem is ambiguous syntax, notice:

@a: {1: 1};      // OK, ruleset
@b: (@a);        // OK, set @b to @a
@c: (@a) (@b);   // OK, array/list
@d: (@a) {2: 2}; // ?!, array or unnamed ruleset with one parameter?
// same goes if this to be used as a mixin arg

In other languages with such facility this problem is solved with some specific keyword/operators preceding such lambda (e.g. [] in C++ or just function in JavaScript).

@rjgotten
Copy link
Contributor Author

rjgotten commented Nov 4, 2014

Just in case this is not ugly hack but well defined language facility.

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.

the only problem is ambiguous syntax
In other languages with such facility this problem is solved with some specific keyword/operators preceding such lambda

May I suggest @a: (@b) => { b : b } ?
It's reminiscent of both C#'s lambda delegates and JavaScript's ES6 Harmony arrow notation for functions with preserved lexical scope. Provided you place detached rulesets at the head of the types the parser tries to apply for expressions (which I think it already is), this should result in a deterministic parsing without ambiguities.

And if you really wanted to have a list with an explicit => symbol in there, well; there's always ~"=>" as an escape hatch. (It's not like you don't already have this problem with calc() to some degree.)

@seven-phases-max
Copy link
Member

rather than something that was thought about up front.

(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).


@a: (@b) => { b : b } - Yes, could be something like this (though personally I extremely don't like => :)

@matthew-dean
Copy link
Member

Per the discussion in #1943, and also related to passing mixins by reference (issue # ?), I would suggest this syntax:

.mixin(@args) {
    box-shadow+: @args black;
}
a {
    each(10px 10px, -20px -20px, .mixin);
}

...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: .for(@list, .mixin); and do whatever you want with the list / mixin parameters.

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).

@seven-phases-max
Copy link
Member

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.

@matthew-dean
Copy link
Member

Per separate discussion with @plugin, this now seems like an ideal candidate to close in favor of a user-defined function via @plugin.

@matthew-dean
Copy link
Member

Also, this is strangely relevant to #1505 which I just commented on, because we're demonstrating passing a CSS list to a custom function.

@seven-phases-max
Copy link
Member

this now seems like an ideal candidate to close in favor of a user-defined function via @plugin

Note that currently you can neither pass identifiers like .mixin to a function (not allowed by the parser) nor you can call a function w/o assigning its result to something... So, no, for now it's simply impossible to implement such each via plugin.

@matthew-dean
Copy link
Member

So, no, for now it's simply impossible to implement such each via plugin.

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.

@rjgotten
Copy link
Contributor Author

@seven-phases-max
So, no, for now it's simply impossible to implement such each via plugin.

Oh... I wouldn't be too sure about that.

@matthew-dean
Copy link
Member

Oh... I wouldn't be too sure about that.

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?

@rjgotten
Copy link
Contributor Author

it seems you're only doing a partial solution with the plugin

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...

Based on your gist, it looks like you're agreeing with @seven-phases-max. No?

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 Definition.prototype.evalCall works when passing parameters into mixins, actually) and then I return that wrapped ruleset as a result.
A detached ruleset, ofcourse can be called to emit CSS.

This wrapping layer solves both problems.

@seven-phases-max
Copy link
Member

@rjgotten

Oh... I wouldn't be too sure about that.

What you show there is a mixin that is not different from for example this (there you just simplify Less loop via some lambda function) but to be fair the plugin is sort of redundant there as we can write a mixin with the identical functionality without any plugin (if I'm not mistaken the plugin there is used only to ban outer scope variables).

So, no, to cancel:

So, no, for now it's simply impossible to implement such each via plugin.

You need to show each as a function not as a mixin ;) (otherwise it's just the same discussion as after #1943 (comment)).

The argument was that you could neither pass identifiers like a mixin to a function

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 @v and @i (or whatever I need) I don't want that predefined @item and @index bloatware! ;)

@rjgotten
Copy link
Contributor Author

as a user I want my code to have lambda args named @v and @i (or whatever I need) I don't want that predefined @item and @index bloatware! ;)

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:

  1. a bind(@name-a, @name-b, ..., @ruleset), and
  2. a pass(@bound-ruleset, @value-a, @value-b, ... )

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 pass and then evaluate the resulting wrapped detached ruleset.

.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'.

@seven-phases-max
Copy link
Member

.each(foo bar baz, bind(v, i, { ...

In that context the three-parens-overkill is capable of more magical things:

.each(foo bar baz, { .function(@v, @i, @l) {
    threesome: @v at @i of ~"[" @l ~"]";
}});

(impl.).

@rjgotten
Copy link
Contributor Author

@seven-phases-max
That's actually what I had been using before plugin functions and as you already noted in your gist: it breaks on overloaded signatures.

You can make it work in the simple case with a 'catch-all' signature based on a rest parameter (so that the .functioncall will always have a matching signature something to match, regardless of the parameters the consumer has supplied).
However, it then starts doing really, reaaaaa------lly weird stuff when you start using nested call structures such as an .each inside an .each. Using the detached ruleset directly sidesteps that issue.

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. ^_^

@seven-phases-max
Copy link
Member

However, it then starts doing really, reaaaaa------lly weird stuff when you start using nested call structures such as an .each inside an .each . Using the detached ruleset directly sidesteps that issue.

Yep, that's why I stick to my DR-free .for after DRs were introduced... It has no problems with nesting, allows custom @item name and the only problem there is to watch it's not used in a mixin to be expanded into the global scope w/o surrounding {}.

@rjgotten
Copy link
Contributor Author

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 @plugin function that pulls assignable return values out of evaluated rulesets...)

@seven-phases-max
Copy link
Member

(Yes; I do have a @plugin function that pulls assignable return values out of evaluated rulesets...)

Just to share an idea. I have some plugin in development (but have not time to finish it soon I'm afraid) that allows you to use native Less mixins as native functions in the canonical way, e.g.:

.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.:

  • make a mixin to accept DR as lambda.
  • make a helper function(s) to hack DRs to make it help to make what the mixin has to make.
  • invent some hacky way to use that mixins if you're not happy with default variable names (or for any other reason the passing-DRs-in syntax does not fit well)
  • etc.

@rjgotten
Copy link
Contributor Author

But the point is: probably you could consider something in this direction too? (to get rid of too deep DR dependencies/hacks).

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?

@seven-phases-max
Copy link
Member

In which case; which one do you take?

Just the same way as anywhere else: LDW.

@seven-phases-max
Copy link
Member

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).

@rjgotten
Copy link
Contributor Author

I actually have primitive functions as well; both for lists and maps.
My list augments are pretty much what you've written as well.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 3, 2018

Come to think of it, maybe we should bail on using the colon-prefix here.
It may introduce ambiguation in the grammar, since it resembles pseudo-classes such as :nth-child()

👍

Okay, so are we closer to consensus on making this a built-in each() function that passes in what we're calling either an "anonymous mixin" or a "detached ruleset with parameters"? I think it should be more the latter in the code-base until we can unify the two ruleset-call types.

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 (@key, @value) is more Less-y. If you have 2 args in your definition, then 2 args will be passed in. If not, value will be passed in.

@rjgotten
Copy link
Contributor Author

rjgotten commented Jul 3, 2018

I'd actually go for (@value, @key) since @value is in 99% of the cases what you'll be iterating over and @key is some secondary thing you might be using in the process.

It also has parity with forEach iteration in JavaScript.
And even though Less isn't JavaScript; let's face it, the both of them operate in the front-end and there'll be a lot of instance of shared mind-space for developers there. Might as well make it easier for them.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 3, 2018

I'd actually go for (@value, @key) since @value is in 99% of the cases what you'll be iterating over and @key is some secondary thing you might be using in the process.

That's fine with me. I'm good with the argument that @key is secondary and not entirely about JS parity, although it's fine that it shares mind-space, as you say.

@calvinjuarez
Copy link
Member

I like where this is! Yeah, I’m for the plain function.

each(@list, (@item, @i) {});

Plain and simple.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 4, 2018

@rjgotten @calvinjuarez

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: each(@list; key, value; {}); Buuuut I recognize that's not ideal.

The thing is, if we want (@item, @i) {} to be a legit value, I see a few problems:

  1. Is (@item, @i) {} a form of detached ruleset value that can be used anywhere? If it's only used with each() and can't be used with any other function, why?
  2. If it can be used elsewhere, what is it? Is it a mixin or a detached ruleset, because a) if it's a detached ruleset, the rules for arguments are non trivial. If it's not the same args as mixins, why not? b) if it's a mixin, well how does that work? Can it have guards? Basically all of this thread needs to be resolved: Unify Mixin and DR behaviour (and related Functions stuff) less-meta#16

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 each(). (It wouldn't be the only special-cased function, as if can receive "when conditions", and no other function can.) Alternatively, we could avoid any parsing changes and "inject" 2 special vars into the block, just like @arguments is now. That's essentially what I did by default in my implementation. We could inject @key and @value into each iteration of the ruleset. Honestly, that's easiest, because I already did the work lol.

So the easiest, which can be implemented today, is basically:

each(@list, {
  prop-@{key}: @value;
});

Unless we special-case / parse each, but special-casing functions and a new rule for each args takes time.

Thoughts?

@rjgotten
Copy link
Contributor Author

rjgotten commented Jul 4, 2018

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.

@matthew-dean
Copy link
Member

@rjgotten

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. @key and @value okay?

@rjgotten
Copy link
Contributor Author

rjgotten commented Jul 4, 2018

If we're only going to loop over lists, I'd use @index rather than @key,
If we're going to support looping over properties (e.g. of a detached ruleset, used as a map/dictionary) than the more general @key is more appropriate.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 4, 2018

If we're going to support looping over properties (e.g. of a detached ruleset, used as a map/dictionary) than the more general @key is more appropriate.

That's exactly what I was thinking; that it would support looping over lists or rulesets-as-maps.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 4, 2018

@rjgotten We could also inject a different var name depending on list type, as long as it was documented which one to use. I've seen languages do that. It would be trivial to determine if it's a ruleset or not as the list.

Actually, we may want to do do @key AND @index (and @value), in case someone wants to do something with the position of a rule in a map. For simple lists, the value of @key could be set to @index, for least surprise.

@rjgotten
Copy link
Contributor Author

rjgotten commented Jul 4, 2018

@matthew-dean
(...)
you're absolutely right. 'rulesets as maps' are ordered maps by definition, so an index makes sense for them as well.

@matthew-dean
Copy link
Member

See: #3263

@calvinjuarez
Copy link
Member

calvinjuarez commented Jul 8, 2018

Actually, we may want to do do @key AND @Index (and @value), in case someone wants to do something with the position of a rule in a map.

This makes me wonder what @index means in the context of a map. That is, in a list loop, you could call extract() and pass the index. It seems like there are a lot of caveats and inconsistencies introduced this way (e.g. extract(@looped-thing, @index) works in some loops, but not all).

@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]; // → ???
})

@matthew-dean
Copy link
Member

matthew-dean commented Jul 8, 2018

So, in the implementation of this, I ran into an issue of nested each(), and wondering if we need named params. In an attempt to name params using (@value, @key) { }, I realized that was waaaay ambiguous for parsing, if we wanted to later do "anonymous mixins" or "detached rulesets with args". As soon as you write @dr: (@value ... up until then, this could finish with @dr: (@value + 1); or @dr: (@value, @key) { }, so it forces backtracking.

That is to say, two points:

  1. Named vars are required if you ever want to nest each() functions. As a first implementation, they're probably not needed? Maybe not address nesting at first? But if you want them later.... how are we going to reconcile injected vs. declared?
  2. I just wanted to make the point that this form for anonymous mixins/dr-args @dr: (@value, @key) { } is really too ambiguous.

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 (@value, @key) { } for DR args is the wrong approach IMO for Less.

Or maybe this is the wrong approach to support each(). I dunno...

@calvinjuarez
Copy link
Member

calvinjuarez commented Jul 8, 2018

@dr: #(@arg1, @arg2) { };

I was thinking about some specifier to make it clear you're defining a mixin. I'd think . should also be acceptable, since standard "nonymous" mixins allow either . or #.

In other words, conceptually, we're just expanding the current mixin form to include no char after the . or #, though they wouldn't behave the way standard mixins would, in that they wouldn't be "author-callable", and they wouldn't allow "overloading" ('cause they'd be defining unique one-off mixins instead of additions to existing ones.

@matthew-dean
Copy link
Member

@calvinjuarez

This makes me wonder what @Index means in the context of a map. That is, in a list loop, you could call extract() and pass the index. It seems like there are a lot of caveats and inconsistencies produced this way.

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 @index to extract() on a map? Because I think that's a completely separate issue.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 8, 2018

In other words, conceptually, we're just expanding the current mixin form to include no char after the . or #, though they wouldn't behave the way standard mixins would, in that they wouldn't be "author-callable".

Probably could be author-callable eventually, but not-callable could work for this first use-case? I wondered which to go with: #( or .(, but I like the idea of either/or.

@calvinjuarez
Copy link
Member

calvinjuarez commented Jul 8, 2018

-less-log: @map[$@key];

$@var works!? I had no idea! I thought it was nixed! That makes me so happy!

@matthew-dean
Copy link
Member

$@ works!? I had no idea! I thought it was nixed! That makes me so happy!

Ha, yeah I slipped it in bug fixes before I released 3.5. ^_^

@calvinjuarez
Copy link
Member

calvinjuarez commented Jul 8, 2018

Probably could be author-callable eventually, but not-callable could work for this first use-case?

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 .(){} and #(){} wouldn't be overloadable/stackable.

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 .(){} mixin, which means "anonymous" is what they are and we can just call them that straight-up.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 8, 2018

@calvinjuarez

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 each(), to be something that could be generalized at some point.

@rjgotten
Copy link
Contributor Author

rjgotten commented Jul 9, 2018

I'd think "anonymous" would suggest "not author-callable"

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.

@matthew-dean
Copy link
Member

@rjgotten Thanks, that's a great explanation.

What do you think about the syntax tweak / PR?

@rjgotten
Copy link
Contributor Author

rjgotten commented Jul 9, 2018

I think the added # or . makes a lot of sense and not just from a parser disambiguation point of view.

.( a ) { } is to .foo( a) { } in Less as function( a ) { } is to function foo( a ) { } in JavaScript.

It literally reads as: "here I declare a mixin" ( the leading . ) "which I won't give a name for further reference."

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 => marker, whereas if you find the .( or #( sequence at the head you already know you'd be dealing with an anonymous mixin without much - if any - lookahead required.

@matthew-dean
Copy link
Member

matthew-dean commented Jul 9, 2018

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 => marker, whereas if you find the .( or #( sequence at the head you already know you'd be dealing with an anonymous mixin without much - if any - lookahead required.

I mean this is my main reason; #() / .() is just easier to implement, but I wouldn't push it if the syntax didn't gel with people. If it gels, then I think it's working as expected in #3263. Please review when you get the chance!

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

7 participants