🐈

Gleamでwispを利用してWebアプリケーションでHelloWorldとFizzBuzz

2024/04/29に公開

スクラップからの転記記事です。

GleamでEralngベースでWebアプリケーションを作成する場合、「Awesome Gleam」によれば、「Web applications」の箇所には、「gleam_elli」か「wisp」の選択肢があるようです。

https://github.com/gleam-lang/elli

https://github.com/gleam-wisp/wisp

gleam_elliは、Erlang製のWebserverである「Elli」をGleamで利用できるようにしているものっぽい。言語比率も、Gleam76%に対して、Erlang23%になっている。
一方、wispは99.8%がGleamでした。

実績としてはelliベースのgleam_elliの方が安心感はありますが、今回はGleamが力を入れているであろうwispの方を試してみることにします。

まずは、wispの「docs/examples」に用意されている「00-hello-world」から試してみることにします。

https://github.com/gleam-wisp/wisp/tree/main/examples/00-hello-world

プロジェクトの作成とインストール

まずは、プロジェクトの作成とwispのインストールから行います。
一応、前提として、環境の記録。

  • 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
    │
261let 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
    │
262let 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
    │
987let 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
     │
1019let 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
   │
44let 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で利用されています。
  • src/app/router.gleam
    • ルーティング処理、およびその際によばれる処理が記載されている。サンプルであるためにここに記載されていると思うが、実際にはルーティングだけ記載する……と思いたい。

実装

それでは、サンプルコードに倣って実装します。
基本的にコピペになりますが、一部変更とか入れてみます。

src/wisp_hello_world.gleam

ここは、基本はそのままです。
newしたときにあらかじめ設定されているデバッグ文に、サンプルのコードを追加します。

ポート番号だけ「8000」→「8080」に変えます。

src/wisp_hello_world.gleam
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

ここは変更せず。

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の名前の部分だけ変えます。

src/app/router.gleam
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

起動したようなので、ローカル環境で以下にアクセスします。

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 /                   <-- アクセスしたパスのログが記録されている

画面も表示されています。

hello

ルーティング内容の確認

サンプルコードには、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()
  }
}

http://localhost:8080/comments

アクセスすると、comments関数が呼ばれます。
Getメソッドで動くので、list_comments関数が呼ばれて、画面には「Comments!」が返ります。

Commentsページの表示

また、該当のないURLの場合は、NOT FOUNDが返ります。

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を行います。

実行結果がこちら。

FizzBuzz 通常の数字

FizzBuzz Fizz

FizzBuzz Buzz

FizzBuzz FizzBuzz

ソースコード全体

src/app/router.gleam
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)
}

まとめ

今回は、Gleam+wispを利用して、簡単なWebアプリケーションを作成してみました。

wispのexamplesディレクトリには、他にもDBアクセスしたりjsonを利用したりといったものがあるので、その動きを確認してみようと思います。

Discussion