関数型言語のことなんて知らなくても、ただそこに便利な関数があるので使っていきましょうというお話です。
LodashやRamdaには、リスト操作やオブジェクト操作に関する便利な関数がたくさんあります。
["a", "b"]
と [1, 2]
から { a: 1, b: 2 }
を作る処理を思い浮かべてください。
何も知らなければforループを使って書いたりしそうですが、_.zipObject
やR.zipObj
を知っていればこれを使うだけでできてしまいます。
以下で、Clojureのclojure.coreにあるよく使う関数を中心に、Lodash, Ramdaを使ったときにどうやって実現できるのか調べてみました。
調べてみた感想としては以下のとおりです。
- JavaScript標準の関数ではまだまだ不足している部分が多く、LodashとRamdaは一長一短
- Lodashは副作用がある関数(対象オブジェクトを変更するもの)と副作用がない関数がある。Ramdaは副作用がないものばかり
- Lodashで副作用がないものを使いたいならlodash/fpを使えってことなんでしょうね
- RamdaはTypeScriptの型定義と相性が悪い部分がありそうで、anyを使う場面が多い
TypeScriptの場合、以下でライブラリをインストールするとサンプルコード実行ができます。
(@types/ramda
の型定義は少し古いっぽいです。公式リンクにあったtypes/npm-ramda#dist
を使っています)
npm i -SE lodash ramda
npm i -DE @types/lodash types/npm-ramda#dist
基本的なもの。リスト要素の追加・取得など
tsconfig.jsonのtargetはES2017にしています。
import * as _ from "lodash";
import * as R from "ramda";
console.log("range:", // => [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
_.range(0, 10),
R.range(0, 10));
const dataList = ["A", "B", "C", "D", "E"];
console.log("append:", // => [ 'A', 'B', 'C', 'D', 'E', 'Z' ]
[...dataList, "Z"],
_.concat(dataList, "Z"),
R.append("Z", dataList));
console.log("prepend:", // => [ 'Z', 'A', 'B', 'C', 'D', 'E' ]
["Z", ...dataList],
_.concat(["Z"], dataList),
R.prepend("Z", dataList));
console.log("concat:", // => [ 'A', 'B', 'C', 'D', 'E', 'X', 'Y' ]
dataList.concat(["X", "Y"]),
_.concat(dataList, ["X", "Y"]),
R.concat(dataList, ["X", "Y"]));
console.log("first:", // => A
dataList[0],
_.head(dataList), // _.first(dataList)もあり
R.head(dataList));
console.log("last:", // => E
dataList[dataList.length - 1],
_.last(dataList),
R.last(dataList));
console.log("rest:", // => [ 'B', 'C', 'D', 'E' ]
dataList.slice(1),
_.tail(dataList), // _.drop(dataList)もあり。_.restは別の関数
R.tail(dataList));
console.log("take:", // => [ 'A', 'B' ]
dataList.slice(0, 2),
_.take(dataList, 2),
R.take(2, dataList));
console.log("drop:", // => [ 'C', 'D', 'E' ]
dataList.slice(2),
_.drop(dataList, 2),
R.drop(2, dataList));
console.log("take-last:", // => [ 'D', 'E' ]
dataList.slice(-2),
_.takeRight(dataList, 2),
R.takeLast(2, dataList));
console.log("drop-last:", // => [ 'A', 'B', 'C' ]
dataList.slice(0, -2),
_.dropRight(dataList, 2),
R.dropLast(2, dataList));
console.log("take-while:", // => [ 'A', 'B' ]
_.takeWhile(dataList, (data) => data !== "C"),
R.takeWhile((data) => data !== "C", dataList));
console.log("drop-while:", // => [ 'C', 'D', 'E' ]
_.dropWhile(dataList, (data) => data !== "C"),
R.dropWhile((data) => data !== "C", dataList));
filter/map/reduceなど
このあたりまではJavaScript標準の関数でもわりといけます。
const f = (data: string) => data === "C" || data === "D";
console.log("find:", // => C
dataList.find(f),
_.find(dataList, f),
R.find(f, dataList));
console.log("some:", // => true
dataList.some(f),
_.some(dataList, f),
R.any(f, dataList));
console.log("filter:", // => [ 'C', 'D' ]
dataList.filter(f),
_.filter(dataList, f),
R.filter(f, dataList));
console.log("filterNot/remove:", // => [ 'A', 'B', 'E' ]
dataList.filter((data) => !f(data)),
_.reject(dataList, f),
R.reject(f, dataList));
const mf = (data: string) => data === "C" ? null : "X" + data;
console.log("map:", // => [ 'XA', 'XB', null, 'XD', 'XE'
dataList.map(mf),
_.map(dataList, mf),
R.map(mf, dataList));
console.log("mapNotNull/keep:", // => [ 'XA', 'XB', 'XD', 'XE' ]
dataList.map(mf).filter((data) => data != null),
_(dataList).map(mf).without(null).value(),
R.without([null], R.map(mf, dataList))); // R.pipe(R.map(mf), R.without<string|null>([null]))(dataList)
const mf2 = (data: string, i: number) => data + i;
console.log("map-indexed:", // => [ 'A0', 'B1', 'C2', 'D3', 'E4' ]
dataList.map(mf2),
_.map(dataList, mf2),
R.addIndex(R.map)(mf2 as any, dataList)); // TypeScriptでは<any>でないとエラーになることがある(2018/04現在)
const mf3 = (data: string) => [data, data];
console.log("flatMap/mapcat:", // => [ 'A', 'A', 'B', 'B', 'C', 'C', 'D', 'D', 'E', 'E' ]
_.flatMap(dataList, mf3),
R.chain(mf3, dataList));
const nested = [1, [[2]], [[[3]]]];
console.log("flatten(一段階):", // => [ 1, [ 2 ], [ [ 3 ] ] ]
_.flatten<any>(nested), // TypeScriptでは<any>でないとエラーになることがある(2018/04現在)
R.unnest(nested));
console.log("flatten:", // => [ 1, 2, 3 ]
_.flattenDeep<any>(nested), // TypeScriptでは<any>でないとエラーになることがある(2018/04現在)
R.flatten<any>(nested)); // TypeScriptでは<any>でないとエラーになることがある(2018/04現在)
const rf = (acc: string, val: string) => acc + val;
console.log("reduce(初期値なし):", // => ABCDE
dataList.reduce(rf),
_.reduce(dataList, rf),
R.reduce(rf, R.head(dataList)!, R.tail(dataList)));
console.log("reduce(初期値あり):", // => XABCDE
dataList.reduce(rf, "X"),
_.reduce(dataList, rf, "X"),
R.reduce(rf, "X" as string, dataList));
console.log("reduce(途中でbreak):", // => YABC
R.reduce((acc, val) => {
if (acc.length > 3) {
return R.reduced(acc);
}
return acc + val;
}, "Y", dataList));
console.log("zip/interleave:",
// [ 'XX', 'A', 99 ], [ 'YY', 'B', 88 ], [ undefined, 'C', undefined ], ... ]
_.zip(["XX", "YY"], dataList, [99, 88]), // 最も長いリストと同じ長さになる(存在しない要素はundefinedに)
// [ [ 'A', 99 ], [ 'B', 88 ] ]
R.zip(dataList, [99, 88])); // 2つまでしか指定できない
高度なリスト操作
groupByはよく使うので標準でほしいですね。
const users = [
{ user: "fred", age: 48 },
{ user: "barney", age: 36 },
{ user: "fred", age: 40 },
];
console.log("sort-by:", // => [ { user: 'barney', age: 36 }, { user: 'fred', age: 48 }, { user: 'fred', age: 40 } ]
_.sortBy(users, ["user"]),
_.sortBy(users, (item) => item.user),
R.sortBy(R.prop("user") as any, users),
R.sortBy((item) => item.user, users));
console.log("group-by:",
// => { '3': [ { user: 'barney', age: 36 } ], '4': [ { user: 'fred', age: 48 }, { user: 'fred', age: 40 } ] }
_.groupBy(users, (item) => Math.floor(item.age / 10)),
R.groupBy((item) => String(Math.floor(item.age / 10)), users));
console.log("frequencies:", // => { '3': 1, '4': 2 }
_.countBy(users, (item) => Math.floor(item.age / 10)),
R.countBy((item) => Math.floor(item.age / 10), users));
const dupList = [1, 1, 2, 1];
console.log("distinct:", // => [ 1, 2 ]
_.uniq(dupList),
R.uniq(dupList));
console.log("dedupe:", // => [ 1, 2, 1 ]
_.reduce(_.tail(dupList), (acc, val) => val !== _.last(acc) ? _.concat(acc, val) : acc, [_.head(dupList)]),
R.dropRepeats(dupList));
console.log("shuffle:", // => [ 'D', 'B', 'A', 'C', 'E' ]
_.shuffle(dataList));
console.log("interpose:", // => [ 'A', '$', 'B', '$', 'C', '$', 'D', '$', 'E' ]
R.intersperse("$", dataList));
console.log("reductions:", // => [ 2, 6, 24, 120 ]
R.scan((acc, val) => acc * val, 2, [3, 4, 5]),
R.scan(R.multiply as any, 2, [3, 4, 5])); // TypeScriptではas anyにしないとエラーになる
console.log("partitionAll:", // => [ [ 'A', 'B' ], [ 'C', 'D' ], [ 'E' ] ]
_.chunk(dataList, 2),
R.splitEvery(2, dataList));
オブジェクトに対する操作
複雑なことをしようとすると、toPairs(), fromPairs()のお世話になります。
let dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" };
console.log("get-in:", // => 2
_.get(dataMap, "y.y2"),
_.get(dataMap, ["y", "y2"]),
R.path(["y", "y2"], dataMap));
console.log("assoc-in:", // => { x: 'x1', y: { y1: 1, y2: 2, y3: 33 }, z: 'Z' }
_.set(dataMap, "y.y3", 33), // dataMapは変更される
_.set(dataMap, ["y", "y3"], 33), // dataMapは変更される
R.assocPath(["y", "y3"], 33, dataMap)); // dataMapは変更されない
dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" };
console.log("dissoc-in:", // => { x: 'x1', y: { y1: 1 }, z: 'Z' }
_.omit(dataMap, "y.y2"), // dataMapは変更されない
_.unset(dataMap, "y.y2"), // dataMapは変更される
(() => { dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" }; })(),
R.dissocPath(["y", "y2"], dataMap)); // dataMapは変更されない
console.log("update-in:", // => { x: 'x1', y: { y1: 1, y2: 4 }, z: 'Z' }
_.update(dataMap, "y.y2", (data) => data * data), // dataMapは変更される
(() => { dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" }; })(),
R.over(R.lensPath(["y", "y2"]), (data: number) => data * data, dataMap)); // dataMapは変更されない
console.log("select-keys:", // => { x: 'x1', z: 'Z' }
_.pick(dataMap, ["x", "z"]),
R.pick(["x", "z"], dataMap));
console.log("entries:", // => [ [ 'x', 'x1' ], [ 'y', { y1: 1, y2: 2 } ], [ 'z', 'Z' ] ]
Object.entries(dataMap),
_.entries(dataMap),
_.toPairs(dataMap),
R.toPairs(dataMap));
// forEach
_.forIn(dataMap, (value, key) => console.log(key + ":" + value));
R.forEachObjIndexed((value, key) => console.log(key + ":" + value), dataMap);
console.log("mapValues:", // => { x: 'x#x1', y: 99, z: 'z#Z' }
_.mapValues(dataMap, (value, key) => typeof (value) === "string" ? `${key}#${value}` : 99),
R.mapObjIndexed((value, key) => typeof (value) === "string" ? `${key}#${value}` : 99, dataMap));
console.log("Objectに対するmap:", // => { '$x': 'x', '$y': 'y', '$z': 'z' }
_(dataMap).toPairs().map(([key, value]) => ["$" + key, key]).fromPairs().value(),
R.fromPairs(R.map(([key, value]) => ["$" + key, key], R.toPairs(dataMap)) as any));
console.log("merge:", // => { x: 'x1', y: 'Y', z: 'Z', zz: 'ZZ' }
Object.assign(dataMap, { y: "Y", zz: "ZZ" }), // dataMapは変更される
_.assign(dataMap, { y: "Y", zz: "ZZ" }), // dataMapは変更される
R.merge(dataMap, { y: "Y", zz: "ZZ" })); // dataMapは変更されない
dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" };
console.log("mergeDeep:", // => { x: 'x1', y: { y1: 'YYY', y2: 2 }, z: 'Z' }
_.merge(dataMap, { y: { y1: "YYY" } }), // dataMapは変更される
(() => { dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" }; })(),
R.mergeDeepRight(dataMap, { y: { y1: "YYY" } })); // dataMapは変更されない
console.log("merge-with:", // => { x: 'x1X', y: { y1: 1, y2: 2 }, z: 'Z' }
_.mergeWith(dataMap, {x: "X"}, (v1, v2) => v1 + v2), // dataMapは変更される
(() => { dataMap = { x: "x1", y: { y1: 1, y2: 2 }, z: "Z" }; })(),
R.mergeWith((v1: string, v2: string) => v1 + v2, dataMap, {x: "X"})); // dataMapは変更されない
console.log("zipmap:", // => { a: 1, b: 2 }
_.zipObject(["a", "b"], [1, 2]),
R.zipObj(["a", "b"], [1, 2]));
一つひとつの関数は単純でも、それを組みわせると高度なことが簡単にできるようになるのがLodashやRamdaのいいところですね。