超強力な関数型プログラミング用ライブラリ Ramda.js を学ぼう #2 - lens でオブジェクト操作も思いのまま
wakamsha
lens とは?
端的に言うと、 getter
や setter
を関数型プログラミング的に抽象化したものです。対象となるオブジェクトや配列の特定のプロパティやインデックスからデータを取得したり変換することが出来ます。と、これだけ聞くと何の凄さも伝わってきませんが、ネストの激しい複雑なデータ構造に対して不変性を保ちつつアクセスできるというのが一番の特徴として挙げられます。
元々は Haskell に lens
というパッケージがあり、これを Ramda.js の API として実装したものだと思っていただければ OK です。
諸説ありますが、対象のプロパティに対し『レンズのようにフォーカスする』というのが語源のようです。
オブジェクトのプロパティにアクセスする lens 関数を作ってみる
それでは実際に Ramda.js の lens を使ってみましょう。以下のようなオブジェクトデータを用意します。
type User = {
name: string;
email: string;
age: number;
address: {
zipcode: string;
};
};
const user: User = {
name: 'Bret',
email: 'bred@april.biz',
age: 22,
address: {
zipcode: '92998-3874',
},
};
この user
オブジェクトの age
プロパティにアクセスする lens 関数を作成してみましょう。
import { assoc, lens, prop } from 'ramda';
const ageLens = lens(prop<string>('age'), assoc<string>('age'));
lens
関数は第一引数に getter 関数、第二引数に setter 関数を受け取って Lens
インスタンスを返します。prop
は Ramda.js の標準的な getter 関数です。通常は prop(key, object)
とすることで object にある key の値を返しますが、ここでは key のみをしていすることでカリー化された関数 ( getter ) を返すようにします。assoc
は Ramda.js の setter 関数です。通常は assoc(key, value, object)
と三つの引数を受け取って結果値を返しますが、 key
のみを受け取るとカリー化された関数 ( setter ) を戻り値として返します。
では実際に user
オブジェクトに lens 関数でアクセスしてみましょう。
getter
import { assoc, lens, prop, view } from 'ramda';
const ageLens = lens(prop<string>('age'), assoc<string>('age'));
view(ageLens, users); //=> 22
view
関数はいわゆる Lens の getter を実行する API です。渡された第二引数に渡したデータ構造に対して第一引数に渡した Lens インスタンスの getter を実行した結果を返します。 ageLens
は age
というプロパティに対する Lens インスタンスなので、 user
オブジェクトの age
プロパティにアクセスし、その値を取得します。
setter
import { assoc, lens, prop, set } from 'ramda';
const ageLens = lens(prop<string>('age'), assoc<string>('age'));
const newUser = set(ageLens, 23, users);
console.log(newUser);
{
"name": "Bret",
"email": "bred@april.biz",
"age": 23,
"address": {
"zipcode": "92998-3874"
},
}
set
関数は Lens の setter を実行する API です。渡された Lens インスタンスによってフォーカスされたデータ構造のプロパティ値を更新した結果を返します。ちなみに 引数に渡した user
自体はそのままであり、 newUser
は完全に新規に作られたデータ構造です ( 不変性の担保 ) 。
『lens すごい!!』と言いたいところですが、正直これだけではそこまで旨味を感じられないことでしょう。そもそも name
や age
など第一階層でプリミティブなプロパティならまだしも address.zipcode
のようなネストしたプロパティに対してはどうアクセスすれば良いのでしょうか?prop
も assoc
も単一の key 名しか指定できないため、これでは zipcode
にアクセスするのが非常に大変なことになってしまいます。
ネストしたプロパティに直接アクセスするには
lensPath
ネストしたプロパティに直接アクセスするには lensPath
という API を使います。lensPath
は、その名の通り対象となるデータ構造のプロパティ名をパスに見立てて配列で指定することでダイレクトにフォーカス出来る API です。
import { lensPath, view } from 'ramda';
const zipcodeLens = lensPath(['address', 'zipcode']);
view(zipcodeLens, user); //=> '92998-3874'
パスにはプロパティ名 ( string
) だけでなく配列のインデックスも指定出来るので、以下のようなデータ構造に対しても簡単にアクセス出来ます。
import { lensPath, view } from 'ramda';
const post = {
id: 1,
title: 'ポラーノの広場',
body: `あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモリーオ市、郊外のぎらぎらひかる草の波。`,
tags: [
{ name: '小説', slug: 'novel' },
{ name: '宮沢賢治', slug: 'kenji-miyazawa' },
],
};
const tagLens = lensPath(['tags', 1, 'name']);
view(tagLens, post); //=> '宮沢賢治'
lensIndex と lensProp
lensPath
はネストしたオブジェクトハッシュに対して使えるものですが、対象のデータ構造が配列であれば lensIndex
という API でもアクセス可能です。lensIndex はアクセスしたいインデックス値を渡して使います。
const headLens = lensIndex(0);
view(headLens, ['foo', 'bar', 'baz']); //=> 'foo';
set(headLens, 'hoge', ['foo', 'bar', 'baz']); //=> ['hoge', 'bar', 'baz']
この他にも lensProp
という lens
と prop
の組み合わせを一括で指定できるシンタックスシュガー的な API もあります。
import { assoc, prop, lens, lensProp, view } from 'ramda';
// どちらも同じ
const ageLens1 = lens(prop<string>('age'), assoc<string>('age'));
const ageLens2 = lensProp('age');
view(ageLens1, user); //=> 22
view(ageLens2, user); //=> 22
もともと Ramda.js には lens
API しかなかったのですが、 v0.14.0
で lensIndex
と lensProp
が、 v0.19.0
で lensPath
が追加されました。よって今となっては lensPath
さえあれば全てまかなえることになります。
僕個人としては、アクセスしたいプロパティがネストしてるか否かで lensProp
と lensPath
を使い分けるようにしています。
締め
lens のお陰でどんなに複雑なデータ構造であっても非常に簡単な記述でアクセス出来ます。しかも setter においては、フォーカスした箇所を更新するだけでなく元のデータ自体には一切変更を加えずに完全に同じ構造のデータを新規に生成して返すという、関数型プログラミングの不変性まで担保されているのは驚くべきことです。lens を使いこなすことで JavaScript でのオブジェクト操作はより高次元なものへと昇華します。覚えておいて損はありません。