yecc/leex を使ってみる

yacc/lex のようなツールは、いろんなプログラミング環境向けにあります。Erlang にも yecc/leex と いうツールがありますので、今日はそれをちょっと使ってみるみたいな。それにしても e がくどいですね。

Step1: まずは構文解析

まずはHelloWorld 的な位置づけの、電卓を書いてみます。とりあえず構文のみ。

prs.yrl

Nonterminals
lines line expression term block.

Terminals
integer
'(' ')' '+' '-' '*' '/' '~n'.

Rootsymbol lines.

lines -> line.
lines -> lines line.

line -> expression '~n'.

expression -> term.
expression -> expression '+' term.
expression -> expression '-' term.

term -> block.
term -> term '*' block.
term -> term '/' block.

block -> '(' expression ')'.
block -> integer.

yecc の構文は yacc を踏襲しない独自なものになっています。押さえるべきは、

  • Token が {token-name, line-num} というタプルであることが期待されていること。(token-name は atom であればよい)。
  • Nonterminal で非終端要素、 Terminals で終端要素を教えてあげて、 RootSymbols でルートシンボルを指定する
  • あとは還元規則を書くだけ。

ビルドは、Erl からやる場合はこんな感じ。

1> y(prs).
{ok,"prs.erl"}
2> c(prs).
{ok, prs}

生成された prs.erl を見ればわかりますが、parse/1 という関数がつくられているので、これに入力を渡してあげます。まだ スキャナーが無いので、タブル直打ちで実行させてみましょう。

3> prs:parse([{integer, 1}, {'+',1}, {integer,1}, {'*',1}, {integer, 1}, {'~n',1}]).
{ok,'$undefined'} 

一応エラーにならずに実行されました。

Step2: 計算をさせてみる

それだけだと何にも意味がないので、処理をつけていく。トークンは、{トークン種別, 行数, …好きに使ってね…} という構成なので、3要素目にたとえば 数値を入れるとかして使う。

  • prs.yrl
Nonterminals
lines line expression term block.

Terminals
integer
'(' ')' '+' '-' '*' '/' '~n'.

Rootsymbol lines.


lines -> line.
lines -> lines line.

line -> expression '~n' : io:format("(ans) ~w~n", ['$1']).

expression -> term                : '$1'.
expression -> expression '+' term : '$1' + '$3'.
expression -> expression '-' term : '$1' + '$3'.

term -> block                     : '$1'.
term -> term '*' block            : '$1' * '$3'.
term -> term '/' block            : '$1' / '$3'.

block -> '(' expression ')'       : '$2'.
block -> integer                  : element(3, '$1').

構文木を構築せずにその場で計算してたり、一行毎に結果を標準出力に出したりしているなんともアレゲな感じですが、ゆるしてください。

意味値は '$1' で参照します。注意すべきは 1オリジン。ここいらへんは yacc と一緒です。

15> y(prs).                                                                    
{ok,"prs.erl"}
16> c(prs).                                                                    
{ok,prs}    
17> prs:parse([{integer, 1, 3}, {'+',1}, {integer,1,2}, {'*',1}, {integer, 1,5}, {'~n',1}]).
(ans) 13
{ok,'$undefined'}

Step3:Lexarを書く

leex をつかって 字句解析器を作ります。まぁ、意外性のない感じです。

  • scn.xrl
Definitions.

INT = [0-9]+
WHITESPACE = [\s\t]


Rules.

{INT}         : {token, {integer, TokenLine, list_to_integer(TokenChars) }}.
\+            : {token, {'+', TokenLine}}.
\-            : {token, {'-', TokenLine}}.
\*            : {token, {'*', TokenLine}}.
\/            : {token, {'/', TokenLine}}.
\(            : {token, {'(', TokenLine}}.
\)            : {token, {')', TokenLine}}.
\n            : {token, {'~n', TokenLine}}.
{WHITESPACE}+ : skip_token.

Erlang code.

TokenLine でそのToken のいる行数がとれ、skip_token で飛ばしたいものを指定します*1

Lexerのコードを作るには leex:file/1 を使います。 yecc みたいにy(). 的な erl 上の短縮名はないみたい。

