immutable.jsは、facebook社が作っているJavaScriptのライブラリでimmutableなコレクションを提供してくれます。List, Stack, Map, OrderedMap, Set, OrderedSet and Record
があるとのこと。
関数型言語にインスパイアされて作っているらしいので、関数型に馴染みのある関数が出てきて嬉しいです。
他にも、正格でなく遅延評価してくれるという点も嬉しいですね。
インストール
$ npm install immutable
coffeeのREPLで試します。
$ coffee
まずrequireします。めんどうなのでSeqの変数を置きます。
coffee> Immutable = require 'immutable'
coffee> Seq = Immutable.Seq
簡単な使い方
coffee> Seq([1,2,3]).map((i) -> i + 1)
# Seq [ 2, 3, 4 ]
coffee> Seq([1,2,3]).filter((i) -> i % 2 == 0)
# Seq [ 2 ]
Scalaエンジニアの方、テンションがあがりますね。はい。
可変引数でも書けるようです。
Seq.of(1,2,3)
下の方でもっといっぱい例書きます。
JSからImmutableへ
構造がネストしている場合は、Immutable.fromJS
を使う。
coffee> data = {a: 1, b: [1,2,3]}
#{ a: 1, b: [ 1, 2, 3 ] }
coffee> Immutable.Seq(data)
#Seq { "a": 1, "b": 1,2,3 }
coffee> Immutable.fromJS(data)
#Map { "a": 1, "b": List [ 1, 2, 3 ] }
JSのオブジェクトに変換
toArray/toObject/toJSがあります。
ネストしてなければtoArrayかtoObjectを明示的に使うのが良さそう。Listの中にMapがあったり、ネストしてる場合はtoJSでないとダメ。
coffee> Seq([1,2,3]).map((i) -> i + 1).toArray()
# [ 2, 3, 4 ]
coffee> Seq([1,2,3]).map((i) -> i + 1).toObject()
# { '0': 2,
# '1': 3,
# '2': 4 }
coffee> Seq([1,2,3]).map((i) -> i + 1).toJS()
# [ 2, 3, 4 ]
objectの扱い便利ですね。
coffee> Seq({a: 1, b: 2, c: 3}).map((i) -> i + 1)
# Seq { "a": 2, "b": 3, "c": 4 }
coffee> Seq({a: 1, b: 2, c: 3}).map((i) -> i + 1).toArray()
# [ 2, 3, 4 ]
coffee> Seq({a: 1, b: 2, c: 3}).map((i) -> i + 1).toObject()
# { a: 2, b: 3, c: 4 }
他のコレクション
各コレクションは内部的にはTrie木を使っているらしいので、Listだから添字アクセスはO(n)になるみたいなことは気にしなくていいはず。
mapやfilterみたいなコレクションメソッドは、Iterableに実装してあり、各コレクションはIterableを継承しているので、共通で使えるようになっているみたいです。
Seqを基本的には使い、必要あればListやMapを使うという感じでいいと思います。
Map
updateやmergeなどできるようです。
coffee> Map({"a": 1, "b": 2})
# Map { "a": 1, "b": 2 }
coffee> Map([["a", 1], ["b", 2]])
# Map { "a": 1, "b": 2 }
coffee> Map({"a": 1, "b": 2}).set("c", 3)
# Map { "a": 1, "b": 2, "c": 3 }
coffee> Map({"a": 1, "b": 2}).delete("a")
# Map { "b": 2 }
coffee> Map({"a": 1, "b": 2}).update("a", -> 3)
# Map { "a": 3, "b": 2 }
coffee> Map({"a": 1, "b": 2}).merge Map({"b": 3, "c": 4})
# Map { "a": 1, "b": 3, "c": 4 }
List
Arrayでおなじみのpopやらpushなどが使えます。
coffee> List.of(1,2,3).push(4)
# List [ 1, 2, 3, 4 ]
coffee> List.of(1,2,3).pop()
# List [ 1, 2 ]
coffee> List.of(1,2,3).unshift(0)
# List [ 0, 1, 2, 3 ]
coffee> List.of(1,2,3).shift()
# List [ 2, 3 ]
coffee> List.of(1,2,3).set(0, 9)
# List [ 9, 2, 3 ]
coffee> List.of(1,2,3).delete(0)
# List [ 2, 3 ]
coffee> List.of(1,2,3).clear()
# List []
Set
重複のない集合です。
coffee> Immutable.Set.of(1,2,2,3)
# Set { 1, 2, 3 }
coffee> Immutable.Set.of(1,2,2,3).map((i) -> i + 1)
# Set { 2, 3, 4 }
OrderedSet
追加した順番を保証してくれるSet。
coffee> Immutable.OrderedSet.of(1,3,2,1).toArray()
[ 1, 3, 2 ]
どういうときに使うんだろうか?
Range
範囲(無限)のリストを扱えます。
coffee> Immutable.Range(1, 100)
# Range [ 1...100 ]
coffee> Immutable.Range(1)
# Range [ 1...Infinity ]
coffee> Immutable.Range(1).map((i) -> i + 1).take(3)
# Seq [ 2, 3, 4 ]
Record
クラスのようなものを作れます。immutableなのでEntityとして使うと良さそう。
coffee> Foo = new Immutable.Record({n: 1})
# [Function: Record]
coffee> foo1 = Foo {n: 2}
# Record { "n": 2 }
coffee> foo1.get 'n'
# 2
coffee> foo1.set 'n', 3
# Record { "n": 3 }
mapしたらバグ踏みました...。
coffee> Foo = new Immutable.Record({n: 1})
# [Function: Record]
coffee> foo1 = Foo {n: 2}
# Record { "n": 2 }
coffee> foo1.get 'n'
# 2
coffee> foo1.map((i) -> i + 1)
# undefined
coffee> foo1.get 'n'
# 3
コレクションメソッドの例
flatMap。
coffee> Seq([1,2,3]).flatMap((i) -> if i == 1 then Seq(["one"]) else Seq([]))
# Seq [ "one" ]
flatten。
coffee> Seq.of(1,2,3,Seq.of(4,5),6)
# Seq [ 1, 2, 3, Seq [ 4, 5 ], 6 ]
coffee> Seq.of(1,2,3,Seq.of(4,5),6).flatten()
# Seq [ 1, 2, 3, 4, 5, 6 ]
forEach。末尾の値が返る..。false返すと止まります。
coffee> Seq([1,2,3]).forEach((i) -> console.log(i))
# 1
# 2
# 3
# 3
coffee> Seq([1,2,3]).forEach((i) -> if i == 2 then false else console.log(i))
# 1
# 2
find。マッチした値が返ります。Optionとかないのでundefinedに..。
coffee> Seq([1,2,3]).find((i) -> i == 2)
# 2
coffee> Seq([1,2,3]).find((i) -> i == 99)
# undefined
reduce。JSのreduceと違い、空でも例外飛ばないのですごくいいですね。
coffee> Seq([1,2,3]).reduce((acc, i) -> acc + i)
# 6
coffee> Seq([]).reduce((acc, i) -> acc + i)
# undefined
take。
coffee> Seq([1,2,3]).take(2)
# Seq [ 1, 2 ]
coffee> Seq([1,2,3]).takeWhile((i) -> i <= 2)
# Seq [ 1, 2 ]
first/rest。(head/tail)
coffee> Seq([1,2,3]).first()
# 1
coffee> Seq([1,2,3]).rest()
# Seq [ 2, 3 ]
every。全部trueか、空か。(forall)
coffee> Seq([1,2,3]).every((i) -> i <= 3)
# true
coffee> Seq([]).every((i) -> i <= 3)
# true
some。trueなものがあるか。(exists)
coffee> Seq([1,2,3]).some((i) -> i == 2)
# true
coffee> Seq([1,2,3]).some((i) -> i == 4)
# false
max/count。
coffee> Seq([1,2,3]).max()
# 3
coffee> Seq([1,2,3]).count()
# 3
coffee> Seq([1,2,3]).maxBy((i) -> i % 2 == 0)
# 2
coffee> Seq([1,2,3]).countBy((i) -> i % 2 == 0)
# Map { false: 2, true: 1 }
join。
coffee> Seq([1,2,3]).join(',')
# '1,2,3'
Optionみたいなの導入してほしい。