LoginSignup
1
1

More than 5 years have passed since last update.

CowboyとErlydtlで逆ポーランド式を計算するアプリケーションを作成

Last updated at Posted at 2015-02-27

今回やりたいことは下の図で示します.
Screen Shot 2015-02-28 at 12.38.00 AM.png

Screen Shot 2015-02-28 at 12.39.21 AM.png

つまり逆ポーランド記法をウェブ上で入力して,サーバー側で計算してから,答えを返すという実用性ゼロのプログラムを作りたいと思います.しかも中途半端な画面で申し訳ないです.要はErlangのCowboyでアプリケーションサーバーを作る練習をしたいです.これを作る途中に私にとっては一番ややこしいのはErlangのBinary, List, Integerの間の変換でした.

前提知識としてまず,

今回は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を使いたいと思います。

最終の画面のは下のようになります。

スクリーンショット 2015-03-02 15.14.46.png

スクリーンショット 2015-03-02 15.15.04.png

背景知識としてまずは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
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1