40> leex:file('scn.xrl').
{ok,"./scn.erl"}
41> c(scn).   
{ok,scn}
42> scn:string("42+2 * 3").
{ok,[{integer,1,42},
     {'+',1},
     {integer,1,2},
     {'*',1},
     {integer,1,3}],
    1}
43> 

せっかくなので、さっき作ったパーザーとくっつけてみます。srn:string/1 で文字列から字句解析。

86> prs:parse(element(2, scn:string("(1+1)\n"))).
(ans) 2
{ok,'$undefined'}

ファイルから読み込むのはイカのような感じ。

  • calc.txt
1 + 1
3 + 4 * 5
(6 + 7) * 8

file:read_file/1 で読み込んだ内容を binary_to_list/1 で文字列化して testscn:string/1 に食べさせる。

60> test:parse(element(2, testscn:string(binary_to_list(element(2, file:read_file('calc.txt')))))).
(ans) 2
(ans) 23
(ans) 104
{ok,'$undefined'}

Step4: 曖昧性と衝突の解決

Reduce/Recude 衝突 と Shift/Reduce 衝突

LR(1) の構文解析の仕組みは、いっぱい本がでていますので、ホントはそちらを読むのが良いかと思いますが、とりあえず「どんな規則をかけばよいのか」「どんな規則を書いたらだめなのか」を説明します。

  • 生成されるパーザーは、トークンを順番に喰っていき(これをシフト(Shift)という)、喰ったトークン列が 還元規則の左側にマッチするとき 右側に還元(Reduce)される
  • 左辺側に還元(Reduce)するときには、ただ一つの可能性しか内容に書く。そうしないと Reduce/Reduce Conflict が起こる
  • シフト(Shift)すべきか 還元(Reduce)すべきかわからなくなるときも Shift/Reduce Conflict が起こる

もうすこし補足すると LR(1) の 1 は、「一つ先読み」という意味なので、2番目3番目のやつは「一つ先読みしても/一つ先読みしたら」どっちをやればいいのかわからなくなるということ。

Reduce/Reduce衝突はたとえばこれ

  • err1.yrl
Nonterminals target block.
Terminals a b c.
Rootsymbol target.

target -> a b c.
target -> block.

block -> a b c.
10> y(err1).
err1.yrl: Parse action conflict scanning symbol '$end' in state 5:
   Reduce to target from a b c (rule 1 at line 5)
      vs.
   reduce to block from a b c (rule 3 at line 8).
err1.yrl: Warning: conflicts: 0 shift/reduce, 1 reduce/reduce
error

a b c と食べた時、どちらの還元規則を適用すればいいか、パーザーにはわかりません。これはエラーになります。

Shift/Reduce 衝突はこんな感じ。

  • err2.yrl
Nonterminals target block.
Terminals a b c d.
Rootsymbol target.

target -> a b c d.
target -> block d.

block -> a b c.
12> y(err2).
err2.yrl: Warning: conflicts: 1 shift/reduce, 0 reduce/reduce
{ok,"err2.erl"}

a b c と来た場合、その後 d をシフトすべきか、 a b c をblock に還元すべきかわかりません。これは、警告になります。

shift/reduce 衝突はよ 中置演算での数式と、ぶら下がり else が典型的です。たとえば以下の様な規則において、"a + b - c" という入力があったとき a + b の次のアクションとして、"a + b" を式に還元すべきか、- c を シフトすべきかわかりません。

  • err3.yrl
Nonterminals exp.
Terminals integer '+' '-'.
Rootsymbol exp.

exp -> integer         : element(3, '$1').
exp -> exp '+' exp     : {'+', '$1', '$3'}.
exp -> exp '-' exp     : {'-', '$1', '$3'}.
26> y(err3).
err3.yrl: Warning: conflicts: 4 shift/reduce, 0 reduce/reduce
{ok,"err3.erl"}

ちなみにこの警告を無視するとどうなるかというと、右結合として処理されます。

