sass
sass copied to clipboard
Dynamic mixin invocation
One of the largest stumbling blocks that contributors face right now is being able to cleanly build extendable systems using Sass. While the zip/index method currently proposed as a best practice works OK for variables, it simply does not work for mixins or functions. While we could build a lookup function/mixin for dealing with this, if we are looking to have 3rd party add-ons to a system we've made, the writing of that lookup function/mixin needs to be passed off to the end user, creating a terrible end user experience especially for inexperienced users. As system contributors, we need a way to develop APIs for our systems in a way that is contained from within our extensions. The only way I can see this being accomplished is through mixin and function interpolation.
We are currently running into this problem with attempting to create an Output API for the next generation of Susy, one of the most widely used Compass extensions available (between the two versions, it's something like the 2nd most installed Sass/Compass gem that's not Sass or Compass itself; right behind Bootstrap). While we can create a lookup function/mixin for people to contribute output styles, it leaves the burden on the end user to do it if there are output styles created in contrib. We are thus left with the following user experience, which IMO is terrible:
Inside Extension
$output-styles: isolation, float;
@mixin output-list($input, $output) {
@if $output == 'isolation' {
@include isolation($input);
}
@else if $output == 'float' {
@include float($input);
}
}
@mixin isolation($input) {…}
@mixin float($input) {…}
Inside non-core Output Style
$output-styles: append($output-styles, 'table');
@mixin table($input) {…}
Inside User's file
Would need to write to use non-core Output style, a bit too technical for most users
@debug $output-styles;
Read output styles from debug. Only really works from Command Line, not from GUI apps which many many people use.
DEBUG: "isolation", "float", "table"
@mixin output-list($input, $output) {
@if $output == 'isolation' {
@include isolation($input);
}
@else if $output == 'float' {
@include float($input);
}
@else if $output == 'table' {
@include table($input);
}
}
What we'd much much rather prefer is to do the following.
Inside Extension
$output-styles: 'isolation', 'float';
@mixin output-styles($input, $output) {
@each $style in $output-styles {
@if $output == $style {
@mixin #{unquote($style)}($input);
}
}
}
@mixin isolation($input) {…}
@mixin float($input) {…}
Inside non-core Output Style
$output-styles: append($output-styles, 'table');
@mixin table($input) {…}
Inside User's file
NOTHING! They'd just need to use the system like they'd expect to be able to without any setup! Everyone's happy!
I do think it's incongruous from a user's perspective why they can interpolate a placeholder selector for extend but not for mixin & function definitions and calls. It seems like we can add interpolation for @include and @mixin fairly easily. However, function calls are trickier and I think it may be better to have a generic call($function-name, $arglist...)
function that can be used to call a function with the provided arguments.
My primary use case is for mixins, so I'm happy with that solution. The generic call for function works for me too. Both would solve the issues I need solved.
For another real world example I'm writing a mixin right now that has a keyword list, and needs to call a related function if an argument passed in contains a keyword. With function interpolation I could do (pseudocode) if $keyword_list contains $var { #{var}() }
. So much shorter, more maintainable, more extensible. The call()
function would work great too, just a general +1 to the idea.
Any movement/further thoughts on this? We're running into quite a few places where this functionality would be immensely useful
I'm reasonably open to this, although I doubt I'll have time to implement it in the near future.
I'll take a swing at it. @nex3 do you prefer call
or invoke
(or something else) for a generic function calling function?
call
matches JavaScript, which is a good enough criterion for me.
+1. Was looking for exactly this yesterday, in the same form as with #673. I was surprised to find that it didn't work.
If I understand it correctly, one can do
@media #{$breakpoint} {...}
but not
@include #{$mixin_name};
?
What about a mixin-including function? Something like:
include(mixin_name, args...);
?
@lunelson Interpolating a @media string (which gets printed straight out to CSS) and interpolating a mixin call (which then needs to dynamically call another piece of code) are very different processes, so it shouldn't be too surprising that it didn't work (especially considering you can't interpolate variables or functions either).
As stated above by @chriseppstein, he thinks that they'll be able in do interpolation for mixins with the current @include
syntax and he and @nex3 have agreed they like the call
syntax for functions, I don't think we need to confuse the issue w/more proposed syntaxes.
Thanks @Snugug, I see your point and I missed @chriseppstein's mention concerning @include
above. All good then, glad to hear it.
@chriseppstein how goes the work on the call/invoke methods? I ran into the need today and spent an hour trying to homebrew a solution in pure SASS before I realized it was impossible. I'd love to help, but unfortunately I have no ruby experience, and glancing over the apparently relevant files (interpolation.rb and functions.rb?), I realized this might not be the best project to jump in on headfirst.
If there's anything I can do to help, please let me know. In the interim, is their a recommended solution around this? Should I just build a lookup table for the functions I plan to use and a behemoth if tree?
Thanks!
What is the state of this call/invoke function calling function feature?
Low-priority.
After thinking about this more, I don't really like the idea of adding interpolation support to mixin names. I dislike interpolation and only ever want to use it when there is no way of doing a more readable syntax. For example, I find this to be less readable than some of the options I demonstrate below:
@include #{$some-mixin}($arg1, $arg2);
Since, we have full control over what is a valid syntax for @include
, I don't see a need to resort to a syntax hack. So I would like to consider some different syntax options. Som options that come to mind:
-
@include $some-mixin with ($arg1, $arg2)
-- Any expression that evaluates to a string could be used before thewith
keyword. I think a space after thewith
would be optional. However if thewith
clause is optional for mixins with no arguments (as opposed towith ()
-- which I dislike), this has a degenerative ambiguity where the expression is a simple identifier. So that leads me to think we need some sort of token early on in the mixin expression. -
@include mixin $some-mixin with ($arg1, $arg2)
-- What i don't like about this is that it makes include seem like it could include things that aren't mixins, but it does allow thewith
clause to be omitted for mixins with no arguments. This syntax can be parsed with only a single token look-ahead to disambiguate it from an include of a mixin namedmixin
.
Thoughts?
I like the former much more than the later, but the more I think about this holistically, the more I'm not entirely thrilled by either especially when compared to interpolation. How, for instance, would either of the options proposed work with #366, assuming that's still on the roadmap? Can the answer be implied interpolation?
What if a variable/expression is included for a mixin name (mixins currently strictly disallow for naming conventions that would look like a variable/expresion, and this would stay) that it would attempt to resolve the variable/expression? If it doesn't resolve to a string, throws an error. Once it's been resolved to a string, it uses that as its mixin name? From there it's just mixin as normal, probably passing through a check to see if the mixin exists (and if not, throwing an error).
Something as simple as the following:
$foo: 'bar';
@mixin baz {
content: 'baz';
}
.baz {
@include $foo;
}
This would also work with the syntax in #366:
.baz {
++$foo;
}
Most of the desire to use interpolation on mixins/functions stems from the fact that they are not first-class entities (ie. they can't be passed as arguments to other mixins/functions). If mixins should become a first-class entity, we'll have to have some way of expressing that anyway, even if adding interpolation wasn't on the table.
Snugug's proposed @include $my-mixin
syntax seems the simplest, most logical way to go and would be an easy concept for non-programmers to grasp.
@Snugug @cimmanon The issue here is that using a variable is not forced. Any valid SassScript expression would be allowed, including no indirection at all. So any syntax we choose must allow for a bare identifier to be unambiguous. This is the case for @include $my-mixin
, but this implies that @include $my-mixin($a, $b)
or @include some-fn-that-returns-a-mixin-name($asdf)($a, $b)
would be valid, but they make me a bit squeemish.
@chriseppstein That's exactly what I'm implying. I would assume the same would be true for either of your proposed syntaxes, and that's how it would work with interpolation. For me, the most common use for this feature would be something like the following (and why I like interpolation):
$mixins: 'foo', 'bar', 'baz';
@mixin api($mixin-type) {
@each $mixin in $mixins {
@if $mixin-type == $mixin {
@include namespace($mixin) {}
}
}
}
Or, with interpolation, would look something like the following:
$mixins: 'foo', 'bar', 'baz';
@mixin api($mixin-type) {
@each $mixin in $mixins {
@if $mixin-type == $mixin {
@include #{$mixin}-namespace {}
}
}
}
@Snugug the problem is that we need to know what parts of that expression are the SassScript that means the mixin name and what parts are the argument list so that we can parse them appropriately. It's very hard to suss out the last parenthesis group using either a regexp or recursive descent parsing. As such, I'm pretty sure we need to have a way to demarcate the argument list expressions from the name expression. (correct me if I'm wrong, @nex3)
Ahh, I understand your point now. In that case, this brings me back to one of two things that I think are the best options for this; either magical interpolation but only allowing for variables, or interpolation. I understand that you're not a fan of interpolation, but it does make that distinction for us that you're looking to have, it's an established (if not liked) pattern, and it doesn't add additional complexities to the mixin syntax, allowing for the mixin syntax to take on other forms. Additionally, when it comes to #366 for instance, one of the wants is mixins to look like properties using that syntax, and having interpolation brackets would bake it look identical to property interpolation that we can do now.
Regarding #366, that syntax is simply sugar to support a common use case, but as I envision it, it does not express the full power of the @include
directive. As such, it's just a shortcut parsing scheme for generating an include directive and it could support interpolation like properties do, since the goal of that syntax is to be more property-like. So I don't consider this decision to be linked to that one.
Forcing the use of a variable is unnecessary and arbitrary -- the only places we do that are where the variable is an L-Value (being assigned to).
And I don't agree that interpolation is a better option than the one I specified above. It's a jumble of curly braces and hash signs and it's hard to read. I don't see a need to escape our own syntax. CSS is a verbose language and we do not need to be terse here. I think the include syntax for dynamic mixin names can and should more closely resemble the @for
directive.
Maybe where I'm getting caught up is the additions to the @include
syntax proposed to allow for this to happen with, to me, don't feel right. Maybe the answer is a new directive so we have full control over it, something like @call
? That way, this would work closer to how #812 works? So something like the following:
.foo {
@call baz [with (arglist)];
}
.bar {
@call qux [with (arglist)] {
content: 'Qux';
}
}
I'd even be happy with @call mixin baz…
instead of just @call baz
if that meant that there was a SassScript API of some sorts of creating new literals.
I don't think a completely different directive is warranted to accomplish this. It's still just an include of a mixin. If I'm reading a file and I see @call
and I don't know what it is and I have to go study it. If I see a slightly different way of using @include
I can correctly infer what is going on without having been taught it.
I'm not entirely sure I agree with the later part of your statement. If I saw @include mixin $foo
as a green user, I'd be confused why I needed to tell the interface that's used for using mixins that I'm using a mixin.
@Snugug right. As I stated originally "What i don't like about this is that it makes include seem like it could include things that aren't mixins". But I don't want to solve this with interpolation, nor a new directive. Some other keywords are fine though.
Is there a way for us to solve both use cases presented with the current @include syntax? A semantic suffix works when there is an arglist, but not when there isn't. A semantic prefix works without an arglist, but makes it appar as if there could be more to @import
. What about a flag of some sorts like is being used for @extend
?
@include $foo-bar !flag [(arglist) {}]
Possible options for !flag
could be:
-
!optional
-
!dynamic
-
!call
-
!eval
@chriseppstein Unless the user is really confused, I highly doubt anyone is going to believe that @include
can be used on anything other than a mixin. Other languages seem to do just fine without adding new language constructs for functions vs variables containing functions.
It's been quite a while since I've used PHP, but this should work (also note that passing functions around as arguments to other function is done with the function name as a string, or at least it used to be).
$my-function = 'is_string';
echo $my-function('foo');
JavaScript that we all know and love...
var foo = function(x) { return x * 2 }
console.log(foo(3));
I've been using Haskell in my day to day programming for the past year or so and there's no differentiation between "variable" and "function" (partially because there's no such thing as variables in Haskell), everything is an expression.
foo :: Bool
foo = True
bar :: Bool -> Bool
bar x = not x
If I try to pass an argument to an expression that takes no arguments (eg. foo False
), then the compiler informs me that I'm doing it wrong.
@chriseppstein: Is your argument against interpolation solely readability? Each of the alternate suggestions puts a higher mental burden on me than reading an interpolated string.
If interpolation isn't going away for variables, i.e. #{$foo}-something
, I would much rather see an existing language construct reused for an expected result than a new new directive added.
For me, #{$foo}(args)
is the most understandable, readable option so far.
@cimmanon I don't let PHP inspire any of my language design :)
In the javascript case, the variable foo is bound to a function reference. SassScript does not have first class references for mixins and functions. Are you proposing that we create such a construct? To this point, it hasn't seemed necessary and I still don't see a strong argument for it. It implies we have to introduce new definition constructs, etc. which I'm not a big fan of.
@robwierzbowski We've rejected interpolation for variables. Instead, we're adding a map data structure in 3.3. Regarding you claim that you find interpolation easier to read than a keyword, all I can say is that whenever I see a large block of code making heavy use of interpolation, I find it ugly and unreadable. Maybe this isolated use is not, in and of itself unreadable. But it adds to a general unreadability for code en-masse. It's obviously an aesthetic, so we're not going to reach agreement through persuasive arguments, but I appreciate your feedback.