3. 入力された数式を解釈する

(2019.12.31. 公開)

 本プログラムの動作としては、以下のものを想定しています。

 テキストの入力は現時点でも可能です。今回は、「更新ボタンのクリックへの応答」、そして「数式の解釈」について考えてみます。

3-1. ボタンのクリックへの応答

 wxWidgets では、ユーザーの動作はすべて「イベント」としてプログラムに知らされます。プログラム側は、イベントをつかまえて、必要な動作を実行すればよいことになります。

 イベントは、基本的には「そのイベントが発生したウィンドウ」に送られます(wxWidgets では、ボタンやテキストなど、画面上の部品も含めてすべて「ウィンドウ」と呼びます)。ボタンをクリックした場合は、そのボタンにイベントが送られます。ボタンに「イベントを処理するプログラムコード」を登録しておけば、イベントが発生した時にそのコードを実行してくれます。

 イベントを処理するプログラムコードのことを「イベントハンドラ」と呼び、「関数」の形で登録します。「関数」とは、Lua では function ... end で定義されるものです。例として、ボタンがクリックされた時の処理を OnClick という関数に記述したとしましょう。この関数は、下のようになります。

function OnClick(event)  --  必ず event という引数を持つ
  -- 中身はあとで考える
end

 この関数=イベントハンドラをボタンに登録するには、Connect() を使います。Connect()wxEvtHandler クラスのメソッドです。ボタンを含むすべてのウィンドウは wxEvtHandler のサブクラスなので、Connect() を使うことができます。

  frame.button:Connect(wx.wxEVT_BUTTON, OnClick)
-- wx.wxEVT_BUTTON は「ボタンがクリックされたイベント」を表す

 Connect() の第1引数には、イベントの種類を表す定数を置きます。この定数を探すためには、イベントを発生する GUI 部品のクラス、この場合は wxButton のドキュメントを開きます。すると、以下のような記述が見つかるでしょう。

Event macros for events emitted by this class:

 EVT_BUTTON(id, func) と書いてあるところは、C++ でプログラムを作成するときの話なので、ここでは無視して構いません。その次の、"Process a wxEVT_BUTTON event, when the button is clicked." の記述に注目します。この記述から、ボタンがクリックされた時には wxEVT_BUTTON というイベントが発生することがわかります。このイベントを捕まえてください、というのが、上の Connect() 関数の第1引数の意味です。

 Connect() の第2引数は、関数名です。これを OnClick() と書いてはいけません。カッコの有無で、意味が変わってしまいます。OnClick は「OnClick という関数そのもの」、OnClick() は「OnClick という関数を呼び出した結果」を表します。Connect() で必要なのは、「関数を呼び出した結果」ではなくて「関数そのもの」ですから、カッコなしの OnClick を使います。

 これで、「更新」ボタンを押すと、関数 OnClick が呼び出されるようになりました。次に OnClick の中身を記述しますが、その前に、入力された数式をどう解釈するかを考えておきましょう。

3-2. 入力された数式の解釈

 下のように、y = x^3 という数式を入力して、「更新」ボタンを押したとします。OnClick が呼び出されますが、そこではどういう処理をするべきでしょうか。

 まず、「この数式はそもそもどう使われるものなのか」を考えます。このプログラムで実現したいのは、「ここに入力した数式をグラフ化して表示する」ことです。数式は y = (x の関数) という形で記述されるものとします。すると、グラフを書く時に必要なのは、x の値を変化させて、それぞれに対する y の値をこの式を使って計算することでしょう。

 いろいろな方法が考えられますが、ここでは、入力された数式から、下のような「Lua の関数」を作ることにしましょう。

 入力された文字列の前に function (x) \n local y を置き、文字列の後に return y \n end を置きます(\n は改行)。こうすると、入力された文字列の中の x は関数の仮引数、y はローカル変数として解釈され、「与えられた x の値に対して y を計算し、その値を返す」という関数ができあがります。

 この「Lua 関数を表す文字列」を Lua プログラムとして読み込むためには、Lua の組み込み関数である loadstring() を使います。loadstring("文字列") は、文字列を Lua プログラムとして解釈して、問題なければそのプログラムの内容を関数として返し、問題があれば nil とエラーメッセージを返します。次のように使います。

  f, errmsg = loadstring(str)
  if f then
    f()  --  str の内容を実行する
  else
    --  errmsg を表示する
  end

 一つ注意があります。実は、loadstring() の仕様である「関数として返す」という動作が、慣れないとわかりにくいのです。上の図に示した「Lua の関数」を見てみてください。function (x) \n local y \n y = x^3 \n return y \n end というのは、確かに「Lua の関数」です。ところが、loadstring() にこの文字列を直接渡すと、以下のように苦情を言われます。

% f, errmsg = loadstring("function (x) \n local y \n y = x^3 \n return y \n end")
% print(f); print(errmsg)
nil
[string "function (x) ..."]:1: '<name>' expected near '('

 '(' の近くに '<name>' が必要だ、と言っております。どういうことかと言うと、loadstring() に渡す文字列は、Lua の「文」(または「文」の並び)でないといけないのです。

 Lua の文法では、「文」と「式」は明確に区別されます。上の「関数」を少し簡潔にして、次の例を見てみましょう。

function (x) return x^3 end  -- 「関数」を返す式
func = function (x) return x^3 end  --  'func' という名前の「関数」を定義する文
function func (x) return x^3 end  -- 上と全く同じ(シンタックスシュガー)

 loadstring() に渡す文字列は「文」でないといけないので、1行目の文字列を渡すとエラーになります。2行目・3行目の文字列ならば、正しく解釈できます。

 それでは、「loadstring() が関数を返す」という Lua マニュアルの説明は、何を意味しているのでしょうか。その答えは、「loadstring(str) が返した『関数』を実行すると、文字列 str が表す『文』を実行したことになる」ということです。つまり、「文字列 str の内容を Lua の文として実行したい」場合は、次の手順を踏めばよいわけです。

 まとめると、loadstring() を使って、上の例のように「func という名前の関数」を定義するには、下のように書けばよいことになります。

  f, errmsg = loadstring("func = function (x) return x^3 end")
  if f then
    f()   -- ここで「loadstring() が返した『関数』」を実行する
          -- つまり、関数 func が定義される
  else
    --  errmsg を表示する
  end

 このプログラムでは、数式を表す関数を frame(メインウィンドウを表すものでしたね)のテーブルフィールドとして保存することにします。そうすると、ボタンが押された時の処理は、下のようになります。

  local eq = frame.textctrl:GetValue()  --  文字列を textctrl から得る
  local str = "frame.func = function (x) local y\n" .. eq .. "\nreturn y; end"
    --  関数定義のための文字列を作成する
  local f, errmsg = loadstring(str)  --  解釈する
  if f then
    f()  -- frame.func が関数として定義される
    --  画面を更新する
  else
    --  エラーメッセージを表示する
  end

3-3. エラーメッセージの表示

 エラーが出た時には、ユーザーに知らせなくてはなりません。ここではシンプルに、loadstring() が返したエラーメッセージを画面にそのまま表示してみます。「数式」という文字列と、「更新」ボタンの間に、エラーメッセージを表示する wxStaticText を1つ追加します。SetForegroundColour で、表示色を赤色にしましょう。(Colour の綴りがイギリス英語なので、注意してください。)

  frame.stext = wx.wxStaticText(frame, -1, "数式")
  frame.etext = wx.wxStaticText(frame, -1, "")
  frame.etext:SetForegroundColour(wx.wxRED)
  frame.button = wx.wxButton(frame, -1, "更新")
  -- (中略) --
  local hsizer1 = wx.wxBoxSizer(wx.wxHORIZONTAL)
  hsizer1:Add(frame.stext, wx.wxSizerFlags():Border(wx.wxLEFT + wx.wxRIGHT, 5))
    -- wxSizerFlags() の引数 "1" が無くなっているのに注意。
    -- つまり、「数式」という文字列の幅は一定。
    -- その代わり、次のエラーメッセージの文字列を横幅いっぱいに広げる。
  hsizer1:Add(frame.etext, wx.wxSizerFlags(1):Border(wx.wxLEFT + wx.wxRIGHT, 10))
  hsizer1:Add(frame.button, wx.wxSizerFlags():Border(wx.wxRIGHT, 5))

 これを使って、OnClick を下のように書くことができます。

function OnClick(event)
  local eq = frame.textctrl:GetValue()
  local str = "frame.func = function (x) local y\n" .. eq .. "\nreturn y; end"
  local f, errmsg = loadstring(str)
  if f then
    f()
    frame.etext:SetLabel("")
  else
    frame.etext:SetLabel(errmsg)
  end
end

 これで、「更新」ボタンを押すと、数式の解釈を行うようになります。グラフ表示はまだ実装していないので、何も起きませんけど。

 エラーがあれば知らせてくれます。といっても、不親切ですけどね。ユーザーから見れば「"return" なんか使ってないぞ?」と思いますよね。あとで改良することにしましょう。

 本章のプログラム: [graphcalc03.wx.lua]

目次