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

Mixin return values #538

Closed
matthew-dean opened this issue Dec 28, 2011 · 59 comments
Closed

Mixin return values #538

matthew-dean opened this issue Dec 28, 2011 · 59 comments

Comments

@matthew-dean
Copy link
Member

Can I set a return value as a color? As in:

.myselector {
   border: 1px solid .custommixin(@colorValue);
}

Just wondering in response to another user's issue regarding color functions: #488

@cloudhead
Copy link
Member

Oh I see, no. But you can do..

.myselector () {
  @return: 2px;
}

.class {
  .myselector;
  border: @return solid red;
}

pretty ugly though.

@matthew-dean
Copy link
Member Author

Ah, of course. It might be nice to have a returned value for custom color functions (and other custom value functions) when mixin guards are in place. @return could be a reserved variable for LESS. As you say, it's a bit ugly to just have a mixin do nothing but variable assignment when it's called.

Would be nicer to see the first example I posted supported at some point. The rationale being that you have spin, lighten, darken, etc functioning as built-in mixins, so the logic for return values is already there in LESS, it would just be opening up the door for custom mixins.

@matthew-dean
Copy link
Member Author

Not I think that programming said functionality is easy mind you, just that it would empower people to extend the logic a bit.

@cloudhead
Copy link
Member

yes, true, but I also like how it currently is, where the 'functions' are all javascript..

@jacobrask
Copy link

Couldn't variables accept arguments the same way mixins do? My usecase would be for instance:

