本編ではトークンの種類はRubyの基本的なデータ型に対応させる(いわば手抜きの)アプローチをとり、演算子を(Rubyの)シンボルで表すことにしていた。
この電卓が機能的に発展してプログラミング言語になり、トークンの種類が多様になると、
function Token(type,value){
this.type=type
this.value=value
return this
}
のような関数を作ってデータの再設計を行う必要があるだろうが、
ここでは、Symbol(JSにもある)を使って演算子をトークン化しておく。
数値は(まずは文字列としてマッチされる)はJSの数値に変換し、計算自体はJSの機能を借用することにしておく。
数値の代わりに、被演算子の位置に名標(後に変数として扱う予定)も書いていいことにしておき、それに対応するようスキャナを改良する。名標は文字列のままにしておくことにする(本編と同じ方針)。
関数の引数として渡す。各関数は作業結果を値として返すことになる。
console.log(val(parse(scan(s))))
メソッドチェーン
s.scan().parse().val().print()
引数として関数を渡す。
function scan(s,parse,val,print) {
// s を字句解析し、結果を配列 a として生成したのち
parse(a,val,print)
}
function parse(tokens,val,print) {
// tokens を構文解析し、結果を tree として生成し、
val(tree,print)
}
function val(tree, print) {
// r = 計算結果 (tree から求める)
print(r)
}
scan(s,parse,val,r=>console.log(r))
function Node(op,l,r) {
this.op=op; this.l=l; this.r=r;
} // この関数をオブジェクトのコンストラクタとして(new演算子で)呼ぶ
木構造全体を表すためのデータ型は特に必要なく、木の根(root)に位置するNode を示すことで木全体のデータを示すことができる。
前述の、全体の流れで言うと、関数(メソッド)parse() が Node を生成し、 Node のメソッドとして val() を定義し、val() が計算結果を (おそらく Number型として)生成することになる。
function Node(op,l,r) {
this.op=op; this.l=l; this.r=r; // ここは上記と同じ
this.val=function(){ ... } // ここでメソッド定義ができる
}
あるいは、上記コンストラクター定義の外でもメソッドは書ける。
Node.prototype.val=function(){ ... }
// ちなみに、
Node.prototype.val=()=>{ ... } // ダブルアローを使うと、
// この書き方はNG(this がグローバルオブジェクトを指すことになるので)
=>
の動作(thisの扱い)の差異が効いてくる箇所。 JSの、ありがちな落し穴の1つなので確認しておこう。本編で扱ったように、パターンの種類を増やしておく(文字列と、実数)。
また、演算子(もしくは演算子ではないがトークンとしては演算子と同等に扱うもの)の種類も、次の章での改良を想定して若干増やしておく。具体的には ‘(’、‘)’ (括弧)と、‘=’ (イコール)。
トークンの種類によって、しかるべきデータ型に変換したものをトークン列(配列)として返すことにしたい(これについてはあとであらためて論じるが)。
ただし、Rubyのcase式に比べて、それに(形式上は)相当するJS(やCやJava)の switch文は機能的に不充分で、 RubyでもJSでも 比較を ===
演算子で行っているという点では同じなのだが、 JSの ===
では正規表現のマッチングやデータの型の比較を扱えない。
仕方ないのでJSでは、昔ながらの if ~ elseif による構文を使うことになる。
String.prototype.scan=function() {
return this.match(/[-+\/*%()=]|\d+(?:\.\d+)?/g).
map(e=>{
if(e.match(/\d+\.\d+/)) return parseFloat(e) // 実数
else if(e.match(/\d+/)) return parseInt(e) // 整数
else if(e.match(/[-+\/*%()=]/)) return sym(e) // 演算子
else return e // おそらく文字列(変数として扱う)
})
}
Rubyとは違い、JSでは ifの構文は「if文」であり、値を返さない(Rubyでは「if式」だったが)。 そのため、分枝ごとに return を書く冗長っぽい表現になる。
また、if文をreturnの対象にすることができないため、 アローの右側のブレースも省略できない。
こうして作っ に保持されていてたトークン列の、それぞれのトークンの種類は、typeof
演算子の値で判定できるように作った。ただし、
演算子について、これも typeof
演算子の値で判別できるよう、 演算子はSymbol型に変換して保持しておくことにした。 文字列 str をSymbolに変換するのは Symbol(str)
でいいが、実は、
Ruby言語での Symbol は、同値(同一かどうかは不確定)の Stringから生成されたSymbolは同一だと判定できるよう調整される(Lisp言語の intern と同等の動作)のに対し、
JavaScriptでは Symbolはまったく逆の目的で作られていて、生成されたSymbolが互いに(同じ文字列から生成されたとしても)同一にならないよう作られている。
この差異を吸収し、同じ演算子(から変換されたSymbol)同士が同一だと判定されるよう、 以下のコードを用意して、変換は関数 sym() (下のリストではアロー式で定義した)で 行うことにする。
const symbols={}
const sym=str=>symbols[str]||(symbols[str]=Symbol(str))
// ↑2回目以降 ↑1回目(生成し、代入し、その値を関数の値とする)
*1つの文字列から一旦生成されたSymbolはオブジェクト symbols に保持されていて 2回目以降は Symbol()
によって変換するのではなく、 既出のものを再利用する、という仕組み。
まずは、優先度を考慮しない(前から順の)構文解析のプログラム例から。
Array.prototype.parse=function() {
var a=[] // stack
var tokens=[...this] // トークンリストのコピー(念のため)
a.push(tokens.shift())
let t
while(t=tokens.shift()) {
// console.log(t, a)
switch(typeof t) {
case 'symbol': a.push(t) ; break
case 'number': case 'string':
a.push(new Node(a.pop(),a.pop(),t)) ; break
}
}
return(a[0]) // スタックに1つ残ったNodeオブジェクト を返す
}
{ }
は、この log出力の行がなければ省略可能(繰返しの対象が switch文1つなので)。Node.prototype.val=function(){
function val1(ope,r,l){ // 内部でのみ使える関数、計算を実行する
// console.log("val1",ope,r,l)
switch(ope){
case sym('+'): return r+l
case sym('-'): return r-l
case sym('*'): return r*l
case sym('/'): return r/l
case sym('%'): return r%l
default: console.log(`unknown operator ${ope.toString()}`);
return 0 // もしここに引っかかったらエラーとして検出できるよう
}
}
function val0(n){ // 内部用再帰関数
// console.log(n)
switch(typeof n) {
case 'string': return 0 // 今は仮にこうしておく
case 'number': return n // 値をそのまま返す
default: // then 'this' is Node object
var op,l,r ; ({op,l,r}=n)
console.log('~val1:',op,l,r)
return val1(op,val0(l),val0(r)) // 引数に再帰呼出を使っている
}
}
return val0(this)
}
({op,l,r}=this)
の部分は、Nodeオブジェクト(構造体のようなもの、 this で参照している)の、それぞれの属性の値を取り出して、同名の変数に代入する、 完結な書き方(分割代入の一種)。