Skip to content

Instantly share code, notes, and snippets.

@chriseppstein
Created May 6, 2012 21:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseppstein/ce0af5c1886f952978c6 to your computer and use it in GitHub Desktop.
Save chriseppstein/ce0af5c1886f952978c6 to your computer and use it in GitHub Desktop.
In Sass 3.2 and 3.3, Sass will be rolling out changes that make argument passing semantics more flexible to support a number of very natural APIs that are currently not possible.

Variable Argument Passing in Sass

This document outlines how Sass plans to handle passing a variable number of arguments to mixins and functions. These features will be rolled out over two releases. In Sass 3.2, we will release a minimal implementation that handles positional arguments very nicely and keyword style arguments partially. This is enough to handle the most common use cases.

In Sass 3.3, we will add a new data structure representing a map/hash/associative array, this will enable a complete solution to receive and handle unbound named arguments passed to a function or array. The syntax and API for maps is still in flux at this time, so consider any syntax used in this example directional.

This document explains the complete syntax and usage of this feature. Aspects that will not be implemented in 3.2 are annotated.

Basic Syntax

There are two parts to variable arguments. The first is the ability to recieve some or all of the arguments passed to a mixin or function and to be able to work with those values as a collection of data bound to a single local variable instead of as individual local variables.

The second part is the ability to pass a collection of data to a mixin and to have that data be interpreted as arguments to the mixin or function.

In both cases, ellpisis (3 dots: ...) is used to indicate that you are receiving or sending variable arguments.

On the declaration side, the ellipsis will precede the variable that is recieving the variable arguments: ...$args. On the calling side, the ellipsis trail the list or map variable that contains the arguments: $args....

Open Question: Alternate forms of the ellipsis syntax might have the caller have preceding ellipsis and the receiver have the trailing ellipsis. Or, both sides could use a trailing ellipsis.

Note: All of these examples will use mixins, but the same syntax and semantics will apply to @function arguments.

Declaration Syntax

When the last argument to a mixin is a variable followed by an ellipsis, that variable will receive all of the arguments passed which were not bound to the other explicitly declared arguments. This special variable is called the "Argument List Variable" or arglist for short. The arglist variable must be the last variable in the declaration.

Receiving Positional Arguments

The most simple case is where all arguments are received as a single variable

@mixin box-shadow(...$shadows) {
     -moz-box-shadow: $shadows;
  -webkit-box-shadow: $shadows;
          box-shadow: $shadows;
}
.shadowed { @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999) }

becomes the following output:

.shadowed {
     -moz-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
  -webkit-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
          box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
}

Receiving Keyword Arguments

The argument list variable primarily acts exactly like a comma delimited list and can be used as list in all contexts. However the list is a special list which is transparently holding the keywords inside it. To access the keyword arguments as a map, you must use the keywords() function.

@mixin width-selectors(...$args) {
  // Note: type-of($args) == list
  $keywords: keywords($args);
  @each $k in keys($keywords) {
    // Note: type-of($k) == string
    .column-#{$k}{ width: fetch($keywords, $k);}
  }
}
@include width-selectors($small: 40px, $medium: 100px, $large: 350px);

Generates:

.column-small { width: 40px }
.column-medium { width: 100px }
.column-large { width: 350px }

Note: The keywords function returns a map, so it will not be implemented until Sass 3.3.

Mixing named arguments with variable arguments

It's possible to combine variable arguments syntax with named arguments.

@mixin grid-system($column-width, $gutter-width, ...$breakpoints) {
  // Generate your grids
}

@include grid-system(50px, 15px, 320px, 480px, 800px);

Disallowing keywords or positional arguments

Mixins that accept variable arguments will always accept any number positional and keyword arguments.

In order to disallow one type or the other, Sass will add a new @error directive which is exactly like @warn, except it will cause the compilation to fail.

@mixin box-shadow(...$shadows) {
  @if has-keywords($shadows) {
    @error "The box-shadow mixin does not accept keyword arguments."
  }
     -moz-box-shadow: $shadows;
  -webkit-box-shadow: $shadows;
          box-shadow: $shadows;
}
@include box-shadow($unexpected: true); // Raises an error.

or to disallow positional arguments:

@mixin only-keywords(...$args) {
  @if length($args) > 0 {
    @error "The only-keywords mixin does not accept positional arguments."
  }
  // ...
}

Calling Syntax

When you have lists or maps of data that you need to pass as arguments, the ellipsis can be used to expand it into arguments. Ellipses can be used any number of times by the caller and at any place in the argument list.

Expanding a list into positional arguments

You can bind a list of values to positional arguments by preceding the list with ellipsis. Here are some examples of how you can

$some-value: 100px;
$list-1: 1px, 2px, 3px;
$list-2: 10px 20px 30px; // space separated lists can also be used as an argument list.
@include some-mixin($list-1...) // some-mixin(1px, 2px, 3px)
@include some-mixin($list-1..., $list-2...) // some-mixin(1px, 2px, 3px, 10px, 20px, 30px)
@include some-mixin($some-value...) // some-mixin(100px)
@include some-mixin($list-1..., -5px) // some-mixin(1px, 2px, 3px, -5px)
@include some-mixin(-5px, $list-1...) // some-mixin(-5px, 1px, 2px, 3px)
@include some-mixin(-5px, $list-1..., -10px, $list-2...) // some-mixin(-5px, 1px, 2px, 3px, -10px, 10px, 20px, 30px)

