つまり逆ポーランド記法をウェブ上で入力して,サーバー側で計算してから,答えを返すという実用性ゼロのプログラムを作りたいと思います.しかも中途半端な画面で申し訳ないです.要はErlangのCowboyでアプリケーションサーバーを作る練習をしたいです.これを作る途中に私にとっては一番ややこしいのはErlangのBinary, List, Integerの間の変換でした.
前提知識としてまず,
-
Erlangで逆ポーランド記法の書き方について下のリンクで解説されています.
http://www.ymotongpoo.com/works/lyse-ja/ja/10_functionally_solving_problems.html
-
そして前回の投稿を目を通していただきたいです.今回はこのソースをベースに作られているものです.
今回はEchoサーバーを逆ポーランド記法を計算してくれるサーバーに変更させます.
大いに変更したのはsrc/toppage_handler.erl
ファイルです.
$ cat toppage_handler.erl
%% Feel free to use, reuse and abuse the code in this file.
-module(toppage_handler).
-export([init/2]).
init(Req, Opts) ->
Method = cowboy_req:method(Req),
HasBody = cowboy_req:has_body(Req),
Req2 = maybe_echo(Method, HasBody, Req),
{ok, Req2, Opts}.
maybe_echo(<<"POST">>, true, Req) ->
{ok, PostVals, Req2} = cowboy_req:body_qs(Req),
Echo = proplists:get_value(<<"echo">>, PostVals),
Result = do_rpn(Echo),
echo(Result, Req2);
maybe_echo(<<"POST">>, false, Req) ->
cowboy_req:reply(400, [], <<"Missing body.">>, Req);
maybe_echo(_, _, Req) ->
%% Method not allowed.
cowboy_req:reply(405, Req).
echo(undefined, Req) ->
cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req);
echo(Echo, Req) ->
cowboy_req:reply(200, [
{<<"content-type">>, <<"text/plain; charset=utf-8">>}
], Echo, Req).
do_rpn(B) when is_binary(B) ->
L = binary_to_list(B),
[Res] = lists:foldl(fun do_rpn/2, [], string:tokens(L, " ")),
case is_float(Res) of
true ->
list_to_binary(float_to_list(Res, [{decimals, 2}]));
false ->
list_to_binary(integer_to_list(Res))
end.
read(N) ->
case string:to_float(N) of
{error, no_float} ->
list_to_integer(N);
{F, _} ->
F
end.
do_rpn("+", [N1, N2|S]) -> [N2 + N1|S];
do_rpn("-", [N1, N2|S]) -> [N2 - N1|S];
do_rpn("*", [N1, N2|S]) -> [N2 * N1|S];
do_rpn("/", [N1, N2|S]) -> [N2 / N1|S];
do_rpn("^", [N1, N2|S]) -> [math:pow(N2, N1)|S];
do_rpn("ln", [N|S]) -> [math:log(N)|S];
do_rpn("log10", [N|S]) -> [math:log10(N)|S];
% sumの場合も同じで、戻り値はfoldlの二個目の引数を書き換える
% 要はアキュミュレータは足されていく
do_rpn("sum", Stack) -> [lists:sum(Stack)];
do_rpn(X, Stack) -> [read(X) | Stack].
do_rpn/1関数を使って,逆ポーランド式を計算して,結果を返します.
ここで気をつけるところはCowboyがユーザー入力の内容をbinary stringとして,取得して
Echo = proplists:get_value(<<"echo">>, PostVals),
do_rpn/1関数が一旦そのbinaryをリストにして,計算するようにしています.
L = binary_to_list(B),
そして計算した結果をもう一回binaryに戻して,クライアントに送ります.
list_to_binary(float_to_list(Res, [{decimals, 2}]));
index.htmlファイルも少し修正しましたが,大したこと書いてません.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<!-- <link rel="stylesheet" href="styles.css"> -->
<style>
div {
width:300px;
height:300px;
border-style: solid;
border-width: 1px;
margin: 100px auto;
box-shadow: 10px 10px 5px #888888;
}
form {
text-align: center;
margin: 100px auto;
}
</style>
</head>
<body>
<div >
<form method="post" action="/process" class="form-horizontal" role="form">
<input type="text" name="echo">
<input type="submit" value="submit">
</form>
</div>
</body>
</html>
これでコンパイルしてからアプリケーションを立ち上げます.index.htmlを修正するたびにコンパイル必要みたいです.
$ make
$ _rel/cowboy_post_release/bin/cowboy_post_release console
上のコードをベースにしてテンプレートエンジンerlydtlを使いたいと思います。
最終の画面のは下のようになります。
背景知識としてまずはerlydtlを読んでもらいたいです。
最初に修正するファイルはcowboy_post/Makefileで
PROJECT = cowboy_post
DEPS = cowboy erlydtl merl
dep_cowboy = git https://github.com/ninenines/cowboy master
dep_erlydtl = git https://github.com/erlydtl/erlydtl master
dep_merl = git https://github.com/richcarl/merl master
include erlang.mk
そして cowboy_post/src/cowboy_post.app.srcファイル
{application, cowboy_post, [
{description, ""},
{vsn, "0.1.0"},
{id, "git"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
cowboy,
erlydtl,
merl
]},
{mod, {cowboy_post_app, []}},
{env, []}
]}.
念のため,merlを追加しましたけど、多分なくても動くはずです。
そのあと、cowboy_postディレクトリ下にtemplatesディレクトリを作成、そのディレクトリの中にecho.dtlファイルの作成します。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</head>
<style>
div {
width:300px;
height:300px;
border-style: solid;
border-width: 1px;
margin: 100px auto;
box-shadow: 10px 10px 5px #888888;
}
form {
text-align: center;
margin-top: 100px;
margin-left: 25px;
}
p {
margin-left : 45px;
}
</style>
<body>
<div>
<form method="post" action="/process" class="form-horizontal" role="form">
<input type="text" name="echo">
<input type="submit" value="mysubmit">
</form>
<p> {{ answer }} </p>
</div>
</body>
</html>
それに合わせて、cowboy_post/priv/static/index.htmlファイルも修正します。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</head>
<style>
div {
width:300px;
height:300px;
border-style: solid;
border-width: 1px;
margin: 100px auto;
box-shadow: 10px 10px 5px #888888;
}
form {
text-align: center;
margin-top: 100px;
margin-left: 25px;
}
p {
margin-left : 45px;
}
</style>
<body>
<div>
<form method="post" action="/process" class="form-horizontal" role="form">
<input type="text" name="echo">
<input type="submit" value="mysubmit">
</form>
</div>
</body>
</html>
この段階で $ make app
コマンドを打ったらcowboy_post/ebin/echo_dtl.beamファイルが現れます。今打たなくても大丈夫で、修正箇所はまだ残っています。
cowboy_post/src/toppage_handler.erlファイルを下のように修正します。
%% Feel free to use, reuse and abuse the code in this file.
-module(toppage_handler).
-export([init/2, do_rpn/1]).
init(Req, Opts) ->
Method = cowboy_req:method(Req),
HasBody = cowboy_req:has_body(Req),
Req2 = maybe_echo(Method, HasBody, Req),
{ok, Req2, Opts}.
maybe_echo(<<"POST">>, true, Req) ->
{ok, PostVals, Req2} = cowboy_req:body_qs(Req),
Echo = proplists:get_value(<<"echo">>, PostVals),
Result = do_rpn(Echo),
{ok, L} = echo_dtl:render([
{answer, Result}]),
Result2 = list_to_binary(L),
echo(Result2, Req2);
maybe_echo(<<"POST">>, false, Req) ->
cowboy_req:reply(400, [], <<"Missing body.">>, Req);
maybe_echo(_, _, Req) ->
%% Method not allowed.
cowboy_req:reply(405, Req).
echo(undefined, Req) ->
cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req);
echo(Echo, Req) ->
cowboy_req:reply(200, [
{<<"content-type">>, <<"text/html; charset=utf-8">>}
], Echo, Req).
do_rpn(B) when is_binary(B) ->
L = binary_to_list(B),
[Res] = lists:foldl(fun do_rpn/2, [], string:tokens(L, " ")),
case is_float(Res) of
true ->
list_to_binary(float_to_list(Res, [{decimals, 2}]));
false ->
list_to_binary(integer_to_list(Res))
end.
read(N) ->
case string:to_float(N) of
{error, no_float} ->
list_to_integer(N);
{F, _} ->
F
end.
do_rpn("+", [N1, N2|S]) -> [N2 + N1|S];
do_rpn("-", [N1, N2|S]) -> [N2 - N1|S];
do_rpn("*", [N1, N2|S]) -> [N2 * N1|S];
do_rpn("/", [N1, N2|S]) -> [N2 / N1|S];
do_rpn("^", [N1, N2|S]) -> [math:pow(N2, N1)|S];
do_rpn("ln", [N|S]) -> [math:log(N)|S];
do_rpn("log10", [N|S]) -> [math:log10(N)|S];
% sumの場合も同じで、戻り値はfoldlの二個目の引数を書き換える
% 要はアキュミュレータは足されていく
do_rpn("sum", Stack) -> [lists:sum(Stack)];
do_rpn(X, Stack) -> [read(X) | Stack].
要は下のコードを追加されました。
{ok, L} = echo_dtl:render([
{answer, Result}]),
後レスポンスはplainではなくて、htmlにします。
cowboy_req:reply(200, [
{<<"content-type">>, <<"text/html; charset=utf-8">>}
], Echo, Req).
これでcowboy_postディレクトリに下のコマンドを打つと、出来上がります。
$ make app
$ make
$ _rel/cowboy_post_release/bin/cowboy_post_release console