再帰無しでmap/reduce

3 min読了の目安(約3400字TECH技術記事

Illustratorで使えるscriptはES3のものらしい。
普段Elixirばかり書いているのでJSの事情は知らないのですがES5ままではmap/reduceなどのElixirでおなじみの関数は実装されておらず配列操作は forを使うのが一般的らしい。
しかし今更for文を書く事には耐えられそうにないのでmap/reduceを実装することにした

再帰・無し

再帰関数が書ければそもそもfor文なんて書かなくて済む。と思って侮っていたらどうやらJSでは再帰関数を書けないらしい...。かなしい

不本意ながらforを使って便利関数を実装するしか無さそうだ...

reduce

使えないものは仕方ないのでreduceを実装する

var Enum = {};
Enum.reduce = function(arr, acm, f) {
  var res = acm;
  for (var i = 0; i < arr.length; i++) {
    res = f(arr[i], res);
  }
  return res
}

var res = Enum.reduce([1,2,3], 0, function (i, acm) {
  return acm + i;
})

console.log(res);
// => 6

副作用を出してしまっているけどまあ動いてるからヨシ!

map

reduceができたのでreduceを使ってmapを実装する。

Enum.map = function(arr, f) {
  return Enum.reduce(arr, [], function(v, acm) {
    acm.push(f(v));
    return acm
  });
}

var res = Enum.map([1,2,3], function(i) {
  return i * 2;
});
console.log(res);
// => [2, 4, 6]

だいぶいい感じ!

each

イラレスクリプトでは結構手続き的な書き方をすることも多い。
別にmapでやっても良いんだけど後で読み返したときに戻り値は関係ないと伝えたいのでeachも実装しておく

Enum.each = function (arr, f) {
  Enum.map(arr, function (v) {
    f(v);
  });
}
Enum.each([1,2,3], function (i) {
  console.log(i);
});

// 1
// 2
// 3

mapをラップしただけだが意図は伝わる。と思う。

filter/reject

雑にレイヤー上の全テキストフレームを取ってきて特定の文字列が入力されているものだけを抽出するときもあったので実装する。

Enum.filter = function(arr, f) {
  return Enum.reduce(arr, [], function(v, acm) {
    if (f(v)) {
      acm.push(v);
    }
    return acm;
  });
}

ついでにfilterの対になるrejectも実装しておく

Enum.reject = function(arr, f) {
  return Enum.reduce(arr, [], function(v, acm) {
    if (!f(v)) {
      acm.push(v);
    }
    return acm;
  });
}

これでヨシ
ここまでやればほとんどforを見なくて済むはず?

eachSlice

これはニッチな例だけどあると便利なので実装したい。
配列を指定した個数単位の2次元配列にしてくれる関数。

[1,2,3,4,5,6]
[[1, 2], [3, 4], [5, 6]]

こういうのですね。

Enum.eachSlice = function(arr, amount) {
  return Enum.reduce(arr, [], function(v, acm) {
    if (acm.length == 0) {
      var newAcm = [[]];
    } else {
      var newAcm = acm;
    };

    var last = newAcm.slice(-1)[0];
    if (last.length < amount) {
      last.push(v)
    } else {
      newAcm.push([v]);
    }
    return newAcm;
  });
}

この関数、作ったはいいけど保守はしたくないなぁ...

compact

特定の条件で配列にnullが紛れ込むので排除したい。
これはrejectを使えば簡単に実現できそうだぞ!

Enum.compact = function(arr) {
  return Enum.reject(arr, function(a) {
    return a === null;
  });
}

なんかJSのfalseの判定ってもっと簡潔に書けそうな気はするけどイラレスクリプト触ることがそんなに無いから許されたい。

include

指定した値が配列に含まれているか検証したいときに使います。

Enum.include = function(arr, v) {
  var res = Enum.filter(arr, function(a) {
    return a == v;
  });
  return res.length >= 1;
}

(Map)keys

これは配列操作ではないので番外的な感じですが、どうやらES3には Object.keysが無いらしく {}からキーを取得できないとわかった。
for inというのを使うと取得できるっぽいのでこっちも実装しておく

var Map = {};
Map.keys = function(map) {
  var keys = []
  for (var key in map) {
    keys.push(key);
  };
  return keys;
}

groupBy

Map.keysとincludeが実装できたのでgroupByを実装できます!
配列を指定した条件をキーにしたオブジェクトに変換してくれます。

[1,2,3,4,5]

// % 2の結果をキーにした場合
{
  '0' => [2, 4],
  '1' => [1, 3, 5]
}
Enum.groupBy = function(arr, f) {
  return Enum.reduce(arr, [], function(v, acm) {
    var newAcm = acm;
    var key = f(v).toString();
    if (Enum.include(Map.keys(acm), key)) {
      newAcm[key].push(v);
    } else {
      newAcm[key] = [v];
    };
    return newAcm;
  });
}

おわり

やってみた感想

普段お世話になっている関数群を再帰無しで実装してみたので親しみが増した。
これでforを使える言語ならある程度自由にEnumerableなコードを使えそうで安心した。

必要にかられて作っただけなので詳しい方おられたらここが変だよコメントいただけると嬉しいです。