前回は四則演算を行う簡単な電卓プログラムを作りました。今回は電卓プログラムに変数と関数の機能を追加してみましょう。
前回作成した電卓は、計算結果を表示したあとそれを保持していないので、計算結果を再利用することができません。一般の電卓のように、計算結果を記憶しておくメモリ機能があると便利です。この機能を「変数 (variable)」として実現することにします。プログラミング言語で言えば、大域変数 (グローバル変数) と同じ機能になります。
変数を実装するのであれば、変数に値を代入する操作が必要になります。文法に「文」を定義する、つまり「代入文」を追加する方法もありますが、今回は簡単な電卓プログラムなので、代入演算子 "=" を用意して式の中で処理することにしましょう。代入演算子は右辺の式の値を左辺の変数に代入するので、文法は次のように表すことができます。
[EBNF] 式 = 代入式 | 式1. 代入式 = 変数, "=", 式. 式1 = 項, { ("+" | "-"), 項 }. 項 = 因子, { ("*" | "/"), 因子 }. 因子 = 数値 | ("+" | "-"), 因子 | "(", 式, ")" | 変数. 変数 = 識別子 [注意] 数値と識別子の定義は省略
演算子 = は他の演算子と違って右結合になることに注意してください。このため、他の演算子よりも優先順位を低くし、右辺の式の評価を優先して行います。そして、その結果を変数にセットします。文法では、式を 代入式 | 式1 に変更し、代入式で演算子 = の処理を行います。式1は今までの式の定義と同じです。これで演算子 = の優先順位を低くすることができます。あとは代入式の処理で、右辺の式を先に評価して、その結果を変数にセットすればいいわけです。
それから、因子に「変数」を追加します。変数の定義は「識別子」とし、識別子は先頭文字がアルファベットで、それ以降の文字はアルファベットだけではなく数字 (0 - 9) を含んでいてもかまいません。Scheme 入門で作成したプログラムと違って、今回のプログラムは構文木を組み立ててからそれを評価するので、構文解析の段階では変数をそのまま返すだけで OK です。
次は文法に関数を追加しましょう。関数の処理は「因子」に追加します。
[EBNF] 式 = 代入式 | 式1. 代入式 = 変数, "=", 式. 式1 = 項, { ("+" | "-"), 項 }. 項 = 因子, { ("*" | "/"), 因子 }. 因子 = 数値 | ("+" | "-"), 因子 | "(", 式, ")" | 変数 | 関数, "(", 引数リスト, ")". 変数 = 識別子 関数 = 識別子 引数リスト = 式, { ",", 式 }. [注意] 数値と識別子の定義は省略
関数の名前は識別子とし、そのあとに引数をカッコで囲んで渡します。カッコの中は「引数リスト」として定義します。これは「式」をカンマで区切って並べたもので、一般的な手続き型言語の関数呼び出しと同じ形式になります。
ただし、変数と関数は同じ識別子なので、このままでは区別することができません。この場合、簡単な方法が 2 つあります。ひとつは関数として登録されている識別子を関数とする方法、もうひとつは次のトークンが左カッコ (lpar) であれば関数とする方法です。今回は前者の方法を採用することにしましょう。
それではプログラムを作ります。最初に、変数と関数を表すデータ型を定義します。次のリストを見てください。
リスト : データ型の定義 (* 演算子の定義 *) datatype operator = Add | Sub | Mul | Quo | Assign (* 関数の定義 *) datatype func = F1 of value -> value | F2 of (value * value) -> value (* トークンの定義 *) datatype token = Number of value (* 数 *) | Ident of string (* 識別子 *) | Oper of operator (* 演算子 *) | Lpar | Rpar (* (, ) *) | Semic (* ; *) | Comma (* , *) | Quit (* 終了 *) | Others (* その他 *) (* 式の定義 *) datatype expr = Num of value (* 数値 *) | Var of string (* 変数 *) | Op1 of operator * expr (* 単項演算子 *) | Op2 of operator * expr * expr (* 二項演算子 *) | App of func * expr list (* 関数の適用 *)
operator に代入演算子 = を表す Assign を追加します。関数を表すデータ型が func です。引数が 1 つの関数を F1 で、引数が 2 つの関数を F2 で表します。token には識別子を表す Ident とカンマ ( , ) を表す Comma を追加します。expr には変数を表す Var と関数呼び出しを表す App を追加します。関数の引数はリスト (expr list) に格納します。
次は、変数と関数を格納する大域変数とアクセス関数を定義します。
リスト : 関数と変数のアクセス関数 (* 変数を格納する配列 *) val global_env : (string * value ref) list ref = ref [] (* 探索 *) fun lookup(name) = let fun iter [] = NONE | iter ((x as (n, _))::xs) = if n = name then SOME x else iter xs in iter(!global_env) end (* 更新 *) fun update(name, value) = case lookup(name) of NONE => global_env := (name, ref value) :: (!global_env) | SOME (_, v) => v:= value (* value を real に変換 *) fun toReal(Float(v)) = v | toReal(Integer(v)) = Real.fromLargeInt(v) (* 関数を呼び出す *) fun call_real_func1 f v = Float(f(toReal v)) fun call_real_func2 f (v, w) = Float(f(toReal v, toReal w)) (* 関数を格納する配列 *) val func_table= [("sqrt", F1 (call_real_func1 Math.sqrt)), ("sin", F1 (call_real_func1 Math.sin)), ("cos", F1 (call_real_func1 Math.cos)), ("tan", F1 (call_real_func1 Math.tan)), ("asin", F1 (call_real_func1 Math.asin)), ("acos", F1 (call_real_func1 Math.acos)), ("atan", F1 (call_real_func1 Math.atan)), ("atan2", F2 (call_real_func2 Math.atan2)), ("exp", F1 (call_real_func1 Math.exp)), ("pow", F2 (call_real_func2 Math.pow)), ("ln", F1 (call_real_func1 Math.ln)), ("log10", F1 (call_real_func1 Math.log10)), ("sinh", F1 (call_real_func1 Math.sinh)), ("cosh", F1 (call_real_func1 Math.cosh)), ("tanh", F1 (call_real_func1 Math.tanh))] (* 関数の探索 *) fun lookup_function(name) = let fun iter([]) = NONE | iter((n, f)::xs) = if n = name then SOME f else iter(xs) in iter(func_table) end
val lookup = fn : string -> (string * value ref) option val update = fn : string * value -> unit val toReal = fn : value -> real val call_real_func1 = fn : (real -> real) -> value -> value val call_real_func2 = fn : (real * real -> real) -> value * value -> value val lookup_function = fn : string -> func option
変数は大域変数 global_env に連想リストの形式で格納します。値を書き換えるので、要素のデータ型は string * value ref とします。関数 lookup は global_env から名前が name の変数を探索します。見つかった場合はタプルを option に格納して返します。見つからない場合は NONE を返します。関数 update は変数の値を更新します。変数 name が既に存在する場合は、タプルの第 2 要素 v を value に書き換えます。name が存在しない場合は、(name, ref value) を global_env に追加します。
組み込み関数は大域変数 func_table に連想リストの形式で格納します。関数 call_real_func1 と call_real_func2 は、型が real -> real, (real * real) -> real の関数を呼び出すために使います。引数 v, w を関数 toReal で real に変換して、引数に渡された関数 f を呼び出します。結果は Float に格納して返します。関数 lookup_function は名前が name の組み込み関数があるか func_table から探します。
次は関数 get_token を修正します。
リスト : 字句解析の修正 (* 識別子の切り出し *) fun get_ident(s) = let fun iter a = if Char.isAlphaNum(valOf(lookahead(s))) then iter ((valOf(input1(s))) :: a) else Ident(implode(rev a)) in iter [] end (* トークンの切り出し *) fun get_token(s) = let val c = valOf(lookahead s) in if Char.isSpace(c) then (input1(s); get_token(s)) else if Char.isDigit(c) then get_number(s) else if Char.isAlpha(c) then let val (id as Ident(name)) = get_ident(s) in if name = "quit" then tokenBuff := Quit else tokenBuff := id end else ( input1(s); (* s から c を取り除く *) tokenBuff := (case c of #"+" => Oper(Add) | #"-" => Oper(Sub) | #"*" => Oper(Mul) | #"/" => Oper(Quo) | #"=" => Oper(Assign) | #"(" => Lpar | #")" => Rpar | #";" => Semic | #"," => Comma | _ => Others ) ) end
val get_ident = fn : instream -> token val get_token = fn : instream -> unit
記号がアルファベットの場合は関数 get_ident で識別子を切り分けます。get_ident はアルファベットと数字を累積変数 a に格納し、それを implode で文字列に変換します。取り出した文字列が quit の場合、電卓の終了を表すトークン Quit を tokenBuff にセットします。そうでなければ Ident をそのまま tokenBuff にセットします。あとは、代入演算子 = とカンマ "," が入力された場合、それを表すトークン Oper(Assign) と Comma を tokenBuff にセットするだけです。
次は構文解析を修正します。まず最初に、代入演算子の処理を expression に追加します。次のリストを見てください。
リスト : expression の修正 fun expression(s) = let fun iter v = case !tokenBuff of Oper(Assign) => ( case v of Var(_) => (get_token(s); Op2(Assign, v, expression(s))) | _ => raise Syntax_error("invalid assign form") ) | _ => v in iter(expr1(s)) end and expr1(s) = let fun iter v = case !tokenBuff of Oper(Add) => (get_token(s); iter(Op2(Add, v, term(s)))) | Oper(Sub) => (get_token(s); iter(Op2(Sub, v, term(s)))) | _ => v in iter (term(s)) end and term(s) = let fun iter v = case !tokenBuff of Oper(Mul) => (get_token(s); iter(Op2(Mul, v, factor(s)))) | Oper(Quo) => (get_token(s); iter(Op2(Quo, v, factor(s)))) | _ => v in iter (factor(s)) end
val expression = fn : instream -> expr val expr1 = fn : instream -> expr val term = fn : instream -> expr
演算子 +, - の処理は関数 expr1 で行い、演算子 = の処理を expression で行います。expression は最初に expr1 を評価して、その返り値を局所変数 iter の引数 v に渡します。tokenBuff が Assign の場合は代入式の処理を行います。v の値をチェックして、変数を表す Var でなければエラーを送出します。そして、expression を呼び出して右辺の式を評価して、その返り値を Op2 にセットします。expr1 は今までの expression と同じです。
次は関数 factor を修正します。
リスト : 因子の修正 and factor(s) = case !tokenBuff of Lpar => ( get_token(s); let val v = expression(s) in case !tokenBuff of Rpar => (get_token(s); v) | _ => raise Syntax_error("')' expected") end ) | Number(n) => (get_token(s); Num(n)) | Quit => raise Calc_exit | Oper(Sub) => (get_token(s); Op1(Sub, factor(s))) | Oper(Add) => (get_token(s); Op1(Add, factor(s))) | Ident(name) => ( get_token(s); case lookup_function(name) of NONE => Var(name) | SOME f => let val args = get_argument(s) in case f of F1 _ => if length(args) < 1 then raise Syntax_error("not enough args") else () | F2_ => if length(args) < 2 then raise Syntax_error("not enough args") else (); App(f, args) end ) | _ => raise Syntax_error("unexpected token") (* 実引数の取得 *) and get_argument(s) = let fun iter a = let val v = expression(s) in case !tokenBuff of Rpar => (get_token(s); rev(v::a)) | Comma => (get_token(s); iter(v::a)) | _ => raise Syntax_error("unexpected token in argument list") end in case !tokenBuff of Lpar => (get_token(s); iter([])) | _ => raise Syntax_error("'(' expected") end
val factor = fn : instream -> expr val get_argument = fn : instream -> expr list
tokenBuff が Ident の場合、変数または関数呼び出しの処理を行います。最初に lookup_function を呼び出し、識別子 name が組み込み関数かチェックします。そうであれば、組み込み関数を呼び出す App を生成します。get_token を呼び出して次のトークンを求め、関数 get_argument で引数を取り出します。あとは引数の数をチェックして、App(f, args) を返します。関数でなければ変数なので Var(name) を返します。
get_argument はカンマで区切られた式を expression で評価し、それをリストに格納して返します。expression を評価したあと、case で tokenBuff をチェックします。右カッコ (Rpar) であれば、引数 v を累積変数 a に追加して、rev で反転して返します。カンマ (Comma) であれば、まだ引数があるので次の式を評価します。そうでなければ、式に誤りがあるのでエラーを送出します。
次は式を評価する関数 eval_expr を修正します。
リスト : 式の計算 fun eval_expr(Num(n)) = n | eval_expr(Var(name)) = ( case lookup(name) of NONE => raise Calc_run_error("unbound variable") | SOME (_, ref v) => v ) | eval_expr(Op2(op2, expr1, expr2)) = let val w = eval_expr(expr2) in case op2 of Add => eval_op(op +, op +, eval_expr(expr1), w) | Sub => eval_op(op -, op -, eval_expr(expr1), w) | Mul => eval_op(op *, op *, eval_expr(expr1), w) | Quo => eval_op(op div, op /, eval_expr(expr1), w) | Assign => case expr1 of Var(name) => (update(name, w); w) | _ => raise Calc_run_error("Illegal assign form") end | eval_expr(Op1(op1, expr1)) = let val v = eval_expr(expr1) in case (op1, v) of (Add, _) => v | (Sub, Integer(n)) => Integer(~n) | (Sub, Float(n)) => Float(~n) | _ => raise Syntax_error("Illegal expression") end | eval_expr(App(f, args)) = let val vs = map (fn e => eval_expr(e)) args in case f of F1 f1 => f1(hd vs) | F2 f2 => f2(hd vs, hd (tl vs)) end
val eval_expr = fn : expr -> value
最初に、変数 Var(name) の値を求める処理を追加します。lookup で global_env から name の変数を求めます。見つからない場合、その変数は未束縛なのでエラー Calc_run_error を送出します。
次に、二項演算子 Op2 の処理に Assign を追加します。式 expr1 から変数名 name を取り出し、関数 update で name の値を式 expr2 の値 w に変更します。expr1 が変数でない場合はエラーを送出します。このエラーが発生する場合は構文解析でバグがあることになります。
最後に、関数を呼び出す App の処理を追加します。最初に args を eval_expr で評価して引数の値を求め、その値を関数に渡して呼び出すだけです。
あとの修正は簡単なので説明は割愛します。詳細はプログラムリストをお読みください。
それでは実行してみましょう。
Calc> a = 10; 10 Calc> a; 10 Calc> a * 10; 100 Calc> (b = 20) * 10; 200 Calc> b; 20 Calc> x = y = z = 0; 0 Calc> x; 0 Calc> y; 0 Calc> z; 0 Calc> p = p + 1; ERROR: unbound variable Calc> q = 1; 1 Calc> q; 1 Calc> q = q + 1; 2 Calc> q; 2
変数に値を代入すると、その値を使って式を評価することができます。また、式の中に演算子 = が入っていても、その式を評価することができます。x = y = z = 0; のように、多重代入することも可能です。ただし、新しい変数 p で p = p + 1; のようなことはできません。q = 1; を評価したあとならば、既に変数 q は定義されているので、q = q + 1; は評価することができます。
次は組み込み関数を実行してみましょう。
Calc> sqrt(2); 1.41421356237 Calc> pow(2, 32); 4294967296.0 Calc> pi = asin(0.5) * 6; 3.14159265359 Calc> sin(0); 0.0 Calc> sin(pi/2); 1.0 Calc> sin(pi); ~8.881784197E~16
正常に動作していますね。
今回はここまでです。次回はユーザが関数を定義する機能を追加してみましょう。
(* * calc.sml : 電卓プログラム * * Copyright (C) 2012-2021 Makoto Hiroi * * (1) 四則演算の実装 * (2) 変数と組み込み関数の追加 * *) open TextIO (* 例外 *) exception Calc_exit exception Syntax_error of string exception Calc_run_error of string (* 値の定義 *) datatype value = Integer of IntInf.int | Float of real (* 演算子の定義 *) datatype operator = Add | Sub | Mul | Quo | Assign (* 組み込み関数の定義 *) datatype func = F1 of value -> value | F2 of (value * value) -> value (* トークンの定義 *) datatype token = Number of value (* 数 *) | Ident of string (* 識別子 *) | Oper of operator (* 演算子 *) | Lpar | Rpar (* (, ) *) | Semic (* ; *) | Comma (* , *) | Quit (* 終了 *) | Others (* その他 *) (* 式の定義 *) datatype expr = Num of value (* 数値 *) | Var of string (* 変数 *) | Op1 of operator * expr (* 単項演算子 *) | Op2 of operator * expr * expr (* 二項演算子 *) | App of func * expr list (* 関数の適用 *) (* グローバル変数を格納する配列 *) val global_env : (string * value ref) list ref = ref [] (* 探索 *) fun lookup(name) = let fun iter [] = NONE | iter ((x as (n, _))::xs) = if n = name then SOME x else iter xs in iter(!global_env) end (* 更新 *) fun update(name, value) = case lookup(name) of NONE => global_env := (name, ref value) :: (!global_env) | SOME (_, v) => v:= value (* value を real に変換 *) fun toReal(Float(v)) = v | toReal(Integer(v)) = Real.fromLargeInt(v) (* 関数を呼び出す *) fun call_real_func1 f v = Float(f(toReal v)) fun call_real_func2 f (v, w) = Float(f(toReal v, toReal w)) (* 関数を格納する配列 *) val func_table= [("sqrt", F1 (call_real_func1 Math.sqrt)), ("sin", F1 (call_real_func1 Math.sin)), ("cos", F1 (call_real_func1 Math.cos)), ("tan", F1 (call_real_func1 Math.tan)), ("asin", F1 (call_real_func1 Math.asin)), ("acos", F1 (call_real_func1 Math.acos)), ("atan", F1 (call_real_func1 Math.atan)), ("atan2", F2 (call_real_func2 Math.atan2)), ("exp", F1 (call_real_func1 Math.exp)), ("pow", F2 (call_real_func2 Math.pow)), ("ln", F1 (call_real_func1 Math.ln)), ("log10", F1 (call_real_func1 Math.log10)), ("sinh", F1 (call_real_func1 Math.sinh)), ("cosh", F1 (call_real_func1 Math.cosh)), ("tanh", F1 (call_real_func1 Math.tanh))] (* 関数の探索 *) fun lookup_function(name) = let fun iter([]) = NONE | iter((n, f)::xs) = if n = name then SOME f else iter(xs) in iter(func_table) end (* 切り出したトークンを格納するバッファ *) val tokenBuff = ref Others (* 整数の切り出し *) fun get_number(s) = let val buff = ref [] fun get_numeric() = let val c = valOf(lookahead s) in if Char.isDigit(c) then ( buff := valOf(input1(s)) :: (!buff); get_numeric() ) else () end fun check_float(c) = case c of #"." => true | #"e" => true | #"E" => true | _ => false in get_numeric(); (* 整数部の取得 *) if check_float(valOf(lookahead s)) then ( if valOf(lookahead s) = #"." then ( (* 小数部の取得 *) buff := valOf(input1(s)) :: (!buff); get_numeric() ) else (); if Char.toUpper(valOf(lookahead s)) = #"E" then ( (* 指数形式 *) buff := valOf(input1(s)) :: (!buff); let val c = valOf(lookahead s) in if c = #"+" orelse c = #"-" then buff := (valOf(input1(s))) :: (!buff) else () end; get_numeric() ) else (); tokenBuff := Number(Float(valOf(Real.fromString(implode(rev (!buff)))))) ) else tokenBuff := Number(Integer(valOf(IntInf.fromString(implode(rev (!buff)))))) end (* 識別子の切り出し *) fun get_ident(s) = let fun iter a = if Char.isAlphaNum(valOf(lookahead(s))) then iter ((valOf(input1(s))) :: a) else Ident(implode(rev a)) in iter [] end (* トークンの切り出し *) fun get_token(s) = let val c = valOf(lookahead s) in if Char.isSpace(c) then (input1(s); get_token(s)) else if Char.isDigit(c) then get_number(s) else if Char.isAlpha(c) then let val (id as Ident(name)) = get_ident(s) in if name = "quit" then tokenBuff := Quit else tokenBuff := id end else ( input1(s); (* s から c を取り除く *) tokenBuff := (case c of #"+" => Oper(Add) | #"-" => Oper(Sub) | #"*" => Oper(Mul) | #"/" => Oper(Quo) | #"=" => Oper(Assign) | #"(" => Lpar | #")" => Rpar | #";" => Semic | #"," => Comma | _ => Others ) ) end (* 構文木の組み立て *) fun expression(s) = let fun iter v = case !tokenBuff of Oper(Assign) => ( case v of Var(_) => (get_token(s); Op2(Assign, v, expression(s))) | _ => raise Syntax_error("invalid assign form") ) | _ => v in iter(expr1(s)) end and expr1(s) = let fun iter v = case !tokenBuff of Oper(Add) => (get_token(s); iter(Op2(Add, v, term(s)))) | Oper(Sub) => (get_token(s); iter(Op2(Sub, v, term(s)))) | _ => v in iter (term(s)) end and term(s) = let fun iter v = case !tokenBuff of Oper(Mul) => (get_token(s); iter(Op2(Mul, v, factor(s)))) | Oper(Quo) => (get_token(s); iter(Op2(Quo, v, factor(s)))) | _ => v in iter (factor(s)) end and factor(s) = case !tokenBuff of Lpar => ( get_token(s); let val v = expression(s) in case !tokenBuff of Rpar => (get_token(s); v) | _ => raise Syntax_error("')' expected") end ) | Number(n) => (get_token(s); Num(n)) | Quit => raise Calc_exit | Oper(Sub) => (get_token(s); Op1(Sub, factor(s))) | Oper(Add) => (get_token(s); Op1(Add, factor(s))) | Ident(name) => ( get_token(s); case lookup_function(name) of NONE => Var(name) | SOME f => let val args = get_argument(s) in case f of F1 _ => if length(args) < 1 then raise Syntax_error("not enough args") else () | F2_ => if length(args) < 2 then raise Syntax_error("not enough args") else (); App(f, args) end ) | _ => raise Syntax_error("unexpected token") (* 実引数の取得 *) and get_argument(s) = let fun iter a = let val v = expression(s) in case !tokenBuff of Rpar => (get_token(s); rev(v::a)) | Comma => (get_token(s); iter(v::a)) | _ => raise Syntax_error("unexpected token in argument list") end in case !tokenBuff of Lpar => (get_token(s); iter([])) | _ => raise Syntax_error("'(' expected") end (* 式の計算 *) fun eval_op(op1, op2, v, w) = case (v, w) of (Integer(n), Integer(m)) => Integer(op1(n, m)) | (Integer(n), Float(m)) => Float(op2(Real.fromLargeInt(n), m)) | (Float(n), Integer(m)) => Float(op2(n, Real.fromLargeInt(m))) | (Float(n), Float(m)) => Float(op2(n, m)) fun eval_expr(Num(n)) = n | eval_expr(Var(name)) = ( case lookup(name) of NONE => raise Calc_run_error("unbound variable") | SOME (_, ref v) => v ) | eval_expr(Op2(op2, expr1, expr2)) = let val w = eval_expr(expr2) in case op2 of Add => eval_op(op +, op +, eval_expr(expr1), w) | Sub => eval_op(op -, op -, eval_expr(expr1), w) | Mul => eval_op(op *, op *, eval_expr(expr1), w) | Quo => eval_op(op div, op /, eval_expr(expr1), w) | Assign => case expr1 of Var(name) => (update(name, w); w) | _ => raise Calc_run_error("Illegal assign form") end | eval_expr(Op1(op1, expr1)) = let val v = eval_expr(expr1) in case (op1, v) of (Add, _) => v | (Sub, Integer(n)) => Integer(~n) | (Sub, Float(n)) => Float(~n) | _ => raise Syntax_error("Illegal expression") end | eval_expr(App(f, args)) = let val vs = map (fn e => eval_expr(e)) args in case f of F1 f1 => f1(hd vs) | F2 f2 => f2(hd vs, hd (tl vs)) end (* 実行 *) fun toplevel() = ( print "Calc> "; flushOut(stdOut); get_token(stdIn); let val result = expression(stdIn) in case !tokenBuff of Semic => () | Quit => raise Calc_exit | _ => raise Syntax_error("unexpected token"); case eval_expr(result) of Integer(n) => print(IntInf.toString(n) ^ "\n") | Float(n) => print(Real.toString(n) ^ "\n") end ) fun calc() = while true do ( toplevel() handle Syntax_error(mes) => (inputLine(stdIn); print("ERROR: " ^ mes ^ "\n")) | Calc_run_error(mes) => (inputLine(stdIn); print("ERROR: " ^ mes ^ "\n")) | Div => (inputLine(stdIn); print("ERROR: divide by zero\n")) | err => raise err )