28> err3:parse([{integer,0,"1"}, {'+',0}, {integer,0,"2"}, {'-',0}, {integer,0,"3"}]).  
{ok,{'+',"1",{'-',"2","3"}}}

これを避けるため、Step1 では term という 非終端要素を用意して、二項演算子の右辺と左辺を別の要素として定義しているわけですね。(さらに、* と / の優先度を実現するために block という要素も作って3段構えになっています)

結合規則と優先順の指定(優先順位規則)

本質的には上記のように逃げる(というより構文としてちゃんと定義してあげる)のが正しいのですが、それは結構めんどくさいことも多いので、Yacc のようなパーザジェネレータは トークンに対してどちらに結合するのかと、その優先順位を指定することが出来るオプションが設けられていることが多いです。もちろん yecc にもありますので、それを使ってみます。

  • prs.yrl
Nonterminals
lines line expression uminus.

Terminals
integer
float
'(' ')' '+' '-' '*' '/' '~n'.

Left  300 '+' '-'.
Left  400 '*' '/'.
Unary 500 uminus.

Rootsymbol lines.

lines -> line           : ['$1'].
lines -> lines line     : '$1' ++ ['$2'].

line -> expression      : io:format("(ans1) ~w~n", ['$1']), '$1'.
line -> expression '~n' : io:format("(ans2) ~w~n", ['$1']), '$1'.

expression -> expression '+' expression : '$1' + '$3'.
expression -> expression '-' expression : '$1' - '$3'.
expression -> expression '*' expression : '$1' * '$3'.
expression -> expression '/' expression : '$1' / '$3'.
expression -> '(' expression ')'        : '$2'.
expression -> integer                   : element(3, '$1').
expression -> float                     : element(3, '$1').
expression -> uminus                    : '$1'.

uminus -> '-' expression                : io:format("(minus~w)", ['$2']),  -1 * '$2'.
  • scn.xrl
Definitions.

INT = [0-9]+
WHITESPACE = [\s\t]


Rules.
{INT}\.{INT}  : {token, {float,   TokenLine, list_to_float(TokenChars) }}.
{INT}         : {token, {integer, TokenLine, list_to_integer(TokenChars) }}.
\+            : {token, {'+', TokenLine}}.
\-            : {token, {'-', TokenLine}}.
\*            : {token, {'*', TokenLine}}.
\/            : {token, {'/', TokenLine}}.
\(            : {token, {'(', TokenLine}}.
\)            : {token, {')', TokenLine}}.
\n            : {token, {'~n', TokenLine}}.
{WHITESPACE}+ : skip_token.

Erlang code.
  • calc.erl
-module(calc).
-export([from_string/1, from_file/1]).

from_string( String ) ->
    {ok, Tokens, _} = scn:string(String),
    {ok, AnsList}   = prs:parse(Tokens),
    AnsList.

from_file( FileName )->
    {ok, F} = file:read_file(FileName),
    from_string(binary_to_list(F)).
解説
Left  300 '+' '-'.
Left  400 '*' '/'.
Unary 500 uminus.

の部分が結合規則になります。どのように結合するか(あるいはしないか)を Left Right Nonassoc で指定します。その次の数値は優先順位になります。数字の大きい方が優先順位が高いです。

Left と Right の働きは以下を見るとわかりやすいかな。

  • test1.yrl
Nonterminals exp.
Terminals integer '+' '-'.
Rootsymbol exp.

Left 100 '+' '-'.

exp -> integer         : element(3, '$1').
exp -> exp '+' exp     : {'+', '$1', '$3'}.
exp -> exp '-' exp     : {'-', '$1', '$3'}.
  • test2.yrl
Nonterminals exp.
Terminals integer '+' '-'.
Rootsymbol exp.

Right 100 '+' '-'.

exp -> integer         : element(3, '$1').
exp -> exp '+' exp     : {'+', '$1', '$3'}.
exp -> exp '-' exp     : {'-', '$1', '$3'}.
3> test1:parse([{integer,0,"1"}, {'+',0}, {integer,0,"2"}, {'-',0}, {integer,0,"3"}]).
{ok,{'-',{'+',"1","2"},"3"}}
6> test2:parse([{integer,0,"1"}, {'+',0}, {integer,0,"2"}, {'-',0}, {integer,0,"3"}]).
{ok,{'+',"1",{'-',"2","3"}}}