@boxGlow(@spread: 5px, @color: #f00): inset 0 0 @spread @color;
.myselector {
   box-shadow: 1px 1px 1px #000, @boxGlow(2px, #00f);
}

It's also fairly analogous with JavaScript where functions live in regular variables.

@jacobrask
Copy link

@cloudhead: If I spent some time implementing that feature (I haven't looked at the Less source yet, so I can't promise anything), would you be willing to add it if the code is good enough?

@tylertate
Copy link

Adding a return capability to mixins would preserve the mapping of mixins to JavaScript functions, while still facilitating @jacobrask and @MatthewDL's use case. I've documented this a bit more on issue 637.

@lukeapage
Copy link
Member

duplicate of #538

@lukeapage lukeapage reopened this Feb 25, 2013
@lukeapage
Copy link
Member

woops, this is #538

This was referenced Feb 25, 2013
@akashivskyy
Copy link

This would be a very, very useful solution.

I'm trying to implement dynamically resizing images (like [UIImage resizable...] on iOS) and I would like to use a function which will return an URL string, not property/value pair.

Curently I'm using @cloudhead's solution.

@matthew-dean
Copy link
Member Author

Just to tie this in a nice bow, this should be included when the options.json support is added. Issue: #850

@cloudhead
Copy link
Member

Mixins are mixins, we must not conflate their usage with functions. If having the ability to define custom functions in the style sheet is something we want though, that can be a separate discussion.

@seven-phases-max
Copy link
Member

Mixins are mixins, we must not conflate their usage with functions.

The problem is that there's no way to forbid using mixins as functions. It is possible to use one as the other and they (and me :) get to use that ability. So de facto it's only a matter of syntactic sugar and verbosity:

.sum(@a, @b) {
    @-: (@a + @b);
}

usage {
    width: @-;.sum(20px, 33px);
}

Using same "function" twice in the "same scope":

// short (since 1.6.2):
usage {
    & {width:  @-;.sum(55px, 77px)}
    & {height: @-;.sum(11px, 99px)}
}

// oldschool (since 1.4.x):
usage {
    .-() {width:  @-;.sum(20px, 33px)}
    .-() {height: @-;.sum(40px, 42px)}
    .-();
}

Is there a reason to invent some new "Less function declaration/definition" syntax (different from a mixin definition syntax) only to not allow to use width: .sum(20px, 33px); directly?

@matthew-dean
Copy link
Member Author

What the what did you just do there. That's some next-level Less-hackery going on there, lol.

@axelcostaspena
Copy link

@lukeapage I am sorry I can't understand the state of this issue. You've put a reference to this issue on several others but this is set as closed. Is there a plan to allow to use mixins as lambdas or are you suggesting us to take advantage of the global variable setting workaround? In other issue I've understand you were planning to remove that on some future.

@axelcostaspena
Copy link

@seven-phases-max your approach is awesome but there is no way to pass to the sum mixin the result of calling sum two times, am I wrong?
Talking on a somewhat more universal language, this: foo(bar(1, 2), bar(3, 4))

@seven-phases-max
Copy link
Member

@axelcostaspena Well, everything is possible using mixins, it's just a matter of verbosity (obviously 5 or 6 lines of code do not look like a practical replacement for a oneliner).

@matthew-dean
Copy link
Member Author

@seven-phases-max I'm still skeptical, and I feel like the burden of proof is on the multi-line examples. That said, return @x * @x; feels better than return: @x * @x, because it's more logically clear that the function is receiving / returning the value, rather than being assigned to a property that does...something.

But a return statement feels less and less like Less every passing moment.

As to this:

I still can't see any reason why I'll be forced to write
... (@a + @b + @c) * (@a + @b + @c) ;
instead of normal:

@x: @a + @b + @c;
return @x * @x;

The question is better stated: what is the compelling reason to add more syntax to the language just to split that expression onto two lines? The burden should be to prove a more verbose syntax with extra keywords is necessary, not the other way around. There's nothing "normal" about it, as it doesn't exist yet. And it's still not close to a real-world example. @battlesnake's example was still only a single-line expression.

@function {
    web2-control(@direction, @start, @end, @split: 50%, @spread: 5%): 
        linear-gradient(@direction, @start 0%, @start ((@split-@spread)), @end ((@split+@spread)), @end 100%);
}

I like the idea of functions, but it would be disappointing if we added something heavier to the language than was absolutely needed / useful. We already have 3 types of blocks that "do things": mixins, classes/ids than can be "evaluated" and imported into scope (which makes them sort of non-parameter mixins, but they are somewhat different), and detached rulesets. If you add a functions block, all of these definitions have inconsistent syntax from each other. A mixin isn't defined with an @ keyword but a function is. A detached ruleset block is assigned with a colon (and must terminate with a semi-colon), but a mixin block isn't.

I think this should take a step back and look at the syntax as a whole. A @function syntax with a return statement may be okay, but I don't see the logical alignment between all parts of Less and this syntax. And is there logical alignment between Less functions and plugin functions? Somewhere it feels to me like we're missing some cohesion. Do you know what I mean?

@matthew-dean
Copy link
Member Author

That is, just consider how this looks for a moment:

.class {
  prop: val;
}
.class(@x, @y) {
  prop: @x * @y;
}
@class: {
  prop: val;
};
@function class(@x, @y) {
  return @x * @y;
}

I know that a function must be specially defined to distinguish between other blocks, but maybe that's the problem. If function says @function then mixin should say @mixin. Adding just one puts the parts of evaluated blocks in disagreement. So I'm kind of a 👎 if this is going to be the case.

@seven-phases-max
Copy link
Member

seven-phases-max commented May 31, 2015

@matthew-dean I'm a bit lost then. So how exactly

@function web2-gradient(@direction, @start, @end, @split: 50%, @spread: 5%)
    : linear-gradient(@direction, @start 0%, @start ((@split-@spread)), @end ((@split+@spread)), @end 100%)

instead of

@function web2-gradient(@direction, @start, @end, @split: 50%, @spread: 5%) {
    return: linear-gradient(@direction, @start 0%, @start ((@split-@spread)), @end ((@split+@spread)), @end 100%);
}

solves all that problems you mentioned? Maybe then it would make sense to start with questioning the @function itself and not {}?

I did not comment @function (did you forget who suggested @function above? ;) initially just because my opinion about it as radical as always (so now it will start another syntax fight :):

To be honest personally I'd be more happy with either .function .foo, function foo, function:foo or finally foo:function (weird but at least follows :extend). Adopting Sassish way of using @at-rule syntax for new Less "directives" seems to be just too late for Less even if potentially it's indeed much less CSS-conflicting as anything else.


but I don't see the logical alignment between all parts of Less and this syntax

? So you don't really see any similarity between Less mixins and (whatever syntax) functions? I can guess many users do not understand parametric mixins at all thinking of them as some kind of a primitive "CSS-properties-batch" only, but in this particular function/mixin context, hmm...

Either way:
less-plugin-functions does the whole thing in ~50 lines of code (if I remove all the plugin specific hacks) exactly because it does reuse existing core code for parsing and evaluating mixins. Providing all those existing and documented features (you're not obligated to use) for free.
So considering this, now I only wonder how:

.class {
  prop: val;
}
.class(@x, @y) {
  prop: @x * @y;
}
@class: {
  prop: val;
};
@function class(@x, @y) : @x * @y;

can be potentially better to justify most likely more bloating implementation (+ new parsing code) to provide more limited functionality?


And finally, to back up "feature/syntax consistency" I guess that a reformatted table of existing Less feaures the way I see them would be interesting (just in case):

name   {prop: val}  // a ruleset having some standard CSS selector as a name
@name: value;        // variable
@name: {prop: val}; // *unnamed* ruleset assigned to a variable as its value

// mixin
.name(@x, @y) {
    prop: @x * @y;
}

// function (whatever `func` directive)
<func> name(@x, @y) {
    return: @x * @y; // return property of the function
}

@battlesnake
Copy link

Having no prefix for function names would be consistent with the current state of LESS, i.e.

color: darken(@base, 10%);
background: checkerboard(1em, lighten(@base, 20%), white);

Having no prefix for JS functions, while requiring a prefix for functions supplied in the actual LESS code seems somewhat backwards.

I personally don't see any use for multi-line functions, but I may be having a "who needs >640k of RAM" moment here.

If there was a demonstrable need for multi-line functions in future, after we had adopted the @function name(params) : expression; syntax, we could always permit it by using SCAD-style let blocks:

@function multi-line-thing(@a, @b, @c) :
    let { @sum : @a + @b + @c; @product : @a * @b * @c; @mean : @sum / 3; }
        @sum / @product ~", " @mean;

That syntax would still be @function name(params) : expression;. The syntax for expression would be extended to permit let { assignments } expression.

Variables in such blocks are scoped to the expression following the block only, they don't leak out to parent scope.

This way we could add single-expression functions now, but we still have the option to extend them to multi-line in future (if a need was demonstrated), by either using "recipe" block-style functions (with return) or "sub-scope" functional-style functions (with let).

If block-style was to be used, I like the return: value; syntax over the return value; syntax - since everything that isn't a block is still a key: value pair, with return (or maybe result) just being a special key name within functions. The explicit @function syntax then ensures that return: is not treated as a CSS attribute name.

@rjgotten
Copy link
Contributor

rjgotten commented Jun 1, 2015

Everything is ruleset-like, including functions, which just look exactly the same as mixins. Really, when boiled down, the sole discriminators are that mixins can set CSS properties and that functions have to have a return statement. (You cannot and should not do both.)

So why not just rely on that behavior directly?

E.g.

.mixin(@a, @b) {
  value : @a @b;
}
.func(@a, @b) {
  return @a @b;
}

Then let the compiler figure out at the call site whether it is calling a mixin or a function and raising an exception if one or the other is used in an inappropriate context.

Alternatively, if exceptions aren't your thing:

  • Define the CSS output of a function called as a mixin as an empty ruleset.
  • Define the function return value of a mixin as the collated ruleset of all its applied CSS properties and nested rulesets evaluated into their final state, returned as a detached ruleset.

@battlesnake
Copy link

Hmm polymorphism... it makes sense I guess, mixins can't appear on the right-hand side of a colon and functions can only appear there. As long as we ensure that errors are raised if one is used in the wrong place, using mixin syntax for functions seems ok.

@matthew-dean
Copy link
Member Author

Really, when boiled down, the sole discriminators are that mixins can set CSS properties and that functions have to have a return statement.

This is what I was getting at. Should we create a new ruleset type that has a different method of "define"?

But... I also agree with this:

Having no prefix for JS functions, while requiring a prefix for functions supplied in the actual LESS code seems somewhat backwards.

I've always felt that the requirement that a mixin be prefixed with . or # is ugly and annoying. (See the first part of: #1342.) It's also strangely arbitrary that only two out of three types of common selectors can be converted to mixins. That's only relevant here if we allowed mixins to return values.

However, there is some reasoning to have a separate type, since function vars should not be imported into the caller scope, and functions should not have guards, as it's hard to intuit what that would even mean. So there are other differences other than being able to return values.

I'm not necessarily against @function, obviously I endorsed it earlier in the thread. I'm just wondering how this all fits together, and I want us to be cautious. Note that my criticism is not to turn us away from using any one thing; it's to make sure we're asking all the necessary questions and thinking critically.

Even CSS breaks it's own rules sometimes, and contains things like having the vertical-align property cause two entirely different behaviors. So, Less can create new conventions as well. I just want to help make sure any new solutions keep Less easy, intuitive, conservative, and concise.


I'm a fan of limiting to single-line expressions, but if it seems too arbitrary and restrictive to others, then the multi-line form that seems most clear probably is indeed:

@function {
  foo(@a) {
    return @a;
  }
}
// and
@function foo(@a) {
  return @a;
}

Maybe the best argument for a dedicated "define" is that, different from mixins, a function definition can't or shouldn't appear multiple times (in the same scope). So they're more like @plugin functions in that regard. So, because the properties are different enough from mixins (private vars, single definition, return statement), maybe co-opting mixins just wouldn't work.

@rentalhost
Copy link

Why not allow literal javascript inside less? Basically, it'll allow you use all javascript features inside CSS to create functions. Example:

function abs(a) { return Math.abs(a); }

.test { attribute: abs(-5px); }

Note two things:

  1. Function are literal javascript. It can be recognized by something like: "function /\w+/ ..." and stopped by last "}". The problem is identify where "{" start and where "}" ends. Because we can have some internals structures, like if, for, etc.
  2. I don't thinked about how parameter will be passed to function. For instance, what javascript will receive if I pass 5px? And about pass less functions, like abs(convert(5px, pt))?

@seven-phases-max
Copy link
Member

@rentalhost
First of all, inline javascript statements are allowed in the less.js implementation:

div {
    @x: -10.5;
    width: unit(`Math.abs(@{x})`, px);
}

Secondary, Less is designed as a CSS superset, and CSS has very few to do with C-like languages and JavaScript in particular. Thus direct JavaScript statements like function abs(a) {return Math.abs(a);} will be not just confusing and inconsistent there, but also directly conflicting with native Less/CSS syntax.

So two basic reasons, why a code like above should never be recommended to use and a JS-like syntax should never be considered when designing Less syntax, are obvious:

  1. Inline javascript statements require underlying JavaScript environment for the compiler (e.g. such statements will never be supported in PHP or C# or whatever else ports of Less. This is basically spoils the very purpose of the Less itself, which is designed as a platform/environment agnostic self-contained language).
  2. Fundamental "types / language entities" of CSS and JS are simply orthogonal at best and yet again directly conflicting at worst. As you already noticed yourself, there's no way for JavaScript to directly support values like 5px, red, url(//foo/icon.png) and so on and so on. So to pass CSS values and statements to "literal javascript" you will have to provide an intermediate syntax and conversion facility to merge those orthogonal things in the same language. It's not actually a big deal to design such a language, but it will have nothing to do with Less (which is yet again designed as a CSS superset and has nothing to do with JS at all).

In other words, all our discussions above are not a result of so called "not invented here" syndrome, but just an attempt to fit possible function definition syntax to already existing Less syntax-base as smoothly as possible.

@seven-phases-max seven-phases-max removed this from the 1.6.0 or 2.0.0 milestone Feb 4, 2016
@andrewhosgood
Copy link

Please please please:

.halfway-between(@large, @small) {
    max(@large, @small) - (abs(@large - @small) / 2);
}

😃

@matthew-dean
Copy link
Member Author

matthew-dean commented Sep 20, 2016

@andrewhosgood What happens here?

.halfway-between(@large, @small) {
    max(@large, @small) - (abs(@large - @small) / 2);
}
.halfway-between(@large, @small) {
    property: value;
    foo: bar;
}
.rule {
    width: halfway-between(3, 2);
}

Ultimately, a mixin is a parameterized ruleset that can have multiple matches, and returns multiple values, and a function name can only have one match (per scope) and can only return one value.

I think @battlesnake had the simplest and most straightforward syntax proposal:

@function square(@x): @x * @x;

A slight adjustment so that the scope of @x isn't confused:

@function square(@x) { @x * @x; }

Or (possibly) so that we don't confuse with JS-based functions:

@expression square(@x) { @x * @x; }

I think single-use expressions would be simplest to grok, and wouldn't pollute understanding of mixins. And, would also serve most of the examples given. Basically, you're just short-handing (aliasing) expressions for quick re-use. Thoughts? I do wonder if there's a way to "define" the expression name without an at-rule, though. Hmm.

@andrewhosgood
Copy link

@matthew-dean Point taken. Just trying to use existing patterns. Amending my request then, would it be:

@function halfway-between(@large, @small) {
    max(@large, @small) - (abs(@large - @small) / 2);
}

Not worried about syntax, but this would then allow me to do the thing I can't do currently.

@matthew-dean
Copy link
Member Author

matthew-dean commented Sep 21, 2016

Syntax is the hardest part to get right. And you want to get it right because it's a new language feature.

I'm a little wary of @function now, more than I was when this discussion was active in the past, just because of @plugin-defined functions and existing functions in Less.js. And without a return statement it's not very function-y.

@expr square(@x): { @x * @x };
@expr halfway-between(@large, @small): {
    max(@large, @small) - (abs(@large - @small) / 2)
};

Thinking over it more, I think the colon : assignment and the semi-colon ; terminator is more logical. In that square(@x) is being assigned the value of the evaluated expression. This would seem to keep it more in line with detached rulesets. The braces {} indicate that the scope of @x is not the surrounding scope of the statement, but what is being assigned through the expression alias.

I like this simpler concept of re-usable expressions rather than "functions" (which already means "defined by the parsing language" i.e. JavaScript). So @expression or @expr makes sense to me here. I think re-usable expressions has more wide-spread utility and is easier to understand.

@rjgotten
Copy link
Contributor

rjgotten commented Sep 21, 2016

I like this simpler concept of re-usable expressions rather than "functions"

Basically; they're parametrized pre-processor macro expansions like in plain old C, right? But hopefully without the crazy abuse that came with those, since Less will have @plugin functions as a far more decent alternative for the heavier lifting and true complex functions.

@seven-phases-max
Copy link
Member

seven-phases-max commented Sep 21, 2016

@rjgotten

a far more decent alternative for the heavier lifting and true complex functions.

#538 (comment). Though never mind.


And I still object the inventation of the special syntax hardcoded to specific "one liner" examples. Still smells like "I love to invent new cool syntaxes" syndrome.
After all it's just asking for more issues from the parser implementation point of view... For instance recall #2785 and think how ambiguous

{
   foo();
   foo() - bar();
   bar();
}

becomes actually. There you'll have to introduce a lot of new parsing rules to detect which statement is actually the function returning value and which are just root function calls (not even counting a friendly error messages if something goes wrong).
So in short, I think everything I wrote in #538 (comment) is still in effect.

And yes, stressing the Sassish @at-rule directive yet again - never been in Less yet, so why is it going to be now? (It seems like someone is just using too much PostCSS recently ;).

@matthew-dean
Copy link
Member Author

matthew-dean commented Sep 21, 2016

@seven-phases-max

{
   foo();
   foo() - bar();
   bar();
}

I was implying that multi-statements would be invalid here (would evaluate to a single expression), so that wouldn't apply, BUT....

...stressing the Sassish @at-rule directive yet again - never been in Less yet, so why is it going to be now? (It seems like someone is just using too much PostCSS recently ;).

I haven't been using PostCSS, but yes, writing both @function and @expression made me feel somewhat dirty. As I mentioned when this issue was re-commented on, much of the syntax discussion has moved even further away from this possibility. Less just doesn't have a matching syntactical paradigm for this, because everything is declarative. Meaning, even in a return statement in a mixin, these two statements would reasonably be equal, since values are constants for a scope (once resolved).

.function-sqralpha(@c) {
    @a: alpha(@c);
    return @a * @a;
}
.function-sqralpha(@c) {
  return @a * @a;
  @a: alpha(@c);
}

So, either way, with what's discussed so far, it seems somewhat square peg / round hole. I don't think mixins can reasonably pretend to be functions (without some compromises and unexpected behavior), and defining functions (or expressions or macro expansions) via Less (that is, in your stylesheet) has proven to also be problematic.


On a separate note-

One thing that might lean us even a bit more to closing this thread: when I was looking back through comments, including the one you linked to, you had this as a not an ideal way to solve the above problem.

functions.addMultiple({
    sqralpha : function(c) {
        var a = c.alpha; // no docs and no guaranty this internal stays the same, but there's no easy way to reuse built-in Less `alpha()` here
        return new tree.Dimension(a * a); // Dimension? some kind of quantum mechanics?
    }
});

With Less 3.0, I've simplified returns so that any non-Node (other than falsey values) returned by a function is cast to an Anonymous Node, such that it's easily output into your final CSS. Obviously, if you want to include that result in another expression, you need to be explicit in the Node type. Buuuuut.... 3.0 also includes a "late parsing" feature (all values are Anonymous until referenced - this allows property referencing without creating extra parsing work or extra nodes), so that could be repurposed in order to just be like:

functions.addMultiple({
    sqralpha : function(c) {
        var a = c.alpha; // docs have started! doing my best!
        return a * a;     // Cast to Dimension because Less is cool... maybe?
    }
});

And considering 3.0 should be all about functions and @plugin, and the remaining list of things it needs to release... I would say at the least, this isn't the right time for this feature. I did my best to simplify the latest examples, but even with that, your objections are valid, and I'm ok to close for now.

@rjgotten
Copy link
Contributor

rjgotten commented Sep 26, 2016

@seven-phases-max
#538 (comment). Though never mind.

No no; you raise a good point.

There's substantial overhead for the middle-ground, where you have more than a simple one-liner expression to do, but not enough to warrant firing up a JS plugin and writing code that handles all the node-type mappings.

While @matthew-dean raises a good point that node return types could be inferred, that still feels like a lot of overhead for some simple math or string interpolation. (In particular the latter; string interpolation requires a temporary variable, so you can never write a one-line expression for it.)

If working a usable return into the syntax is the problem, then why not skip the explicit return statement altogether by specifying what name the out-variable will be bound to:

.answer(@a, @b):@c {
  @c   : ~"The answer is @{sum}";
  @sum : @a + @b;
}

This feels natural with the order independence and declarative nature of the Less language. It also directly extends the signature of mixin declarations in a non-ambiguous manner, which the parser can look for and which avoids the need for an @expression or @function keyword or custom at-rule.

This also means it can work together with mixin overloading and multicast calls. If a mixin is called like a function, it requires that a declaration exists which has both a matching parameter signature and a colon-separated out parameter specified. And if multiple matching overloads exist, the return value becomes a list of the relevant calls in mixin declaration order. (Just as how multiple matching overloads currently output rules in declaration order.)

@matthew-dean
Copy link
Member Author

Huh. That's actually not so bad.

Question: what happens then with properties, values, and other variables declared in the mixin (if called as a function). Safe to presume they're isolated from the caller scope (since properties couldn't be omitted in the location of the call, if the call is within a value expression)?

That's not bad. We expanded functions to be called everywhere. So it could be that mixins could be called everywhere with the same checks to see if the resulting output node is legal in that location. So then functions = JS and mixins = LESS.

I also liked how you addressed and resolved multiple matching mixins (a list as the result). Clever. And declarative.

I was ready to give up on this idea but this is really not bad.

@matthew-dean
Copy link
Member Author

We could also update the documentation that mixins, by default, "outputs" a ruleset, unless an output var (or vars, or $properties??) are specified.

@matthew-dean matthew-dean changed the title Mixin return values (UPDATED: w/ discussion on @function syntax) Mixin return values Nov 7, 2016
@distransient
Copy link

distransient commented Mar 28, 2017

Has there been an update for this?

@matthew-dean
Copy link
Member Author

@distransient There has been more in-depth discussion around designing solutions for this in: less/less-meta#16.

(Less-meta is a repo for higher-level goal planning for Less.js, rather than individual issues or bug reports.)

@stale
Copy link

stale bot commented Nov 14, 2017

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

@stale stale bot added the stale label Nov 14, 2017
@stale stale bot closed this as completed Nov 28, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests