JavaScriptのOptional Chainingを解説
プログラミング::beginners
序
会社で同僚に「関数が定義されていたら呼び出したいのであればfunc?.()
でいいじゃん」と言ったら驚かれた。
その人は結構Optional Chainingが好きなのだが、なぜその発想にならなかったのか疑問でしょうがなかった。 が、よくよく考えてみると、Optional Chianingに対する理解の仕方によってはありえなくはないということに気づいた。
ここでは、Rubyの同等機能であるぼっち演算子と対比しつつ解説しよう。
初歩的な説明
Optional Chainingはレシーバがnon-nullである場合にプロパティへのアクセスを行う機能である。
つまり、
?.x foo
というコードはおよそ
if (foo != null) foo.x
というものに近い。ただし、foo
へのアクセスは一度しかされないため、foo
が副作用のあるgetterだった場合、その副作用は1度しか発生しない。
Rubyの「ぼっち演算子」の場合もほとんど同じだが、レシーバがnon-nullである場合、メソッドを呼び出す。
&.x foo
これは
.x unless foo.nil? foo
に近い。
「言葉が微妙に違うだけ」と思うかもしれないが、実はこの言葉の違いが、この演算子の本質的な違いに直結してくる。
なお、JavaScriptのコードは比較対象はx != null
である。
x !== null
ではない。つまり、null
だけでなくundefined
を含む。
Rubyでの挙動
まず:
&.y x
これを事前に何もなしにやるとエラーになる。
x
はnil
であってもカバーされるが、NameError
は別問題だからだ。
= nil
x &.y x
であれば許される。
では;
= nil
x &.y.z x
これはNoMethodError
になる。x&.y
がnil
と解釈され、nil.z
になってしまうためだ。
つまり、x&.y
というの自体が、x
がnil
の場合はnil
が返るひとつの式になっている。
そもそもRubyでは.
の右辺はメソッド呼び出ししかなく、カッコがなくてもメソッド呼び出しである。
だからx&.y
あるいはx&.y()
は、「x
がnil
でないのならばy
メソッドを呼び出す」になる。
ちょっと特殊なものとして、Rubyでは.()
は.call()
の別名なので、
= Object.new
y def y.call
"CALLED"
end
&.() y
ということは可能。これは主にProc
オブジェクトで使われる。
結構困るのが添え字に対してぼっち演算子が効きにくいことだ。 例えば
= [1, 2, 3]
a &.[0] a
はSyntaxError
になる。
ただ、Rubyの添え字アクセスは[]
というメソッドなので、
= [1, 2, 3]
a &.[](0) a
とかいうちょっと気持ち悪い書き方はできるし、
= [1, 2, 3]
a &.[] 0 a
というもっと気持ち悪い書き方もできる。
添え字への代入も[]=
メソッドだから
= [1, 2, 3]
a &.[]=(3, 5) a
とかいう読み解くのが困難な書き方もできる。
a&.[0]
みたいな書き方ができないのは、Rubyでは割と痛い点。
JavaScriptでの挙動 ステップ1
JavaScriptですごく重要なこととして、JavaScriptのメソッドはUnbound methodであるということがある。 例えば次のコード
= {}
x .val = 100
x.foo = function() { console.log(this.val) }
x.foo()
x= {}
y .foo = x.foo
y.foo() y
x.foo
におけるthis
はx
を示し、this.val
は100
になる、というのはまぁ分かると思うのだが(これはアロー関数だとそうならない)、y.foo = x.yoo
にも関わらず、y.foo()
はundefined
になる。
つまり、foo
関数はx
に束縛されておらず、レシーバによって挙動が変わる。
この形でこの挙動に当たることはないだろうが、このために
const $i = document.getElementById
$i("FooElement")
みたいにgetElementById
の別名をつけようとして失敗することはありがち。
getElementById
はレシーバを必要とするから、レシーバが変わってしまうと機能しないのだ。
余談だが、この場合
const $i = (id) => document.getElementById(id)
とすれば機能する。
このため、JavaScriptにおけるx.y
は「レシーバとプロパティ」という構図であり、これを崩すことはできない。
PythonとかLuaに慣れている人なら不思議には感じないかもしれない。
さて、JavaScriptもノーコンテキストで
?.y x
とするとReferenceErrorになる。 同じ問題でありながらRubyがコンパイルエラーなのに対し、JavaScriptがランタイムエラーなのはちょっとおもしろい。
ただ、JavaScriptの場合undefined
もnull
扱いなので、undefined
が取れる状況であればOK。
let x
?.y x
で、ここからなのだが、Rubyではエラーになったさらなるチェインだが、JavaScriptではエラーにならない。
let x
?.y.z // undefined x
どういうことかというと、ここではわかりやすさのためにnull checkの代わりに論理評価を用いるが、Rubyの場合は
&& x.y).z (x
という解釈になるのに対して、JavaScriptだと
&& x.y.z x
という解釈になる。
JavaScriptでの挙動 ステップ2
さて、Rubyではそもそもドットの右辺はメソッド呼び出しだが、JavaScriptではプロパティへのアクセスである。 そのため、プロパティに対するアクセスというのが可能だ。
例えば、次のコードはx
のプロパティy
にアクセスし、それを関数として呼び出し、その戻り値のプロパティz
にアクセスする。
.y().z x
添え字アクセスというのもある。 添え字アクセスは結局のところプロパティへのアクセスなので、次のコードは一度もメソッドを呼び出さない。
.y["abc"].z x
Rubyでも条件次第で同じコードが書けるが、Rubyだと2回メソッドが呼び出される。
.y["abc"].z x
JavaScriptはプロパティのアクセスにOptional Chainingが使える。 添え字アクセスでも例外ではない。
?.["y"].z x
そして、「メソッドを呼び出す」という行為に対してもOptional Chainingが使える。
.y?.() x
そして、この挙動はJavaScriptの比較的深いところにある。
まず、関数オブジェクトは「呼び出されたときの自分のレシーバ」を覚えている。だから、
= function() { console.log(this) }
foo .foo = foo
x.foo = foo y
とした場合、実体はひとつのfoo()
であるにも関わらず、x.foo()
と呼び出すか、y.foo()
と呼び出すかで別の関数として振る舞う。
これは、一言で言えば「JavaScriptのthis
はふわっとしている」ということになるし、そもそもそれはDOM操作などイベントコールバックで使う上でthis
が意図したものとして振る舞うようにするにはunboundにする必要があるという意図的なものだ。
だが、unboundで、なおかつthis
という帰属概念があるということは、呼び出されたときのレシーバを知らなければならない。
だから、x.y()
とした場合、トークンとしてはx
,
.y
,
()
という3つになるのだが、y
は次のトークンである()
で呼び出されたときに、手前のトークンであるx
を知っている。
JavaScriptのOptional
Chainingはトークンの間に挟むことができるから、y
と()
の間に入れることができる。
そして、どのトークンに挟んだかに関係なく、左のトークンがnon-nullでなければ右のトークンへと進む。nullであればそこでチェーン全体を打ち切って左のトークンの値を返す。
これは、ドット形式のプロパティアクセスでも、添え字形式でのプロパティアクセスでも、関数呼び出しでも同じだ。 だから
?.()?.y x
とかも書ける。
ただし、関数呼び出しは前述のように、その関数自身がレシーバをコンテキストとして持っているので、その関数のレシーバまで絡んでくる。 感覚的には自然に、使いたいところで使えば動くのに対して、挙動としては案外複雑だ。
これが言語仕様として優れているかというのは宗派次第ではあるのだが、JavaScriptのほうが柔軟で使い心地がいい。 Rubyはシンプルな原則を守っているが、そのために添え字が犠牲になっているのでちょっと残念な感じがある。
おまけ
Rubyは一般的には演算子であるものもメソッドになっているので、かなり気持ち悪いぼっち演算子が書ける。
&.-(10) # x - 10
x&.**(15) # x ** 15
x&.==(20) # x == 20
x&.===(25) # x === 25
x&.+@ # +x
x&.|10&.&20 # x | 10 & 20
x&.^&.*&.+&.**&.<< x
最後のやつは右辺値のない式になるので式で書けない。
さらに、
= nil
x == nil x
はx
はnil
なのでtrue
だが、
= nil
x &.==nil x
はx&.method
自体がx
がnil
であればnil
を返すため、結果はnil
になる。