ちなみに

  • test3.yrl
Nonterminals exp.
Terminals integer '+' '-'.
Rootsymbol exp.

Nonassoc 100 '+' '-'.

exp -> integer         : element(3, '$1').
exp -> exp '+' exp     : {'+', '$1', '$3'}.
exp -> exp '-' exp     : {'-', '$1', '$3'}.

にすると、

9> test3:parse([{integer,0,"1"}, {'+',0}, {integer,0,"2"}, {'-',0}, {integer,0,"3"}]).
{error,{0,test3,["syntax error before: ","'-'"]}}

とエラーになります。これは例えば C言語で、 "A < B < C" がエラーになるのを期待するようなケースですね。



よく単行演算の '-' を ゴニョゴニョするために使われる Yacc の %prec に相当するものはなさそうで、そのかわりUnarry を使うようです。 %prec は他にもいろいろできちゃうので、よくないな、と廃止されたんじゃないかな、とか思ったり。

もし、

Left  300 '+' '-'.
Left  400 '*' '/'.
Unary 100 uminus.

のようにして パーザーを生成させると

83> calc:from_string("-3+2").       
(minus6)(ans1) -5
[-5]

となり-(3+2)となるのがわかります。

まとめ

優先順位規則の適用は、シンプルに shift/reduce衝突を解決でき、すっきりとした文法記述を実現するスマートな手段に思えます。しかし、それが効果的に振る舞う状況は限られており、その実態は goto 文のように厄介な極まりない難読文法を作ってしまいがちです。

lex & yacc プログラミング の 3.5.1 より引用すると、

文法中で発生するすべてのシフト/還元衝突は優先順位規則を使用することで解消することが出来る。しかしながら、この考え方はけっして頭のいい方法ではない。数式文法中の衝突の原因は簡単に理解できるので、優先順位規則がもたらす効果も明白であった。ほかの場合でも、優先順位規則はシフト/還元衝突の問題を解消してくれるが、優先順位規則が文法にもたらす影響というものは、一般には判断しづらい。
優先順位を使うのは次の二つの場合にとどめるように忠告しておく。数式文法中と if-then-else形式の文法中での「懸垂 else (dangling else)」を解消する場合である。これ以外の場合には、できるならば、文法を修正して衝突を解消したほうがいい。

なんてことが書いてあります。ゴールデンハンマーしないように気をつけたいところです。

lex&yaccプログラミング (NUTSHELL HANDBOOKS)

lex&yaccプログラミング (NUTSHELL HANDBOOKS)

原著第二版の翻訳です。1994年の古い本です(オライリー・ジャパンがまだ無い頃なので ASCIIから出ている、というくらい古い)が、決定版とでも言うべき内容になっています。でも、そろそろ flex & bison を翻訳してくれてもいいとおもうの。

flex & bison: Text Processing Tools

flex & bison: Text Processing Tools

*1:サンプルとかだと、Erlang code. の節はなくても良いっぽい感じだったのだけれども、自分でやってみたら「ないよ!」と怒られるのでつけてみたら解決した

オライリーの Erlangプログラミングの wxErlang のチュートリアルを動かしてみたり

やぁっと環境ができましたので、ちょっと作ってみようかと。お手軽なチュートリアルとして手元にあった

Erlangプログラミング

Erlangプログラミング

の 14章をちょちょいと実装。

%% -*- coding: utf-8 -*-
%% オライリーの Erlang プログラミングの 14.4 章のプログラム
%%
%% microblog (14.3) に以下の機能が追加されます
%%
%% |New         | 新しく空のミニブログを生成します
%% |Open        | BLOGファイルに保存されたブログを開きます
%% |Save        | 現状のブログをBLOGファイルに保存します。コンテンツがすでに存在する場合、上書きします
%% |Add entry   | ブログの末尾にエントリを追加します。 エントリには自動で日付が付与されます
%% |Undo latest | 最新のAdd entry を取り消します。この操作は再帰的に行うことができます

