Gleamでwispを利用してWebアプリケーションでハローワールドとFizzBuzz
GleamでEralngベースでWebアプリケーションを作成する場合、「Awesome Gleam」によれば、「Web applications」の箇所には、「gleam_elli」か「wisp」の選択肢がある様子。
gleam_elliは、Erlang製のWebserverである「Elli」をGleamで利用できるようにしているものっぽい。言語比率も、Gleam76%に対して、Erlang23%になっている。
一方、wispは99.8%がGleamであった.
実績としてはelliベースのgleam_elliの方が安心感はあるが、今回はGleamが力を入れているであろうwispの方を試してみることにする。
まずは、wispの「docs/examples」に用意されている「00-hello-world」から試してみることにする
まずは、インストールから。
一応、前提として、環境の記録。
- macOS :(Sonoma 14.4.1)
- Gleam : 1.1.0
- Erlang : Erlang/OTP 26 [erts-14.0.1]
- rebar3 : 3.23.0
プロジェクト作成
まず、プロジェクトの作成。
今回は、「wisp_hello_world
」で作成する。
$gleam new wisp_hello_world
Your Gleam project wisp_hello_world has been successfully created.
The project can be compiled and tested by running these commands:
cd wisp_hello_world
gleam test
続けて、gleam test
を実行。
$gleam test
Compiling gleam_stdlib
Compiling gleeunit
Compiling wisp_hello_world
Compiled in 17.93s
Running wisp_hello_world_test.main
.
Finished in 0.032 seconds
1 tests, 0 failures
wispのインストール
続いて、wispをインストール。gleam add wisp
で追加してやる。
$gleam add wisp
Resolving versions
Downloading packages
Downloaded 17 packages in 2.66s
Added wisp v0.14.0
インストールが終わったので、gleam test
を実行
$gleam test
Compiling gleam_stdlib
Compiling gleeunit
Compiling wisp_hello_world
Compiled in 17.93s
Running wisp_hello_world_test.main
.
Finished in 0.032 seconds
1 tests, 0 failures
MacBook-Air-3:wisp_hello_world $gleam add wisp
Resolving versions
Downloading packages
Downloaded 17 packages in 2.66s
Added wisp v0.14.0
MacBook-Air-3:wisp_hello_world $
MacBook-Air-3:wisp_hello_world $gleam test
Compiling ranger
Compiling birl
warning: Deprecated value used
┌─ /Users/path/to/_sample/wisp_hello_world/build/packages/birl/src/birl/duration.gleam:261:31
│
261 │ let assert Ok(second) = list.at(values, 0)
│ ^^^ This value has been deprecated
It was deprecated with this message:
Gleam lists are immutable linked lists, so indexing into them is a slow
operation that must traverse the list.
In functional programming it is very rare to use indexing, so if you are
using indexing then a different algorithm or a different data structure is
likely more appropriate.
warning: Deprecated value used
┌─ /Users/path/to/_sample/wisp_hello_world/build/packages/birl/src/birl/duration.gleam:262:21
│
262 │ let leading = list.at(values, 1)
│ ^^^ This value has been deprecated
It was deprecated with this message:
Gleam lists are immutable linked lists, so indexing into them is a slow
operation that must traverse the list.
In functional programming it is very rare to use indexing, so if you are
using indexing then a different algorithm or a different data structure is
likely more appropriate.
warning: Deprecated value used
┌─ /Users/path/to/_sample/wisp_hello_world/build/packages/birl/src/birl.gleam:987:36
│
987 │ let assert Ok(weekday) = list.at(weekdays, ffi_weekday(t, o))
│ ^^^ This value has been deprecated
It was deprecated with this message:
Gleam lists are immutable linked lists, so indexing into them is a slow
operation that must traverse the list.
In functional programming it is very rare to use indexing, so if you are
using indexing then a different algorithm or a different data structure is
likely more appropriate.
warning: Deprecated value used
┌─ /Users/path/to/_sample/wisp_hello_world/build/packages/birl/src/birl.gleam:1019:30
│
1019 │ let assert Ok(month) = list.at(months, month - 1)
│ ^^^ This value has been deprecated
It was deprecated with this message:
Gleam lists are immutable linked lists, so indexing into them is a slow
operation that must traverse the list.
In functional programming it is very rare to use indexing, so if you are
using indexing then a different algorithm or a different data structure is
likely more appropriate.
Compiling exception
Compiling filepath
Compiling gleam_crypto
Compiling gleam_erlang
Compiling gleam_http
Compiling thoas
===> Fetching rebar3_ex_doc v0.2.22
===> Analyzing applications...
===> Compiling rebar3_ex_doc
===> Fetching rebar3_hex v7.0.7
===> Fetching hex_core v0.8.4
===> Fetching verl v1.1.1
===> Analyzing applications...
===> Compiling hex_core
===> Compiling verl
===> Compiling rebar3_hex
===> Analyzing applications...
===> Compiling thoas
Compiling gleam_json
Compiling gleam_otp
Compiling glisten
Compiling hpack_erl
===> Analyzing applications...
===> Compiling hpack
Compiling logging
Compiling marceau
Compiling mist
warning: Deprecated value used
┌─ /Users/path/to/_sample/wisp_hello_world/build/packages/mist/src/mist/internal/websocket.gleam:44:39
│
44 │ let assert Ok(mask_value) = list.at(masks, index % 4)
│ ^^^ This value has been deprecated
It was deprecated with this message:
Gleam lists are immutable linked lists, so indexing into them is a slow
operation that must traverse the list.
In functional programming it is very rare to use indexing, so if you are
using indexing then a different algorithm or a different data structure is
likely more appropriate.
Compiling simplifile
Compiling wisp
Compiled in 157.91s
Running wisp_hello_world_test.main
.
Finished in 0.020 seconds
1 tests, 0 failures
テスト自体は正常に終わったが、ワーニングが結構出ている。
結論、他にも、事前にインストールしておくものが必要。
「gleam_erlang」と「mist」と「gleam_http」のインストール
サンプルでは、「gleam_erlang」と「mist」と「gleam_http」の追加も行われていたので、対応する。
まずは、「gleam_erlang」の追加。
$gleam add gleam_erlang
Resolving versions
Added gleam_erlang v0.25.0
この時点で、テスト実行によるワーニングが全て消えていた。
$gleam test
Compiled in 0.76s
Running wisp_hello_world_test.main
.
Finished in 0.019 seconds
1 tests, 0 failures
続けて、mistもインストール。
$gleam add mist
Resolving versions
Added mist v1.0.0
こちらもテスト結果は変わらず。
$gleam test
Compiled in 0.24s
Running wisp_hello_world_test.main
.
Finished in 0.021 seconds
1 tests, 0 failures
最後に、gleam_httpもインストール。
$gleam add gleam_http
Resolving versions
Added gleam_http v3.6.0
一応テストも。
$gleam test
Compiled in 0.33s
Running wisp_hello_world_test.main
.
Finished in 0.041 seconds
1 tests, 0 failures
インストールが完了したので、次はソースコードを触っていく。
ソースコードを作成していく。
examplesの構成を見ると、以下のようになっている必要あり。
src
├── app
│ ├── router.gleam
│ └── web.gleam
├── app.gleam
一方、現状は以下。
src
└── wisp_hello_world.gleam
ここで、「app.gleam
」は、main関数が書かれているファイルなので、既存の「wisp_hello_world.gleam
」が該当する。
となると、app
配下のアイルを追記してやれば良さそう。
なお、サンプルのコードを読み限り、それぞれの役割は以下の通り。
- src/wisp_hello_world.gleam
- main関数が書かれている。ポートを指定して、httpサーバーの起動を行う。
- src/app/web.gleam
- Requestを受け付けてResponseを返す際の一連の処理(ログにヘッダ情報書き出したり、headerの解析だったり)している処理が記載されている。関数名「
middleware
」として定義されており、後述のrouter.gleamで利用されている。
- Requestを受け付けてResponseを返す際の一連の処理(ログにヘッダ情報書き出したり、headerの解析だったり)している処理が記載されている。関数名「
- src/app/router.gleam
- ルーティング処理、およびその際によばれる処理が記載されている。サンプルであるためにここに記載されていると思うが、実際にはルーティングだけ記載する...と思いたい。
実装
それでは、サンプルコードに倣って実装していく。
基本的にコピペになるが、一部変更とか入れてみる。
src/wisp_hello_world.gleam
ここは、基本はそのまま。
newしたときにあらかじめ設定されているデバッグ文に、サンプルのコードを追加。
ポート番号だけ「8000」→「8080」に変えてみる。
import gleam/io
import gleam/erlang/process
import mist
import wisp
import app/router
pub fn main() {
io.println("Hello from wisp_hello_world!")
wisp.configure_logger()
let secret_key_base = wisp.random_string(64)
let assert Ok(_) =
wisp.mist_handler(router.handle_request, secret_key_base)
|> mist.new
|> mist.port(8080)
|> mist.start_http
process.sleep_forever()
}
src/app/web.gleam
ここは変更せず。
import wisp
pub fn middleware(
req: wisp.Request,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
let req = wisp.method_override(req)
use <- wisp.log_request(req)
use <- wisp.rescue_crashes
use req <- wisp.handle_head(req)
handle_request(req)
}
src/app/router.gleam
Helloの名前の部分だけ変えてみる。
import wisp.{type Request, type Response}
import gleam/string_builder
import gleam/http.{Get, Post}
import app/web
pub fn handle_request(req: Request) -> Response {
use req <- web.middleware(req)
// Wisp doesn't have a special router abstraction, instead we recommend using
// regular old pattern matching. This is faster than a router, is type safe,
// and means you don't have to learn or be limited by a special DSL.
//
case wisp.path_segments(req) {
// This matches `/`.
[] -> home_page(req)
// This matches `/comments`.
["comments"] -> comments(req)
// This matches `/comments/:id`.
// The `id` segment is bound to a variable and passed to the handler.
["comments", id] -> show_comment(req, id)
// This matches all other paths.
_ -> wisp.not_found()
}
}
fn home_page(req: Request) -> Response {
// The home page can only be accessed via GET requests, so this middleware is
// used to return a 405: Method Not Allowed response for all other methods.
use <- wisp.require_method(req, Get)
let html = string_builder.from_string("Hello, MzRyuKa!")
wisp.ok()
|> wisp.html_body(html)
}
fn comments(req: Request) -> Response {
// This handler for `/comments` can respond to both GET and POST requests,
// so we pattern match on the method here.
case req.method {
Get -> list_comments()
Post -> create_comment(req)
_ -> wisp.method_not_allowed([Get, Post])
}
}
fn list_comments() -> Response {
// In a later example we'll show how to read from a database.
let html = string_builder.from_string("Comments!")
wisp.ok()
|> wisp.html_body(html)
}
fn create_comment(_req: Request) -> Response {
// In a later example we'll show how to parse data from the request body.
let html = string_builder.from_string("Created")
wisp.created()
|> wisp.html_body(html)
}
fn show_comment(req: Request, id: String) -> Response {
use <- wisp.require_method(req, Get)
// The `id` path parameter has been passed to this function, so we could use
// it to look up a comment in a database.
// For now we'll just include in the response body.
let html = string_builder.from_string("Comment with id " <> id)
wisp.ok()
|> wisp.html_body(html)
}
実行
コードの準備ができたので、実行してみる
gleam run
コンソール側に、以下のようにログメッセージが出る。
Compiled in 0.25s
Running wisp_hello_world.main
Hello from wisp_hello_world! <-- これは、元からあったデバッグ文
Listening on http://localhost:8080
起動したようなので、ローカル環境で以下にアクセス
アクセスすると、アクセスログが記録されており
Compiled in 0.25s
Running wisp_hello_world.main
Hello from wisp_hello_world!
Listening on http://localhost:8080
INFO 200 GET / <-- アクセスしたパスのログが記録されている
画面も表示されている。
ルーティングの内容確認
TOPページ自体意外にも、ページ遷移があり、どうやらコメントを投稿できそう。
ルーティングの分岐自体は、src/app/router.gleam
のこの部分ぽい
pub fn handle_request(req: Request) -> Response {
use req <- web.middleware(req)
// Wisp doesn't have a special router abstraction, instead we recommend using
// regular old pattern matching. This is faster than a router, is type safe,
// and means you don't have to learn or be limited by a special DSL.
//
case wisp.path_segments(req) {
// This matches `/`.
[] -> home_page(req)
// This matches `/comments`.
["comments"] -> comments(req)
// This matches `/comments/:id`.
// The `id` segment is bound to a variable and passed to the handler.
["comments", id] -> show_comment(req, id)
// This matches all other paths.
_ -> wisp.not_found()
}
}
アクセスすると、comments
関数が呼ばれる。
Getメソッドで動くので、list_comments
関数が呼ばれて、画面には「Comments!
」が返る。
また、該当のないURLの場合は、NOT FOUNDが返る。
以下、コンソールログの内容。
INFO 200 GET /
INFO 200 GET /comments
INFO 404 GET /hoge
ルーティングへの遷移追加+FizzBuzzの結果表示
せっかくなので、ルーティングにパスを追加する。
動的に変動するを確認するため、idを渡してその値でFizzBuzzを行うようにする。
修正が入るのは、src/app/router.gleam
のみ。
importの追加
以下の3つを追加する。
import gleam/int
import gleam/result
import gleam/string
これは、以下の理由。
- 文字列→intへの変換が、
int.int.parse(id) |> result.unwrap(0)
のようにしてやらないといけなため。 - 文字列連結が、
string.concat["A", "B", "C"]
のようにして"ABC"のようにしてやる
ルーティング部分の修正
続いて、ルーティング部分に以下のように修正を入れる。
case wisp.path_segments(req) {
// This matches `/`.
[] -> home_page(req)
// This matches `/comments`.
["comments"] -> comments(req)
// This matches `/comments/:id`.
// The `id` segment is bound to a variable and passed to the handler.
["comments", id] -> show_comment(req, id)
["fizzbuzz", id] -> fizzbuzz(req, id) # <-- これを追加
// This matches all other paths.
_ -> wisp.not_found()
}
}
関数の追加
そして、関数「fizzbuzz(req, id)
」はこちら。
fn fizzbuzz(req: Request, id: String) -> Response {
use <- wisp.require_method(req, Get)
let num =
int.parse(id)
|> result.unwrap(0)
let result = case num % 3, num % 5 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(num)
}
let html =
string.concat([id, " is ", result])
|> string_builder.from_string
wisp.ok()
|> wisp.html_body(html)
}
実行結果
上記を既存のコードに追加/修正して、再度gleam runを行う。
実行結果がこちら。
ソースコード全体
import wisp.{type Request, type Response}
import gleam/string_builder
import gleam/http.{Get, Post}
import app/web
import gleam/int
import gleam/result
import gleam/string
pub fn handle_request(req: Request) -> Response {
use req <- web.middleware(req)
// Wisp doesn't have a special router abstraction, instead we recommend using
// regular old pattern matching. This is faster than a router, is type safe,
// and means you don't have to learn or be limited by a special DSL.
//
case wisp.path_segments(req) {
// This matches `/`.
[] -> home_page(req)
// This matches `/comments`.
["comments"] -> comments(req)
// This matches `/comments/:id`.
// The `id` segment is bound to a variable and passed to the handler.
["comments", id] -> show_comment(req, id)
["fizzbuzz", id] -> fizzbuzz(req, id)
// This matches all other paths.
_ -> wisp.not_found()
}
}
fn fizzbuzz(req: Request, id: String) -> Response {
use <- wisp.require_method(req, Get)
let num =
int.parse(id)
|> result.unwrap(0)
let result = case num % 3, num % 5 {
0, 0 -> "FizzBuzz"
0, _ -> "Fizz"
_, 0 -> "Buzz"
_, _ -> int.to_string(num)
}
let html =
string.concat([id, " is ", result])
|> string_builder.from_string
wisp.ok()
|> wisp.html_body(html)
}
fn home_page(req: Request) -> Response {
// The home page can only be accessed via GET requests, so this middleware is
// used to return a 405: Method Not Allowed response for all other methods.
use <- wisp.require_method(req, Get)
let html = string_builder.from_string("Hello, MzRyuKa!")
wisp.ok()
|> wisp.html_body(html)
}
fn comments(req: Request) -> Response {
// This handler for `/comments` can respond to both GET and POST requests,
// so we pattern match on the method here.
case req.method {
Get -> list_comments()
Post -> create_comment(req)
_ -> wisp.method_not_allowed([Get, Post])
}
}
fn list_comments() -> Response {
// In a later example we'll show how to read from a database.
let html = string_builder.from_string("Comments!")
wisp.ok()
|> wisp.html_body(html)
}
fn create_comment(_req: Request) -> Response {
// In a later example we'll show how to parse data from the request body.
let html = string_builder.from_string("Created")
wisp.created()
|> wisp.html_body(html)
}
fn show_comment(req: Request, id: String) -> Response {
use <- wisp.require_method(req, Get)
// The `id` path parameter has been passed to this function, so we could use
// it to look up a comment in a database.
// For now we'll just include in the response body.
let html = string_builder.from_string("Comment with id " <> id)
wisp.ok()
|> wisp.html_body(html)
}