SassScriptでリスト関数を拡張する

その昔Sassに凝っていたころに書いたメモを見つけた。供養のために公開。

当時のSassにはセパレータ判別の関数とかなかったので、今は特に必要ないものもあるね。時間経ったね。


グラデーションのmixinを書いたときに「リストを扱う関数が足りないなあ」と思ったので、関数をいろいろ書いてみている、RubyじゃなくSassで。

is-list($arg)

リストかどうかを返す関数。list 型のほか、可変長引数の arglist 型でも true を返す。

@function is-list($arg) {
  @return type-of($arg) == 'list' or type-of($arg) == 'arglist';
}

ただこれ、ちょっと問題があって、('item') などひとつの値を括弧でくるんだものを渡すと false を返してしまう。

$list: ('item');
@debug is-list($list); // DEBUG: false

上の場合 $liststring 型になって結果 false になる。なんでかというと、Sassでは括弧がリストのデリミタではないので、括弧で包んでもリストにならないから。えー……

ただ、もし括弧をデリミタにすると、演算の優先順位を変える括弧と区別がつけられず、(3 + 4) * 5 という式が「3 + 4 → 7 という値の入ったリストに5を掛けている」など解釈されてしまう。ううむ……

ただ、それはそれで困ったもので nth()index() にリストを突っ込んだつもりがアイテムがひとつだったためエラーになったりした。幸い、この前リリースされたSass 3.2.7でリスト関数についてはアイテムがひとつでもリストとして解釈されるように修正されたのでよかったんだけれど、type-of() の挙動は引き続き変わっていない。まあ、仕様なのでちょっと諦める。

余談:アイテムひとつのリストを作る方法

アイテムひとつのリストを作れないわけではない。空のリストをつくって、そこにアイテムをいっこ加えればよい。こうやって作ったリストは型がちゃんと list になる。

$oneitemlist: append((), 'item');
@debug type-of($oneitemlist); // DEBUG: "list"

contains($list, $value)

ある値がリスト内にあるかどうかを返す関数。

$fruits: 'apple' 'banana' 'cherry';
@debug contains($fruits, 'banana'); // DEBUG: true

実装は index() を使っている。

@function contains($list, $value) {
  @if not is-list($list) {
    @warn 'argument error: #{$list}';
    @return 'error';
  }
  @return type-of(index($list, $value)) == 'number';
}

index() だけで済みそうなものだけど、Sassの index() は値が見つかない場合に false を返すので、JavaScriptでやるような indexOf(item) > -1 みたいなのができない。なので type-of() を使い index() が数値を返すかで判定している。

has($value, $in)

has()はただ、contains()引数の順序が替わっただけ。内部的には contains() を使ってる。

@function has($value, $in) {
  @return contains($in, $value);
}
$oyatsu: 'umaibou' 'miyako-konbu' 'big-katsu';
@debug has('banana', $oyatsu); // DEBUG: false

Sass組み込みのリスト関数は第一引数にリストを取るような設計なんだけど、mixinとかfunctionを書いてると、リストではなく値を前に出したほうがコードが読みやすくなるなと思ったことが結構あったので書いてみた。

あと、has()は引数のキーワードを変えている。これでどうなるかというと、ちょっとヒューマンリーダブル感が出る感じに書ける。

$oyatsu: 'umaibou' 'miyako-konbu' 'big-katsu';
@if has('banana', $in: $oyatsu) {
  content: 'わーい';
}

む、書いてみたけど、そんな分り易くないというか、読みにくくなってるだけかも……

separator($list)

Sassのリストはカンマ区切り、空白区切りと、ふたつの書き方がある。そのどっちを使っているか検出する関数。

@function separator($list) {
  @if not is-list($list) {
    @warn 'argument error: #{$list}';
    @return 'error';
  }
  @if length($list) == 1 {
    @return null;
  }
  $list_comma: ();
  $list_space: ();
  @each $item in $list {
    $list_comma: append($list_comma, $item, comma);
    $list_space: append($list_space, $item, space);
  }
  @if $list == $list_comma {
    @return 'comma';
  }
  @else if $list == $list_space {
    @return 'space';
  }
  @else {
    @return 'error';
  }
}

アイテムとその並びが同じでも、セパレータが違うと同一とみなされないことを利用している。

@debug (1, 2, 3) == (1 2 3); // DEBUG: false

中身は同じでセパレータが違う2つのリストを append() しまくってつくってるので、実行コストが多分高い。

reverse($list, $separator)

項目が逆に並んだリストを作る。オプションでセパレータを変更可能。

@function reverse($list, $separator: null) {
  @if not is-list($list) {
    @return $list;
  }
  @if not has($separator, (space comma)) {
    $separator_orig: separator($list);
    @if has($separator_orig, (space comma)) {
      $separator: $separator_orig;
    }
    @else {
      $separator: space;
    }
  }
  $i: length($list);
  $result: ();
  @while $i > 0 {
    $result: append($result, nth($list, $i), $separator);
    $i: $i - 1;
  }
  @return $result;
}

セパレータを変更可能にする必要なんて(たぶん)ないのだけど、もとのセパレータ知らないと返すリストのセパレータをどうすればいいか悩む→セパレータ検出しよう→じゃあ変更オプションもつけようか的な。こうして無駄な機能が増えていくのだろう……

is() ― なんでも系

引数のキーワードでヒューマンリーダブルにしよう計画をやっていてひらめいた。こんなのはどうだろう。

@function is($args_list) {

  $value: nth($args_list, 1);
  $keyword: nth($args_list, 2);
  $target: nth($args_list, 3);

  @if $keyword == 'in' {
    @return contains($target, $value);
  }
}

こう使える。

$fruits: 'apple' 'banana' 'cherry';
@debug is('banana' in $fruits); // DEBUG: true

関数の引数を「スペース区切りのリストひとつ」として扱って、引数を自然文ちっくに書けるようにしている。引数の数は固定しといて、キーワードに応じて処理を振り分ける。キーワードを増やせば拡張可能。

@function is($args_list) {
  ...
  @if $keyword == 'in' {
    @return contains($target, $value);
  }
  @if $keyword == 'typeof' {
    @return type-of($value) == $target;
  }
  @if $keyword == 'not' {
    @return $value != $target;
  }

is(A not B) が要るかどうかはさておき、こんな感じに拡張可能。


という書きかけでした。ほんとはもうちょっと考えてたはずなんだけど忘れてしまった。

RubyではなくSassでというの、当時はアレ感しかない(まあ今もそうか)けど、libsassの隆盛でRubyでの拡張がちょっとという感じもあるし、本体側の拡張よりもSassScriptでできたほうが今後はいいのかもなと少し思った。速く動くのかどうかは知らないけれど。