-module(miniblog).
-compile(export_all).

-include_lib("/usr/local/lib/erlang/lib/wx-1.6/include/wx.hrl").
% code:lib_dir(wx) ++ "/include/wx.hrl" になります

-define(APPEND, 131).
-define(UNDO  , 132).
-define(OPEN  , 133).
-define(SAVE  , 134).
-define(NEW   , 135).
-define(ABOUT , ?wxID_ABOUT).
-define(EXIT  , ?wxID_EXIT).

start() ->
    wx:new(),
    Frame = wxFrame:new(wx:null(), ?wxID_ANY, "MiniBlog"),
    Text  = wxTextCtrl:new(Frame, ?wxID_ANY,
                           [{value, "MiniBlog"},
                            {style, ?wxTE_MULTILINE}]),
    setup(Frame, Text),
    wxFrame:show(Frame),
    loop(Frame, Text),
    wx:destroy().
    
setup(Frame, Text) ->
    %% elaborate menu bar
    MenuBar = wxMenuBar:new(),
    File    = wxMenu:new(),
    Edit    = wxMenu:new(),
    Help    = wxMenu:new(),

    wxMenu:append(File, ?NEW   , "New\tCtrl-N"),
    wxMenu:append(File, ?OPEN  , "Open saved\tCtrl-O"),
    wxMenu:appendSeparator(File),
    wxMenu:append(File, ?SAVE  , "Save\tCtrl-S"),
    wxMenu:append(File, ?EXIT  , "Quit"),

    wxMenu:append(Edit, ?APPEND, "Add en&try\tCtrl-T"),
    wxMenu:append(Edit, ?UNDO  , "Undo latest\tCtrl-U"),

    wxMenu:append(Help, ?ABOUT , "About MicroBlog"),

    wxMenuBar:append(MenuBar, File, "&File"),
    wxMenuBar:append(MenuBar, Edit, "&Edit"),
    wxMenuBar:append(MenuBar, Help, "&Help"),

    wxFrame:setMenuBar(Frame,MenuBar),

    %% elaborate TextEdit
    wxTextCtrl:setEditable(Text, false),

    %% elaborate status bar
    wxFrame:createStatusBar(Frame),
    wxFrame:setStatusText(Frame, "Welcome to wxErlang"),

    wxFrame:connect(Frame, command_menu_selected),
    wxFrame:connect(Frame, close_window).

loop(Frame, Text) ->
    receive
        #wx{id=?APPEND, event=#wxCommand{type=command_menu_selected}} ->
            Prompt = "Please enter text here.",
            MsgDlg = wxTextEntryDialog:new(Frame, Prompt,
                                           [{caption, "New blog entry"}]),
            case wxTextEntryDialog:showModal(MsgDlg) of
                ?wxID_OK ->
                    Str = wxTextEntryDialog:getValue(MsgDlg),
                    wxTextCtrl:appendText(Text, [10] ++ dateNow() ++ " " ++ Str); %10 as LF(0x0a)
                _ -> ok
            end,
            wxDialog:destroy(MsgDlg),
            loop(Frame, Text);

        #wx{id=?UNDO  , event=#wxCommand{type=command_menu_selected}} ->
            {StartPos, EndPos} = lastLineRange(Text),
            wxTextCtrl:remove(Text, StartPos-2, EndPos+1),
            loop(Frame, Text);

        #wx{id=?OPEN  , event=#wxCommand{type=command_menu_selected}} ->
            wxTextCtrl:loadFile(Text, "BLOG"),
            loop(Frame, Text);

        #wx{id=?SAVE  , event=#wxCommand{type=command_menu_selected}} ->
            wxTextCtrl:saveFile(Text, [{file,"BLOG"}]),
            loop(Frame, Text);

        #wx{id=?NEW   , event=#wxCommand{type=command_menu_selected}} ->
            {_, EndPos} = lastLineRange(Text),
            StartPos    = wxTextCtrl:xYToPosition(Text, 0, 0),
            wxTextCtrl:replace(Text, StartPos, EndPos, "MiniBlog"),
            loop(Frame, Text);

        #wx{id=?ABOUT , event=#wxCommand{}} ->
            Str = "MiniBlog is minimal WxErlang example.",
            MsgDlg = wxMessageDialog:new(Frame, Str,
                                         [{style, ?wxOK bor ?wxICON_INFORMATION},
                                          {caption, "About MiniBlog"}]),
            wxDialog:showModal(MsgDlg),
            wxDialog:destroy(MsgDlg),
            loop(Frame, Text);

        #wx{id=?EXIT  , event=#wxCommand{type=command_menu_selected}} ->
            wxWindow:close(Frame,[])
    end.

dateNow() ->
    {{Yea,Mon,Day},{Hou,Min,Sec}} = erlang:localtime(),
    io_lib:format("~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B", [Yea,Mon,Day,Hou,Min,Sec]).

lastLineRange(Text) ->
    EndPos = wxTextCtrl:getLastPosition(Text),
    LastLineNum = wxTextCtrl:getNumberOfLines(Text) -1,
    StartPos = EndPos - wxTextCtrl:getLineLength(Text, LastLineNum) +1,
    {StartPos, EndPos}.
1> c(miniblog).     
{ok,miniblog}
2> miniblog:start().

まぁ、こんな感じかな。

wxErlang で面白いのは、wxWidgets のイベントを Erlang のメッセージとして プロセスのメールボックスから取得できるところですね。wx:new/0 で wxサーバーを立てて、そのままのプロセスでloopでメッセージを待ち、抜けたらwx:destroy/0 wxサーバーにお亡くなりいただく..というのが大まかな構成になるわけです。

ここらへんは、普通のGUIフレームワークがUIスレッド上での処理は簡単だけれども、UIが固まるのが嫌だからと、ちょっと非同期にしようとすると途端にプログラミングが面倒になるのとは逆をいっているのかな?と思います。多分。

この14章のチュートリアル自信は about とquitしかできない microblog からスタートして、ここに載せた miniblog の作成までと成長させるという、学びやすい構成になっています。一部関数がしれっと定義しないまま使っていて、それぐらい自分で作れるだろう?的なところも、学習にはいい感じでした。


* * *


そんなこんなで、まずはお手つきと思ったのですが、環境の構築のほうに労力を取られてしまいました。wxErlang にもうすこし慣れれば日常のGUIツールをErlangでさくっとつくる、なんてできるかな、とか思わなくもないです。

が、激しく目的外使用というか、手段のために目的を選んでいるような...(^^;

wxErlang が動きませんよ?

(要約) Chromebook Flip (ARM cortex-A17 環境) 上の crouton Ubuntu 14.04 LTS で wxErlang を動かそうとしたら、最終的にソースコードからのビルドになった、という話。 


* * *


今年の豊富ついでに早速 wxErlang でもやってみようと思いました。幸い Chromebook Flip に構築した Ubuntu 環境でも apt-get で erlang はすんなり入ったので、さくっと始められますよ。

というわけで、まずは試し。いちばん小さそうなサンプルをざざっとググって見つけた1章 「wxErlang」 - INAの日記 さんを参考にサンプルを書いて動かそうとしたところ、

-module(guitest).
-compile(export_all).

start() ->
    Wx = wx:new(),
    Frame = wxFrame:new(Wx, -1, "this is the title", [{size, {600, 800}}]),
    wxFrame:show(Frame),
    loop(Frame),
    wx:destroy().

loop(Frame) ->
    receive
        _ -> loop(Frame)
    end.
2> c(guitest).
{ok,guitest}
3> guitest:start().
** exception error: undefined function wx:new/0
     in function  guitest:start/0 (guitest.erl, line 5)

むっふぅ...。wx:new/0 がございませんか...。

Stack Overflow を見ると include 足りてねぇんじゃね、的な指摘があったので

6> code:lib_dir(wx).
"/usr/lib/erlang/lib/wx-1.1.1"

で、パスを調べて

-include_lib("/usr/lib/erlang/lib/wx-1.1.1/include/wx.hrl").

を追加*1

で、実行すると

14> guitest:start().
** exception error: undefined function wx:new/0
     in function  guitest:start/0 (guitest.erl, line 6)

・・・むっふぅ、変わらにゅ。なにがおかしいのかしら。

Erlang のライブラリインポートの仕組みをイマイチ理解していないのだけれど、とりあえず解決すればいいや、的に、たとえば Getting Started with wxWidgets in Erlang を参考に、erlang-shell 側でincludeしてみるものの、

4> My_wx_dir = code:lib_dir(wx).
"/usr/lib/erlang/lib/wx-1.1.1"
5> rr(My_wx_dir ++ "/include/wx.hrl").
[wx,wxAuiManager,wxAuiNotebook,wxCalendar,wxChildFocus,
 wxClipboardText,wxClose,wxColourPicker,wxCommand,
 wxContextMenu,wxDate,wxDisplayChanged,wxErase,
 wxFileDirPicker,wxFocus,wxFontPicker,wxGrid,wxHelp,
 wxHtmlLink,wxHtmlLinkInfo,wxIconize,wxIdle,wxJoystick,wxKey,
 wxList,wxMaximize,wxMenu,wxMouse,wxMouseCaptureChanged|...]
6> rr(My_wx_dir ++ "/src/wxe.hrl").
[wx_env,wx_mem,wx_ref]

しかし、やはりうまくいきません。

おかしいなー、

$ cd /usr/lib/erlang/lib/wx-1.1.1/src
$ ll
合計 100
drwxr-xr-x 3 root root  4096 105 00:46 ./
drwxr-xr-x 5 root root  4096 105 00:46 ../
drwxr-xr-x 2 root root 16384 105 00:46 gen/
-rw-r--r-- 1 root root 12091  820  2014 wx.erl
-rw-r--r-- 1 root root 21525  820  2014 wx_object.erl
-rw-r--r-- 1 root root  2180  820  2014 wxe.hrl
-rw-r--r-- 1 root root  8661  820  2014 wxe_master.erl
-rw-r--r-- 1 root root 14153  820  2014 wxe_server.erl
-rw-r--r-- 1 root root  6972  820  2014 wxe_util.erl

をすると wx.erl といるし、その中にちゃんと

-module(wx).

-export([parent_class/1, new/0, new/1, destroy/0,
	 get_env/0,set_env/1, debug/1,
	 batch/1,foreach/2,map/2,foldl/3,foldr/3,
	 getObjectType/1, typeCast/2,
	 null/0, is_null/1]).
  ・
  ・
  ・

とかあるのだけれどなぁ...と、調べつつ、ふと「あれ、これコンパイルされてるのかしら」とErlang クエックブック Beam ファイルのパスを取得する を参考に code:which/1 で探索すると

11> code:which(wx).                
non_existing

いないじゃーーーーん ...orz

なんかここらへんが原因っぽいなぁ。

自前ビルドする

これを後から何とかする自信が全然なかったので、やっぱり何もかも綺麗な環境からやり直したほうが良いでしょう、ということで、

$sudo aptitude purge erlang

して、http://erlang-users.jp/ の「GitHub レポジトリからビルドする」や
Erlang/OTP 18.1 をソースコードからインストールする | Qiitaを参考に自前ビルド。

現状のさいしんは 18.2.1 なので

$ git clone git://github.com/erlang/otp --bare erlang.otp.upstream
$ git clone erlang.otp.upstream opt.18.2
$ cd opt.18.2/
$ git checkout -b 18.2.1  OTP-18.2.1Switched to a new branch '18.2.1'

とソースと作業ディレクトリを確保。

で、ビルドに必要なライブラリをインストール

$ sudo aptitude install build-essential libncurses5-dev libssl-dev systemtap-sdt-dev autoconf

あとは、こちらも*2

$ sudo aptitude install libwxgtk3.0-dev libwxgtk3.0-0 libwxgtk3.0-0-dbg wx3.0-i18n libwxgtk-media3.0-dev libwxgtk-media3.0-0 libwxgtk-media3.0-0-dbg

で、wx関連も入れる(入れたつもり)。その後いつものconfigure

$ ./otp_build autoconf 2>&1 | tee ../erl_autoconf.log
$ ./configure --disable-hipe --enable-dtrace --without-javac 2>&1 | tee ../erl_configure.log

で、メッセージを見てみると

   ・
   ・
   ・
*********************************************************************
**********************  APPLICATIONS DISABLED  **********************
*********************************************************************

jinterface     : Java compiler disabled by user
odbc           : ODBC library - link check failed

*********************************************************************
*********************************************************************
**********************  APPLICATIONS INFORMATION  *******************
*********************************************************************

wx             : Can not link the wx driver, wx will NOT be useable

*********************************************************************
*********************************************************************
**********************  DOCUMENTATION INFORMATION  ******************
*********************************************************************

documentation  : 
                 xsltproc is missing.
                 fop is missing.
                 xmllint is missing.
                 The documentation can not be built.

*********************************************************************

あうち。Java や ODBC は置いといて、問題は wx と documentation ね。特にwxはこれが使いたくって自前ビルドに走ったのですから。で、メッセージをみると、

configure: Checking for OpenGL headers in /usr/local
checking GL/gl.h usability... no
checking GL/gl.h presence... no
checking for GL/gl.h... no
configure: WARNING: No OpenGL headers found, wx will NOT be usable

と言っています。OpenGL が必要なんかー。というわけで、

$ sudo aptitude install freeglut3-dev libglew1.5-dev

も追加。したら、これで wx周りは解決しました。

あとはdocumentation の部分を解決するため

$ sudo aptitude install xsltproc fop libxml2-utils

*3。なんか、やたらたくさん java 関連のパッケージが入ってストレージに乏しいChromebook ではヒヤヒヤ。MicroUSB上にchroot環境作っておけばよかったなぁ、とプチ後悔。

そして再び confiure して、

$ make 2>&1 | tee ../erl_make.log
$ make install 2>&1 | tee ../erl_install.log

ちなみに make にかなーーーり時間がかかります。特に、wxWidgetsのビルドが長い(configureのときstatic library が見つからなかったので自前ビルドになる)です。

終わったら、早速 起動してみます。

$ erl
Erlang/OTP 18 [erts-7.2.1] [source] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V7.2.1  (abort with ^G)
1> 

ちなみに、apt-get でいれた ときはこんな感じでした。

$ erl
Erlang R16B03 (erts-5.10.4) [source] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)

では、成果の程を見てみましょう。どきどき..

2> code:which(wx).
"/usr/local/lib/erlang/lib/wx-1.6/ebin/wx.beam"

はふんっ、長かった!

で、サンプルを動かしてみれば、

はふー、やったよーー。(長かった....)

まぁ、タイトルバーのバッテンボタンを拾えてなかったりあやしいところもあるのですが*4、そこはおいおい。

それにしても、ARM というマイノリティ環境だと、自前ビルドは本当にうまく行くのかドキドキしますね*5。configure のオプションとかおっかなびっくりでした。

おまけ: Emacserlang-mode 設定

ErlangEmacs びいきなので、普通にインストールするだけで、erlang-modeが追加されてます。なので、.emacs などに 記述を追加すれば OK です。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; erlang-mode
(setq load-path (cons "/usr/local/lib/erlang/lib/tools-2.8.2/emacs"
      load-path))
(setq erlang-root-dir "/usr/local/otp")
(setq exec-path (cons "/usr/local/otp/bin" exec-path))

(require 'erlang-start)

ちなみに、load-path のところの tools-x.x.x の部分は環境に合わせて変更してくださいまし。

*1:これはすごくダサいとおもうのですが、どうすればいいのでしょうかね...。せめて -include_lib(code:dir(wx) ++ "/include/wx.hrl") とか書けるとよいのですが

*2:実は最初は入れ忘れてて、configure のメッセージを見てから追加しました

*3:libxml2-util は xmllint のためですね

*4:追記: wxFrame:connect(Frame, close_window) し忘れてるだけですね(^^;

*5:そんなとき勇気をくれるのがArch Linux のページだったり。このページがあるというだけで、大丈夫という気がしてきます