Note: the convention in sass is for plain values to automatically be cast to a list containing a single element wherever a list is expected. So any simple value can be passed as an argument list.

Expanding a map into keyword arguments

In the simplest case, a map can be used to bind values to arguments of the same name as the keys of the map:

@mixin some-mixin($a, $b, $c) { }
$my-args: (a: 1, b: 2, c: 3);
@include some-mixin(...$my-args);

However, just as in the case of positional arguments, you can pass keyword arguments at any place in the argument list, but for this to work we need to fix the semantics of mixing positional and keyword arguments which is currently broken in some edge cases.

Fixing the semantics of calling with mixed positional and keyword arguments

Currently in Sass, positional arguments take precendence over keyword arguments. This means that keyword arguments are effectively shuffled to the end of the positional arguments.

This means that the following calls are currently equivalent:

@include some-mixin(1, $b: 2, 3);
@include some-mixin(1, 3, $b: 2);

This is bad. To explain, consider this example:

@mixin some-mixin($a, $b, $c) { }
@include some-mixin(1, $b: 2, 3);

The above example generates an error counter-intuitively because the keywords are moved to the end, causing 1 to be mapped to $a, 3 to be mapped to $b and then the keyword argument overwrites $b to have the value of 2 and $c to have no value and thus be missing a value causing an error.

So staring in Sass 3.2, the semantics will be changed so that keyword arguments first are assigned to named parameters and then positional arguments are distributed to any unbound parameters. Then the remainder of the arguments are bound to the variable arguments (if any is declared).

This means that the following will all be equivalent and result in $a: 1, $b: 2, and $c: 3:

@include some-mixin($b: 2, 1, 3);
@include some-mixin(1, $b: 2, 3);
@include some-mixin(1, 3, $b: 2);

Pass-through Syntax

There are cases where you simply need to wrap an existing mixin or function to augment another one. When you want to share the same argument signature or a very similar one, you can use the argument list variable to pass both positional and keyword arguments.

@mixin replace-text-with-dimensions($image, ...$args) {
  @include replace-text($image, $args...);
  width: image-width($image);
  height: image-height($image);
}

In this case, because the $args variable is an arglist, it will pass all the arguments that were received transparently whether they are positional or keywords.

However, let's assume that you want to remove some keyword arguments from being passed through transparently. If you pass the arglist variable to the wrapped mixin, the keyword arguments will be passed along with it. In order to prevent the pass-thru behavior you need to decouple the positional arguments from the keyword arguments:

@mixin some-mixin(...$args) {
  // make a new list with just the positional arguments.
  $positional: join($args, ());
  $keywords: keywords($args);
  // manipulate $keywords here
  @include similiar-mixin($positional..., $keywords...);
}
@nex3
Copy link

nex3 commented May 11, 2012

Generally liking this proposal.

On the declaration side, the ellipsis will precede the variable that is recieving the variable
arguments: ...$args. On the calling side, the ellipsis trail the list or map variable
that contains the arguments: $args....

Open Question: Alternate forms of the ellipsis syntax might have the caller have preceding
ellipsis and the receiver have the trailing ellipsis. Or, both sides could use a trailing
ellipsis.

I'd prefer to have the ellipsis on the same side of the variable for both declaration and calling. Otherwise I suspect it'll be tough to remember which side it goes on where. Even if there's a good mnemonic for it, I'd rather just not make it an issue at all.

For reference, I believe Java puts the ellipsis after the argument (well, after the type, but same idea).

@if has-keywords($shadows) {

I'm not sure this is better than is-empty(keywords($shadows)) or whatever.

@include some-mixin(1, $b: 2, 3);
@include some-mixin(1, 3, $b: 2);

The first @include in this example is actually an error. I recently made the validation of keyword/positional args much more restrictive. Keyword args now may only be passed at the end of the argument list. It's also an error to specify an argument both positionally and by keyword. I think this is better than the behavior you're proposing.

$positional: join($args, ());

I don't like this. I'd rather have a heuristic that says that an explicit variable keyword map takes precedence over the implicit map in $args.

@chriseppstein
Copy link
Author

  1. Ok we'll put the ellipsis after the variable in all cases.
  2. the has-keywords function is sort of a hack to work around the fact that there will be a release where the keywords function doesn't exist but the var-args feature does. But at the rate I'm moving on this, maybe it won't land :(
  3. I wasn't aware of the semantic changes you made regarding where positional arguments are allowed. I thought I tested against a fairly recent version of sass when I was writing this, but I guess not.
  4. I think the heuristic is probably fine, but it does make augmenting an API by passing a single keyword passed in work less conveniently, but I guess if you can't specify the argument twice anyways then you pretty much need to extract any exists keywords first.

Lastly, as I've let this spec bake in the back of my mind over the last couple weeks I can't help but feel like we failed at our goal of optimizing the common case of not accepting variable keyword arguments. Because accepting positional arguments means you have to perform a follow on check to disallow keywords. otherwise a typo by the end user goes uncaught and is very frustrating for authors if the API doesn't follow this best practice.

Because of this, I'm back to considering the following syntax:

@mixin only-positional($args: arguments()) { /* stuff */ }
@mixin only-keywords($kwargs: keywords()) { /* stuff */ }
@mixin both-kinds($args: arguments(), $kwargs: keywords()) { /* stuff */ }

Hit me up on IM if you want to chat about this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment