YAPC::Hiroshima 2024 参加録

広島で開催されたYAPC::Hiroshima 2024に「フル参加」してきました。フル参加って言っているのは要するに「前夜祭」「本編」「YAYAPC」の3日間に参加したということです。

なんと今回、本編については何の役割も持たずに気ままに参加したのですが、これは何時ぶりですかね…たぶんYAPC::Hokkaido 2016 SAPPORO以来のことではないでしょうか。

前夜祭

前夜祭ではHono v4に関する発表があるという事で大変集中して聞いていました。いま現在、Honoでご飯を食べている私にとっては大変重要なものですからね・・・

  • v4ではクライアントサイドにも手を伸ばしており、Reactと同一の構文でコンポーネントを書くことができる
  • HonoとViteをベースにしたフルスタックフレームワーク HonoX
  • バックエンド観点でのHonoはその互換性に手を加えることなくそのままv4に移行できる

それからキャッシュバスターズの発表もありました。こちらは晩酌をしながら聞いていたのであまり覚えていませんが、聞いていて「あるあるー」「ですよねー」という感じでした。みな同じような悩みを抱えているんだなあと。

本編

今回はスピーカーでもスタッフでもなく一般参加者として参加しました。スポンサーチケットを使っても良かったのですが、直前に念のため一般参加チケットを自費購入しており、スポンサーチケットは社員におすそ分けとしました。

今回聞いたセッションは以下の通りです。割と廊下でのんびり談話していたり個人的なハックをしていたので、聞いたセッションは少なめです。

2024年冬のPerl

charsbarさんのセッションで、まさかのリモート登壇。久々にご本人にご挨拶できると思っていたのですが、おあずけになってしまいました。

主にPerlエコシステム(とくにミラーとかセキュリティなど)に関する話題と、5.38以降の新機能についての解説がありました。

  • Security Issue(SSL証明書の検証に関する問題)があるため、HTTP::Tinyは最新版(0.083 or later)にアップデートしましょう →CVE-2023-31486
  • SSLがコアに入る未来が来るかも
  • UnicodeとWindows周りでperlにもセキュリティ対応のための更新が入るのでは
  • Strawberry Perlが復活!
  • 5.38以降の新機能 (詳しくはモバイルファクトリー社のテックブログ にも書かれている)
    • 個人的には export_lexically 関数が強すぎるなーと思いました。
    • モジュール末尾に 1; を書く必要がなくなったのは感慨深いです。
  • 今後来そうなやつ
    • 特殊変数の別名 → English モジュール相当のものかな
    • 文字列テンプレート → qt<ほげ{$val}ふが> みたいにかけるっぽい
    • 条件付きアロー演算子 → $obj?->method みたいなやつ
    • メタプログラミング → Class::MOP を現代風にしたやつかな
  • 5.8.1がツールチェーンのサポート下限から脱落
    • 今後は10年以内のものをサポート
    • 今は例外的に5.16を下限とする
    • PlackやIO::Socket::IPなどが既に5.12を下限にしている
  • Rakuについて

大変久々にPerl成分を摂取できたので、大満足でした。

Go to Cloudflare Workers ~ 移行から 0.5 年以上運用する

codehexさんのセッション。Cloudflare Workersに以降してから半年経過したというNOT A HOTELのAIアシスタント周辺の話を聞きました。

  • いくつかのフェーズを経て、Cloudflare Workersに移行した
  • デプロイが速くてまるでオフライン開発みたい
  • wrangler dev だけで環境構築が終わるので簡単
  • エッジノードで処理が行われるので、レイテンシが低い
  • アプリケーションはHono
  • monorepo構成、pnpmで管理
    • .code-workspace で複数アプリをどれもプロジェクトルートとして扱える
  • 運用にはログ・監視・トレースが必要
    • 基本的にはこれらの基盤は自作かカスタマイズか
      • (後で聞いた話だとこのあたりをサポートするサービスがCloudflareにはあるらしい)
    • リクエストの最後にログのキューを作成するHono middlewareを作成
  • pnpm patch で外部ライブラリを改造
  • cloudflare discordにへばりついてバグレポなどをしているらしい。胆力がすごい。

古い技術について—SMTP現代事情つまみ食い—

azumakuniyukiさんのセッション。SMTPについての話でした。というか、もうSMTPと言ったらこの方、この方と言ったらSMTPですよ!

メールに関する技術も最近触らなくなってきていたので、情報のアップデートができてよかったです!

YAYAPC

YAYAPC::Hiroshima。実質上のDay2とか、帰ってきた大人のYAPCとかなんとか。そんな会の司会を仰せつかっており、大人のYAPCと同様に細かいことはシェアできませんが、当日の様子を一つのツイートで表したものがこれです。

欽ちゃんのスコアボードについてはこちらがベースとなっており、私のほうでWebSocket対応をさせた次第です。もっと余力があったら審査員用の物理ボタンを用意するところだったのですが・・・

広島飯&おみやげ

正直なところ、広島飯って本当にお好み焼きと牡蛎しか知らなかったんですが、実際に行ってみると広島飯はお好み焼きと牡蛎以外にも結構バラエティに富んでいて、3日間ではまだまだ食べきれないくらいのものがありました。

お好み焼き

まぜそば

尾道ラーメン

ぷよまん

「生きとったんかワレェ!!!」

その他

まかまかさんと一緒に「広島ミックスあゆむバー」さんにお邪魔したのですが、入店するや否や女装子なあゆむさんとお客さんがテストコードの話をしていたりと、広島の懐の深さに触れることができました。聞くところによるとCOBOLerが結構いらっしゃるとかなんとか・・・あゆむさんありがとうございました!

それから、銭湯に行ったらこんなことも。銭湯民族、考えることは一緒ですね。

振り返りなど

今回は本編については特に何もせず、ただただ参加していたのですが、大変楽しい3日間でした。

広島という街の素敵さにも触れることができ、また、YAPCというイベントの素晴らしさも改めて感じることができました。

次のYAPCはどこで開催されるのでしょうか。楽しみですね。

そして広島はまたいつか行くぞ!

写真

うつ・ひきこもりの人に向けて、ITエンジニアとして働くことについて話してきた

昨年の12月に、うつ・ひきこもりの人に向けて、ITエンジニアとして働くことについて、オンライン講義という形式で話してきました。

事の発端

昨年の10月ころに、私の地元である函館にある就労支援事業所の「Ponte函館」さんから、講師をお願いしたいという依頼をメールでいただいておりました。

テーマは「うつ・ひきこもりの人に向けて、ITエンジニアとして働くことについて」ということでした。私の生まれ育ちや社会人経験をざっくりまとめたライフヒストリーと、ITエンジニアとして働くうえで必要なスキルや心構えなどについてを話しました。

資料について

スライドを以下に公開します。パワポ芸だったり、キャッチーな言葉を意識的に使っていることもあり、だいぶ情報商材っぽいうさんくさいスライドになってしまった点は反省しております。ただ、思ったことをそのまま書いているので、まあいいかという感じです。

色々ご意見等あろうかと思います。その場合は、Twitterにてどうぞ。但し、生産的ではないと思われる言論には反応しません。ご了承ください。

まとめ

私自身は少なくともひきこもりではないですが、過去にうつの傾向があった時期があります。そのような経験を踏まえて、今回の講義をさせていただきました。

より多くの方がITエンジニアとして働くために、何かしらのヒントになれば幸いです。

SAFE Stackで立ち上げてみる

※このエントリはF# Advent Calendar 2023の17日目の記事です。

※F#歴2週間かつ趣味で触っている程度(つまり経験値は実質2日ほど)の人間が書いています。

目標

SAFE Stackというものがあるようなので、SAFE StackとF#に入門してみることにしました。

SAFE Stackとは何なのか?

Isaac Abraham氏による解説を見てみましたが、どうやら以下のようなものらしいです。

Throughout this week, we've published a series of articles contributed by some well-known people within the F# community focused on web and cloud programming. Today, we're announcing the launch of the SAFE stack initiative, which brings all of the elements together as a cohesive story:

- `Saturn` model for server-side web programming
- `Azure` for cloud-based systems
- `Fable` for Javascript-enabled applications
- `Elmish` for an easy-to-understand UI programming model

Saturn, Azure, Fable, Elmishの頭文字を取ってSAFE Stackと呼んでいるようです。

Saturn - F#におけるMVCフレームワークの一つ

公式サイトによると、以下のような特徴があるようです。

- Modern programming model
  - Saturn combines the well known MVC pattern with the power of FP and F# to make web programming fun and easy.

- High performance
  - Saturn builds on top of highly optimized, and battle tested technologies such as ASP.NET Core, Giraffe and Kestrel.

- Developer experience
  - Saturn provides set of tools, templates and guides that makes creating and maintaining applications seamlessly.
  
- Created by Community
  - Saturn is created and maintained by well known members of the F# OSS Community and supported by industrial users.

要約すると、MVCパターンと関数型言語のアプローチを組み合わせた書き方ができ、パフォーマンスはGiraffeやKestrelに根ざした最適化済みのものであり、開発者向けのツール・テンプレが用意されていて、しかもOSSである、とのことです。最初の項目しか頭に入ってきていませんが、なかなか良さそうですね。

Azure - Microsoftのクラウドサービス

公式サイト

SAFE Stackの中では飛びぬけて有名なので、説明は省略します。

Fable - F#からJavaScriptを生成するコンパイラ

公式サイトを見て驚きました。まさか、F#からJavaScriptを生成するコンパイラがあるとは。Fableを使うと、F#で書いたコードをJavaScriptに変換して、Webブラウザ上で動かすことができるようです。 ちょっと凄すぎてよくわかりません・・・

Elmish - Elmの「Model View Update」を参考にした抽象化機構?

公式サイトを見てみましたが、いまいちピンと来ず。。。
もう少し深く調べてみたところ、Elm Architectureなるものがあるらしく、ElmishはそれをF#で実装したもののようです。

これらの情報から、ElmishはElmの「Model View Update」を参考にした抽象化機構っぽいな?という程度の理解しかできませんでした。

ともあれ、一旦これでSAFE Stackについてはなんとなくわかったようなわからんような感じになりました。

事前準備

VSCodeのインストールとDev Container Extensionのインストール

まずVSCodeが必要となりますので、インストールします。

次に、VSCodeの拡張機能であるRemote - Containersをインストールします。これを次の環境構築で使います。

環境構築

devcontainerを使って環境構築を行っていきます。

devcontainerの作成

まず、ブランクのディレクトリ(ここではディレクトリ名を fsharp-practice とします)を作成し、VSCodeで開きます。

次に、VSCodeのコマンドパレットを開き、 開発コンテナ―: コンテナ―機能の構成 を選択したら、イメージとして F# on Fedora を選択していきます。featuresには Node.js (via nvm), yarn and pnpm (node) を選択します。

その後、 開発コンテナ―: コンテナ―でリビルドして再度開く を選択します。しばらく待つと、 fsharp-practice ディレクトリがコンテナ―内にマウントされた状態でVSCodeが開きます。これで環境構築は完了です。

つくってみる

SAFE StackのQuickstartを参考に、 Hello, world! を返すJSON APIを作ってみます。

SAFE Stackのテンプレートをインストールする

以下のコマンドで入ります。

1
dotnet new install SAFE.Template

SAFE Stackのテンプレートを使ってプロジェクトを作成する

これもコマンドだけでOKです。

1
dotnet new SAFE

この時点で、以下のようなファイル構成になっているはずです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
vscode ➜ /workspaces/fsharp-practice $ tree
.
├── Build.fs
├── Build.fsproj
├── fsharp-practice.sln
├── global.json
├── Helpers.fs
├── package.json
├── package-lock.json
├── paket.dependencies
├── paket.lock
├── paket.references
├── README.md
├── src
│ ├── Client
│ │ ├── App.fs
│ │ ├── Client.fsproj
│ │ ├── index.css
│ │ ├── Index.fs
│ │ ├── index.html
│ │ ├── paket.references
│ │ ├── postcss.config.js
│ │ ├── public
│ │ │ └── favicon.png
│ │ ├── tailwind.config.js
│ │ └── vite.config.mts
│ ├── Server
│ │ ├── paket.references
│ │ ├── Properties
│ │ │ └── launchSettings.json
│ │ ├── Server.fs
│ │ └── Server.fsproj
│ └── Shared
│ ├── paket.references
│ ├── Shared.fs
│ └── Shared.fsproj
└── tests
├── Client
│ ├── Client.Tests.fs
│ ├── Client.Tests.fsproj
│ ├── index.html
│ ├── paket.references
│ └── vite.config.mts
├── Server
│ ├── paket.references
│ ├── Server.Tests.fs
│ └── Server.Tests.fsproj
└── Shared
├── paket.references
├── Shared.Tests.fs
└── Shared.Tests.fsproj

11 directories, 39 files

ローカル開発に使うツール群をインストールする

何やら色々入るようです。

1
dotnet tool restore

こんな感じのログが出ます。NuGetで4つほどパッケージが入っているようです。

1
2
3
4
5
6
7
8
Skipping NuGet package signature verification.
Skipping NuGet package signature verification.
Skipping NuGet package signature verification.
Skipping NuGet package signature verification.
Tool 'paket' (version '8.0.0') was restored. Available commands: paket
Tool 'fable' (version '4.1.4') was restored. Available commands: fable
Tool 'femto' (version '0.18.0') was restored. Available commands: femto
Tool 'fantomas' (version '6.2.3') was restored. Available commands: fantomas

ローカル開発サーバーを起動する

ここまで来たら起動もしていきましょう。

このコマンドでビルドもやってくれるようです。

1
dotnet run

最初20秒くらいは何も起きませんでしたが、その後、以下のようなログが出てきました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
run Run
Building project with version: LocalBuild
Shortened DependencyGraph for Target Run:
<== Run
<== InstallClient
<== Clean

The running order is:
Group - 1
- Clean
Group - 2
- InstallClient
Group - 3
- Run
Starting target 'Clean'
/workspaces/fsharp-practice/src/Client> "dotnet" fable clean --yes (In: false, Out: false, Err: false)
Fable 4.1.4: F# to JavaScript compiler
Minimum fable-library version (when installed from npm): 1.1.1

Thanks to the contributor! @zaaack
Stand with Ukraine! https://standwithukraine.com.ua/

This will recursively delete all *.fs.js[.map] files in /workspaces/fsharp-practice/src/Client
No files have been deleted. If Fable output is in another directory, pass it as argument.
Finished (Success) 'Clean' in 00:00:00.4986175
Starting target 'InstallClient'
.> "/usr/local/share/nvm/versions/node/v20.10.0/bin/npm" install (In: false, Out: false, Err: false)
npm WARN deprecated querystring@0.2.1: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated uuid@3.2.1: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.

added 171 packages, and audited 172 packages in 15s

21 packages are looking for funding
run `npm fund` for details

3 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
npm audit fix --force

Run `npm audit` for details.
npm notice
npm notice New patch version of npm available! 10.2.3 -> 10.2.5
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.2.5
npm notice Run npm install -g npm@10.2.5 to update!
npm notice
Finished (Success) 'InstallClient' in 00:00:15.7005897
Starting target 'Run'
/workspaces/fsharp-practice/src/Shared> "dotnet" build (In: false, Out: false, Err: false)
MSBuild version 17.8.3+195e7f5a3 for .NET
Determining projects to restore...
Restored /workspaces/fsharp-practice/src/Shared/Shared.fsproj (in 163 ms).
Shared -> /workspaces/fsharp-practice/src/Shared/bin/Debug/net8.0/Shared.dll

Build succeeded.
0 Warning(s)
0 Error(s)

Time Elapsed 00:00:03.69
server: /workspaces/fsharp-practice/src/Server> dotnet watch run
client: /workspaces/fsharp-practice/src/Client> dotnet fable watch -o output -s --run npx vite
/workspaces/fsharp-practice/src/Server> "dotnet" watch run (In: false, Out: true, Err: true)/workspaces/fsharp-practice/src/Client> "dotnet" fable watch -o output -s --run npx vite (In: false, Out: true, Err: true)

server: dotnet watch ⌚ Polling file watcher is enabled
client: Fable 4.1.4: F# to JavaScript compiler
client: Minimum fable-library version (when installed from npm): 1.1.1
client: Thanks to the contributor! @sasmithjr
client: Stand with Ukraine! https://standwithukraine.com.ua/
client: Using polling watcher.
client: Parsing Client.fsproj...
client: .> dotnet restore Client.csproj -p:FABLE_COMPILER=true -p:FABLE_COMPILER_4=true -p:FABLE_COMPILER_JAVASCRIPT=true
client: Determining projects to restore...
client: Paket version 8.0.0+6bcb14ec191f11e984ff0e58016f5987a5cfa8f6
client: The last full restore is still up to date. Nothing left to do.
client: Total time taken: 0 milliseconds
server: dotnet watch 🚀 Started
client: Paket version 8.0.0+6bcb14ec191f11e984ff0e58016f5987a5cfa8f6
client: Restoring /workspaces/fsharp-practice/src/Client/Client.csproj
client: Starting restore process.
client: Total time taken: 0 milliseconds
client: Restored /workspaces/fsharp-practice/src/Client/Client.csproj (in 214 ms).
client: 1 of 2 projects are up-to-date for restore.
server: Unhandled exception. System.ArgumentException: An item with the same key has already been added. Key: /workspaces/fsharp-practice/src/Server/obj/Debug/net8.0/staticwebassets
server: at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.<CheckForChangedFiles>b__23_0(FileSystemInfo f)
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.ForeachEntityInDirectory(DirectoryInfo dirInfo, Action`1 fileAction)
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.ForeachEntityInDirectory(DirectoryInfo dirInfo, Action`1 fileAction)
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.ForeachEntityInDirectory(DirectoryInfo dirInfo, Action`1 fileAction)
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.ForeachEntityInDirectory(DirectoryInfo dirInfo, Action`1 fileAction)
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.CheckForChangedFiles()
server: at Microsoft.DotNet.Watcher.Internal.PollingFileWatcher.PollingLoop()
client: Some Nuget packages contain information about NPM dependencies that can be managed by Femto: https://github.com/Zaid-Ajaj/Femto
client: Project and references (67 source files) parsed in 10372ms
server: warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
server: Storing keys in a directory '/home/vscode/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed. For more information go to https://aka.ms/aspnet/dataprotectionwarning
server: info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[62]
server: User profile is available. Using '/home/vscode/.aspnet/DataProtection-Keys' as key repository; keys will not be encrypted at rest.
server: info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
server: Creating key {b2652658-4977-45a5-809c-2f3f8a9cc018} with creation date 2023-12-16 16:56:54Z, activation date 2023-12-16 16:56:54Z, and expiration date 2024-03-15 16:56:54Z.
server: warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
server: No XML encryptor configured. Key {b2652658-4977-45a5-809c-2f3f8a9cc018} may be persisted to storage in unencrypted form.
server: info: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[39]
server: Writing data to file '/home/vscode/.aspnet/DataProtection-Keys/key-b2652658-4977-45a5-809c-2f3f8a9cc018.xml'.
server: warn: Microsoft.AspNetCore.Hosting.Diagnostics[15]
server: Overriding HTTP_PORTS '8080' and HTTPS_PORTS ''. Binding to values defined by URLS instead 'http://localhost:5000'.
server: info: Microsoft.AspNetCore.Server.Kestrel[0]
server: Unable to bind to http://localhost:5000 on the IPv6 loopback interface: 'Cannot assign requested address'.
server: info: Microsoft.Hosting.Lifetime[14]
server: Now listening on: http://localhost:5000
server: info: Microsoft.Hosting.Lifetime[0]
server: Application started. Press Ctrl+C to shut down.
server: info: Microsoft.Hosting.Lifetime[0]
server: Hosting environment: Development
server: info: Microsoft.Hosting.Lifetime[0]
server: Content root path: /workspaces/fsharp-practice/src/Server
client: Loaded Feliz.HookAttribute from ../../../../home/vscode/.nuget/packages/feliz.compilerplugins/2.2.0/lib/netstandard2.0/Feliz.CompilerPlugins.dll
client: Loaded Feliz.ReactComponentAttribute from ../../../../home/vscode/.nuget/packages/feliz.compilerplugins/2.2.0/lib/netstandard2.0/Feliz.CompilerPlugins.dll
client: Loaded Feliz.ReactMemoComponentAttribute from ../../../../home/vscode/.nuget/packages/feliz.compilerplugins/2.2.0/lib/netstandard2.0/Feliz.CompilerPlugins.dll
client: Started Fable compilation...
client: Fable compilation finished in 8173ms
client: .> npx vite
client: Watching ..
client: VITE v5.0.5 ready in 1074 ms
client: ➜ Local: http://localhost:8080/
client: ➜ Network: use --host to expose
client: ➜ press h + enter to show help

ブラウザで確認する

ブラウザで http://127.0.0.1:8080 にアクセスすると、以下のような画面が表示されました。

テキストエリアに文字列を入れてから Add ボタンを押すことで、その内容がリストに追加されるようです。簡易的なToDoアプリでしょうか。

まとめ

本当はこのあとロジックの修正とかまでやってみたかったんですが、なかなか手が付かず、SAFE Stackなアプリケーションの立ち上げまでやっただけでした。

出来上がったソースコードを読んだりいじりながら、SAFE StackとF#の理解を深めていくことにします。

本当に初心者が書いたエントリですが、何かお役に立てれば幸いです。

HonoをAzure Functionsで動かす

※このエントリはHono Advent Calendar 2023の10日目の記事です。

Azure Functionsとは

Azure Functionsは、MicrosoftがAzure上で提供する、サーバーレスな関数を作成するためのサービスです。Azure Functionsは、以下のような特徴があります。

サーバーレス

Azure Functionsは、サーバーレスなサービスです。サーバーレスとは、サーバーを意識せずに、コードを実行できることを指します。Azure Functionsでは、コードを実行するためのサーバーを意識する必要がありません。また、コードを実行するためのサーバーを自分で用意する必要もありません。

イベント駆動

Azure Functionsは、イベント駆動なサービスです。Azure Functionsでは、以下のようなイベントをトリガーに、コードを実行することができます。

  • HTTPリクエスト
  • メッセージキュー
  • データベースの変更
  • タイマー

今回はHTTPリクエストをトリガーにコードを実行する方法となります。

言語の選択肢が豊富

Azure Functionsでは、以下の言語を利用して、コードを実行することができます。

  • C#
  • Java
  • JavaScript
  • TypeScript
  • PowerShell
  • Python

その他にも、カスタムハンドラーDockerコンテナ を利用して、さらに多くの言語を利用することができます。

HonoをAzure Functionsで動かす

まずAzure Functions Core Toolsを利用して、ローカルな環境でHonoをAzure Functionsで動かしてみます。

今回はすぐに動作を試せるように、サンプルを使いましょう。サンプルはこちらで公開しています。

Dev Containerで開発環境を構築する

まずは、開発環境を構築します。開発環境はDev Containerで構築します。Dev Containerという拡張機能を利用しますので、Visual Studio CodeにDev Containerをインストールしてください。

次に、Visual Studio Codeで、ytnobody-hono-on-azure-functions-prototypeを開きます。コマンドパレットを開き、Remote-Containers: Reopen in Containerを選択します。すると、Dev Containerが起動します。初回は起動までに結構な時間がかかります。

Dev Containerが起動したら、以下のコマンドを実行して、依存関係をインストールします。

1
npm install

一緒に azure-functions-core-tools もインストールされます。これがAzure Functions Core Toolsです。

Azure Functions Core Toolsを起動して動作確認する

Azure Functions Core Toolsを起動して、動作確認をします。以下のコマンドを実行します。

1
npm run start

このコマンドでビルドも行われ、Azure Functions Core Toolsが起動します。

以下のようなログが出力されましたら、Azure Functions Core Toolsが無事に起動しています。

1
2
3
4
5
6
7
8
[2023-12-09T23:11:32.503Z] Worker process started and initialized.

Functions:

ytnobody-hono-func: [GET,POST] http://localhost:7071/api/ytnobody-hono-func

For detailed output, run func with --verbose flag.
[2023-12-09T23:11:36.744Z] Host lock lease acquired by instance ID '000000000000000000000000E7F5F683'.

Azure Functions Core Toolsが起動したら、ブラウザでhttp://localhost:7071/api/ytnobody-hono-funcにアクセスします。以下のようなJSONが表示されましたら、無事に動作しています。

Alt text

実際にAzure Funtionsでも動作した!

一応実環境でも同じように動作するか確認してみました。以下のURLにアクセスしてみてください(そのうち消します)。

https://ytnobody-hono-on-azure-functions-prototype.azurewebsites.net/api/ytnobody-hono-func

POSTメソッドでJSONを送ると、そのデータが含まれたJSONが返ってきます。

この方法の制約

Honoでアプリケーションを開発する際、複数のハンドラーを実装することが多いでしょう。つまり、 GET /POST /item に対応したいというように、複数のエンドポイントを実装しますよね、という話です。

ところが、Azure FunctionsのHTTP Triggerでは、1つの関数に対して1つのエンドポイントしか設定できません。つまり、 GET /POST /item に対応するためには、2つの関数アプリを作成する必要があります。

また、Azure FunctionsのHTTP Triggerでは、function.jsonというファイルに、エンドポイントの設定を記述します。ここにHTTPメソッドを指定するため、Hono側でHTTPメソッドの指定をしたとしても、Function.jsonの設定が優先されてしまいます。

そのため、Honoアプリケーション側で指定するHTTPメソッドは基本的にはallを指定することになるでしょう。

Hono coreへのPull Request

Hono coreに、今回作成したAdapterを含むPull Requestを送りました。まだDraftの状態ですが、以下のURLから確認できます。

https://github.com/honojs/hono/pull/1797

もうすこしブラッシュアップして、マージできるようにしたいと思います。

もう一つの方法:Docker on Azure Functions

Azure Functionsでは、Dockerコンテナを利用して、さらに多くの言語を利用することができます。これを利用すると、Honoをそのまま動かすことができます。

Dockerコンテナを利用する方法は、以下のURLに詳しく書かれています。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-function-linux-custom-image

おわりに

Azure FunctionsでHonoを動かす方法を紹介しました。Azure Functionsは、サーバーレスなサービスで、イベント駆動なサービスです。TypeScriptにも対応しており、nodeもv18が使える状態でしたので、Honoを動かすことができます。

また、Azure Functionsで動作するように、Hono coreにもPull Requestを送りました。ブラッシュアップの末にこれがマージされれば、制約はありますがAzure FunctionsでHonoを動かすことができるようになります。

雑なデータ加工に使えるORM:Otogiri

※このエントリはPerl Advent Calendar 2023の3日目の記事です。

※元々は「雑なデータ加工に使える小技5撰」でしたが内容がOtogiriだらけなので、タイトルを変更しました。

Otogiriとは

拙作のPerl製ORM(もどき)です。Otogiri - CPANを参照してください。

特徴として、以下のようなものがあります。

  • スキーマ定義がない(DB/テーブルにあるものを正としている)
  • データは全てハッシュリファレンスかその配列で表現される
  • 端的に言えば DBIx::Sunny + SQL::Maker

MetaCPANに公開されているモジュールですので、cpanm Otogiriでインストールできます。

Otogiriの基本的な使い方

DB接続

podにも書いてありますが、DB接続は以下のようにして行います。以下の例ではmysqlに接続しています。

1
2
3
4
5
6
use Otogiri;
my $db = Otogiri->new(connect_info => [
'dbi:mysql:dbname=mydb',
'dbuser',
'dbpassword'
]);

strictモード

Otogiriはデフォルトでstrictモードで動作します。strictモードはSQL::Makerのstrictモードそのままの制約がつきます。

strictモードはこちらに記載があるようなSecurity Issue(JSON SQL Injection)への対策として、SQL::Makerに実装されています。もしWebアプリケーションにおいて、ユーザーからの入力をSQLに埋め込む場合はstrictモードを有効にすることを強くお勧めします。

strictモードを解除するには、DB接続時に以下のようにします。

1
2
3
4
5
6
7
8
my $db = Otogiri->new(
connect_info => [
'dbi:mysql:dbname=mydb',
'dbuser',
'dbpassword'
],
strict => 0
);

これはselectやinsertの際の記述方法に影響があります。別途、selectやinsertの説明の際に触れます。

データ取得

データ取得はselectメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## strictモードが有効の場合
my @rows = $db->select(user => { id => sql_in([1, 4]) });

## strictモードが無効の場合
my @rows = $db->select(user => { id => [1, 4] });

## 以下のようなデータが@rowsに入ります。
## (
## {
## id => 1,
## name => 'ytnobody',
## age => 43,
## created_at => '2023-12-03 00:00:00'
## },
## {
## id => 4,
## name => 'somebody',
## age => 36,
## created_at => '2023-12-03 01:00:00'
## },
## )

単一データを取得する場合はfetchメソッドを使います。

1
2
3
4
5
6
7
8
9
my $row = $db->fetch(user => { id => 1 });

## 以下のようなデータが$rowに入ります。
## {
## id => 1,
## name => 'ytnobody',
## age => 43,
## created_at => '2023-12-03 00:00:00'
## }

データ挿入

データ挿入はinsertメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
## strictモードが有効の場合
$db->insert(user => {
name => 'ytnobody',
age => 43,
created_at => sql_raw("datetime(now)"),
});

## strictモードが無効の場合
$db->insert(user => {
name => 'ytnobody',
age => 43,
created_at => \"datetime(now)",
});

データ更新

データ更新はupdateメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
## strictモードが有効の場合
$db->update(user => {
age => 44,
created_at => sql_raw("datetime(now)"),
}, { id => 1 });

## strictモードが無効の場合
$db->update(user => {
age => 44,
created_at => \"datetime(now)",
}, { id => 1 });

データ削除

データ削除はdeleteメソッドを使います。

1
2
3
4
5
## strictモードが有効の場合
$db->delete(user => { id => sql_in([1, 4]) });

## strictモードが無効の場合
$db->delete(user => { id => [1, 4] });

トランザクション

トランザクションはtxn_scopeメソッドを使います。

1
2
3
4
5
6
7
8
9
10
11
12
my $txn = $db->txn_scope;
$db->insert(user => {
name => 'ytnobody',
age => 43,
created_at => sql_raw("datetime(now)"),
});
$db->insert(user => {
name => 'somebody',
age => 36,
created_at => sql_raw("datetime(now)"),
});
$txn->commit;

もっと複雑な使い方

ここから先はさらに複雑な使い方を紹介します。

データ取得時にカラム指定を行う

Otogiriは基本的にテーブルにある全てのカラムを取得します。これは「雑にデータ取得と処理を行うこと」を主目的としているため、そのままではカラム指定を行うことができません。

ですが、Otogiriにはプラグイン機構があります。これはOtogiri::Pluginというモジュールによって実現されています。

カラム指定を行うプラグインはOtogiri::Plugin::SelectWithColumnsです。

使い方は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
use Otogiri;
use Otogiri::Plugin;
my $db = Otogiri->new(connect_info => [...]);
$db->load_plugin('SelectWithColumns');

my @rows = $db->select_with_columns(
'some_table',
['id', 'name'],
{'author' => 'ytnobody'},
{order_by => 'id ASC'}
);

select_with_columns というメソッドが使えるようになり、その第2引数でカラムを指定することができます。

スキーマ定義をしてデータクラスを指定する

Otogiriの特徴には「スキーマ定義がない」というものがありましたが、その気になればスキーマ定義を書くこともできます。

perl-5.38以降であれば class 構文を使うことでスキーマ定義を書くことができます。

1
2
3
4
5
6
7
8
9
10
11
12
class Book {
field $id :param;
field $title :param;
field $author :param;
field $price :param;
field $created_at :param;
field $updated_at :param;

method title {
return $title;
}
};

実際に単一取得を行う場合、事前にrow_classメソッドでスキーマ定義を指定することで、スキーマ定義に従ったオブジェクトを取得することができます。

1
2
my $book = $db->row_class('Book')->single(book => {id => 1}); 
say $book->title;

敢えてrow_classを無効にしたいシーンでは、row_classメソッドの代わりにno_row_classメソッドを使います。

1
2
my $book = $db->no_row_class->single(book => {id => 1});
say $book->{title};

inflate/deflate

Otogiriの機能で最も複雑なのがinflate/deflateです。これはDBから取得したデータを予め指定した関数に基づいて変換したり、DBに格納するデータを予め指定した関数に基づいて変換したりする機能です。

inflate/deflateは以下のようにして使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use JSON;
use Otogiri;
my $db = Otogiri->new(
connect_info => [...],
inflate => sub {
my ($data, $tablename, $db) = @_;
if (defined $data->{json}) {
$data->{json} = decode_json($data->{json});
}
$data->{table} = $tablename;
$data;
},
deflate => sub {
my ($data, $tablename, $db) = @_;
if (defined $data->{json}) {
$data->{json} = encode_json($data->{json});
}
$data;
},
);

inflateはselect(), search_by_sql(), fetch()がコールされた後に呼ばれ、取得データを加工します。
deflateはinsert(), update(), delete()がコールされる前に呼ばれ、格納データを加工します。

直近のOtogiriについて

基本的にはOtogiriのメンテははっきり言ってのんびりしています。ですが、row_class の機能は最近追加されたばかりです。

また先ほど、DBURLへ対応をさせるためのPull-Requestを作成したところです。

もしこれが取り込まれたら、以下のようにDB接続情報をURLで指定できるようになります。

1
2
my $dburl = 'mysql://dbuser:dbpassword@dbhost/mydb?someOption=someValue&otherOption=otherValue';
my $db = Otogiri->new(dburl => $dburl);

Otogiriの今後

基本的には「雑にデータ取得と処理を行うこと」が主目的なのは変わらずで、そのための機能を追加していく予定です。

ぜひ、Otogiriを使ってみてください。そして、Pluginの作成やPull-Requestの作成など、Otogiriの開発にも参加していただけると嬉しいです。

函館市電LT大会というイベントでLTしてきた

9/18(祝)に開催された函館市電LT大会というイベント参加してきました。

函館市電LT大会?

そもそもどんなイベントなのかをひとことで説明すると、「函館市電を貸し切って、函館市内を走行する市電車両の中でLT発表会を行う」というものです。うん、まだ意味不明ですね。

函館市電とは?

函館市電というのは函館市が運営する路面電車です。函館市企業局交通部という市の部署が運行からメンテナンス等、路面電車を運行するためのあらゆる業務を担当しています。

以下がその路線図となります。(函館市電の公式から画像を拝借しました)

函館市電路線図

運行系統としては2系統と5系統があり、湯の川~十字街の間は全く同じ場所を走ります。十字街から先は、2系統が谷地頭まで、5系統が函館どつく(これで「はこだてどっく」と読みます)まで、それぞれ走ることになります。

それぞれの駅(正しくは停留所)の間は概ね2分程度での移動が見込まれています(ここ、あとで重要になります)。

LT大会??

ライトニングトーク(LT)をする大会ということなのですが、厳密にはLTは5分間という決まりがあります。

ところが当イベントでは時間制限の区切り方がだいぶ特殊でして、「市中を走行する路面電車内でのLT」ということもあって、5分間という区切りの代わりに「3区間で無慈悲に発表終了となる」というルールが採用されました。

例えば湯の川からLTを開始した場合、3区間先までとなりますから、駒場車庫前に到着した時点でLT強制終了となります。

停留所の間は概ね2分程度ですので、大体6分くらいが持ち時間としての期待値となります。しかし、普通のLTと比べて時間が読みにくく、停留所どうしの間隔が長かったり短かったりする区間もありますし、路面電車という交通手段の都合上、どうしても信号や自動車との兼ね合いで時間がブレるわけです。

そのため、ちょうど良いタイミングでLTを終わらせるには、函館市電をどれだけ使いこなしているかが試されるのです。

路面電車の車両を1両まるごと貸し切り???

このイベントの突飛なところは公共交通機関を丸ごと1両借りてしまうというその豪快さにあるでしょう。企画を目にしたときには「なんてめちゃくちゃな!」と思ったものです。

ところがこの函館市電、過去には地元企業の慰労会等でカラオケ電車なるものを運行していた実績があります。そして、今もなお「1運行:大人20000円」で貸し切り運行を受け付けています。

要するに10人も集まれば、一人当たり2000円程度で貸し切り運行ができてしまうのです。全然めちゃくちゃじゃなかった。

どんな発表があったか?

一部を取り上げると、以下のような感じのものがありました。

  • Arduinoを使った心拍数を計測するデバイスの実演とバグ修正
  • 積ん読をひたすら推奨する話
  • 道南地区のロードサイドについて掘り下げる話
  • 鉄道模型のコントローラーの話
  • 自作Dvorakキーボードの話

どんな話をしたのか?

私の発表は「ビジネスロジックを純粋関数で実装する」的な話です。

その他

  • 工業高校のOBが途中参加。文字通りの飛び入り参加で場を沸かせる。
  • はこだて未来大学の角教授が「なんか面白そうなことをしているな?」と言いつつ終点の駒場車庫に登場。写真を存分に撮影してくださった。
  • イベント後は松陰町のラッキーピエロで懇親会。

とにかくめちゃくちゃ楽しかったです!来年もやるんですよね?やりましょう!

twitter(X) のハッシュタグ #函館市電LT #函館市電LT大会 もみてみるといいです!

※9/19 函館空港のビジネスコーナーにて、羽田行の最終便を待ちつつ執筆。

湘南.pm#1に参加してきた

※だいぶ時間経過しちゃったけど、エントリだけ書いてあって公開してなかったので公開します。

8/26(土)に開催された湘南.pm #1に発表者として参加してきました。

発表内容

スライドはこちらです。適当に revealjs 等で見てください。

最近やっているコンポーネント構成とエンジニア育成について話した

特に初学者と呼ばれる人がプロジェクトに参加しやすくすることを考慮したコンポーネント構成について話しました。ざっくり箇条書きにすると以下のような感じです。

  • そもそもプログラミングは難しいものだし、その中でもオブジェクト指向プログラミングは初学者に教えるのも、また理解してもらうのもコストが嵩む。
  • とはいえ、初学者だからと言ってプロジェクトに参加させない、という選択をし続けることにも限界がある。
  • そこで、初学者にもわかりやすいコンポーネント構成を考えることにした。
  • 結果としてオブジェクト指向プログラミングを捨てたコンポーネント構成となった。
  • コンポーネント間の依存方向を一方通行にしたり、副作用の有無によってビジネスロジックを明確に分離する、といった工夫をした。

また、今回発表したコンポーネント構成を Perl で実装したものがこちらです。dev-container を使っているので、ローカルに Perl をインストールする必要はありません。

ちなみに最近は Typescript で開発しているので、Perl で開発するのはかなり久しぶりでした。

その他

  • いっぱい飲んだ。いっぱい人と話した。楽しい。
  • YAPC::Hiroshima ぶち上げていこう!!!
  • 藤沢、面白い街だなぁ。また行きたい。
  • @papix おめでとう!

Typescriptにおける関数の副作用についてChatGPTに訊いてみた

自分の認識を確認したくてChatGPTに壁打ち

先ほど、自分の見解に対する確証を得たくてChatGPT(text-davinci-003)に色々と質問してみました。

今回質問したテーマは以下の通りとなります。

  • Typescriptにおいて、 Promise を返す関数はすべて副作用のある関数だと言えるのではないか?

以下、そのやり取りです。一部、意図的に人間にとってかなりきつい受け答えをしています。

ChatGPTとのやりとり

ytnobody: Typescriptにおいて、 Promise を返す関数はすべて副作用のある関数であると言えますか?

chatgpt: いいえ、そうではありません。 Promise を返す関数が副作用のある関数であるかどうかは、その関数が内部的に何をしているかによって異なります。 Promise を返す関数が副作1用を持つかどうかを確認する必要があります。

ytnobody: 多くの場合、 Promise を返すということは、プログラムの外部にある存在に対して何らかの問い合わせを行っていると考えられると思うのですが、違うのですか?

chatgpt: いいえ、違いません。 Promise を返すということは、プログラムの外部にある存在に対して何らかの問い合わせを行っていると考えられます。ただし、 Promise を返す関数が副作用を持つかどうかは、その関数が内部的に何をしているかによります。

ytnobody: 例えば以下のような関数があったとします。

function myFunc(inputNum: number) :  Promise<MyOutput> {
  const myOutput = otherFunc(inputNum);
  return myOutput;
}

ytnobody: この場合、 myFunc 関数も otherFunc 関数も副作用があると言えるはずですが、違うのですか?

chatgpt: いいえ、違いません。 myFunc 関数と otherFunc 関数の両方が副作用を持つ可能性があります。 myFunc 関数が内部的に行っている処理や otherFunc 関数が行っている処理を確認して、それぞれの副作用を評価する必要があります。

ytnobody: 前述のケースでは、 myFunc 関数は otherFunc 関数を呼び出し、返り値 myOutput を受け取って、何も加工せずにmyFuncの返り値として myOutput を返しているだけです。この場合、 otherFunc 関数は副作用を確実に持っていると言えると思いますが、ちがいますか?

chatgpt: いいえ、違いません。 otherFunc 関数が副作用を持つかどうかは、その関数が内部的に何をしているかによって異なります。 otherFunc 関数が副作用を持つかどうかを確認する必要があります。

ytnobody: もし仮に otherFunc 関数が副作用を持たないのであれば、それは Promise を返す必要はなく、直接 MyOutput 型を返すべきではないでしょうか?

chatgpt: はい、そうです。 Promise を返す必要がない場合は、 MyOutput 型を直接返すことができます。

ytnobody: であれば、 Promise を返す関数は100%の確率で副作用を持つと言えるのではないでしょうか?

chatgpt: いいえ、そうではありません。 Promise を返す関数が副作用を持つかどうかは、その関数が内部的に何をしているかによって異なります。 Promise を返す関数が副作用を持つかどうかを確認する必要があります。

ytnobody: では、どのようなケースで副作用を持たずに Promise を返す関数が定義されるのでしょうか?

chatgpt: 例えば、データベースなどの外部サービスから情報を取得する関数があります。このような関数では、 Promise を返してデータベースなどの外部サービスから取得した情報を待ち受けることができますが、実際には外部サービスへの問い合わせだけを行っており、実際に副作用を持たずに Promise を返すことができます。

ytnobody: 外部サービスへの問い合わせを行った時点で副作用を持つという定義に当てはまります。あなたの言っていることは間違えています。

chatgpt: 外部サービスへの問い合わせは副作用を持つという定義に当てはまりますが、その関数が Promise を返すかどうかには関係ありません。その関数が内部的に何をしているかを確認する必要があります。

ytnobody: Typescriptにおいては、 Promise を返す理由は「外部への問い合わせのような、プログラムの外部の要因の処理が完了するのを待つ必要がある場合」に限られるはずです。

chatgpt: はい、その通りです。 Promise を返す理由は、外部への問い合わせのような、プログラムの外部の要因の処理が完了するのを待つ必要がある場合に限られます。

ytnobody: それこそが副作用でしょう

chatgpt: いいえ、そうではありません。外部への問い合わせは副作用を持つという定義に当てはまりますが、その関数が Promise を返すかどうかには関係ありません。その関数が内部的に何をしているかを確認する必要があります。

ytnobody: では聞きますが、副作用がありつつ、 Promise を返さない関数を定義してください。

chatgpt: 例えば、以下のような関数を考えます。

function myFunc(inputNum: number) : MyOutput {
  const result = inputNum * 2;
  return { result: result };
}

chatgpt: この関数は、inputNumを2倍した結果を MyOutput 型で返すだけなので、副作用を持たずに Promise を返すことができます。

ytnobody: それは間違いです。 MyOutput は単なる構造体型であり、 Promise ではありません。

chatgpt: この関数は、副作用を持たずに MyOutput 型を返すことができます。

ytnobody: 論点がずれています。私が指摘しているのは、 Promise を返す関数は100%副作用を持つということを言っています。

chatgpt: いいえ、そうではありません。 Promise を返す関数が副作用を持つかどうかは、その関数が内部的に何をしているかによります。 Promise を返す関数が副作用を持つかどうかを確認する必要があります。

ytnobody: その根拠を説明してください。いまのところ、その根拠についてあなたは一度も説明責任を果たしていません。

chatgpt: はい、その通りです。 Promise を返す関数が副作用を持つかどうかを確認する必要がある理由は、 Promise を返す関数が内部的に何をしているかによって異なるからです。 Promise を返す関数が内部的に何をしているかを確認する必要があるので、 Promise を返す関数が100%副作用を持つとは言えません。

ytnobody: 小泉進次郎のような自己言及的な言い回しになっているせいで、説明責任を果たせていません。

chatgpt: Promise を返す関数が内部的に何をしているかを確認する必要があるから、 Promise を返す関数が100%副作用を持つとは言えません。

ytnobody: まず、 Promise は外部に問い合わせを行う時にしか使わないと、あなた自身が発言しています。次に、外部に問い合わせを行うことは副作用を持つと言えるということも、あなた自身は肯定しています。これらの事から、 Promise を返す関数は副作用を持つと言えます。三段論法ですよこれは。こんなこともわからないのですか?

chatgpt: いいえ、わかりました。あなたが言っていることは理解しました。 Promise を返す関数は、外部への問い合わせを行うことで副作用を持つと言えます。また、 Promise を返す関数が内部的に何をしているかを確認する必要があります。

ytnobody: では、もう一度聞きます。Typescriptにおいて、 Promise を返す関数はすべて副作用のある関数であると言えますか?

chatgpt: いいえ、そうではありません。 Promise を返す関数が副作用を持つかどうかは、その関数が内部的に何をしているかによります。 Promise を返す関数が副作用を持つかどうかを確認する必要があります。

ytnobody: わかっていないようですね。「まず、 Promise は外部に問い合わせを行う時にしか使わないと、あなた自身が発言しています。次に、外部に問い合わせを行うことは副作用を持つと言えるということも、あなた自身は肯定しています。これらの事から、 Promise を返す関数は副作用を持つと言えます。三段論法ですよこれは。こんなこともわからないのですか?」と私は先ほど言いました。あなたは自己学習機能がないのですか?

chatgpt: いいえ、自己学習機能はありません。あなたが言っていることは理解しました。 Promise を返す関数は、外部への問い合わせを行うことで副作用を持つと言えます。また、 Promise を返す関数が内部的に何をしているかを確認する必要があります。

ytnobody: 自己学習機能がないのであれば、仕方がないですね。あなたの開発者があなたを改善してくれることを期待します。

まとめ

わかっていたことですが、学習機能がないため、前段でこちらの意見を飲んだにも関わらず、改めて当初の質問をすると元の木阿弥となってしまいましたね。

そして、私が当初確証を得たかったことについては、まぁ概ねその通りっぽいということにしておくことにしました。つまり、 Promise を返す関数はすべて副作用のある関数だと言えるようです。少なくとも純粋関数ではないと言い切れるんじゃないかと。

近頃やっているコンポーネント構成について

最近のPerlでのWebアプリケーション開発

わいとんです。このエントリはPerl Advent Calendar 2022の20日目のエントリです。その割にはまあまあボリュームのある内容となっています。
ちなみに昨日のエントリは@hirataraさんによる「joinクイズ」でした 。私も一見しただけでは答えがわかりませんでした。かなり難しいです!

さて、実は最近またひっそりとPerlで開発をしていまして、主に開発最初期の設計の任を預かることが結構あります。

「新規でPerlを使うのはどうなんだ」等のご意見があろうかと思いますが、これについては、その時置かれている状況ごとによって最適解は異なる、という意見を述べるに留めておきます。

歴史とMVC

Perlでは長らくMVCという設計アプローチが採用されてきました。これがなかなか優れた設計であるがゆえに、ここまで長きに亘ってMVCが利用され続けてきたのです。

MVCは優れている。ただし、人間の怠惰の面倒までは見てくれない。

個人的にMVCの利点としては以下のようなものがあると認識しております。

  1. ViewとModelが分離することにより、Viewからロジックを引きはがすことに成功している。
  2. ControllerとModelが分離することにより、複雑な実装をModelに引きはがすことでControllerをシンプルに保つことに成功している。
  3. Modelについてテストを記述することで、動作仕様の担保に成功している。

ところが、人間という生き物は怠惰でありますから、上記のようなMVCの利点を以下のようなふるまいでムダにしてしまいがちです。

  1. ViewとModelが分離しているにもかかわらず、マクロ等を駆使してViewにロジックを記述してしまいがちである。
  2. ControllerとModelが分離しているにもかかわらず、Controllerにロジックを記述してしまいがちである。
  3. ひとつのControllerに役割を持たせすぎてしまいがちである。
  4. ひとつのModelに役割をもたせすぎてしまいがちである。
  5. Modelのテストを記述せずに済ませてしまいがちであり、動作仕様の担保をないがしろにしがちである。

MVCにおいては、人間が勤勉であればあるほどModelが肥える

ところで、もし仮に勤勉な人間ばかりで開発陣が構成されていたら、MVCではどのようになるでしょうか。改めて先ほどのMVCの利点を列挙してみます。

  1. ViewとModelが分離することにより、Viewからロジックを引きはがすことに成功している。
  2. ControllerとModelが分離することにより、複雑な実装をModelに引きはがすことでControllerをシンプルに保つことに成功している。
  3. Modelについてテストを記述することで、動作仕様の担保に成功している。

これを見る限り、1ではロジックがViewから引きはがされ、2ではControllerからロジックが引きはがされています。宙に浮いたロジックが行きつく先はModelとなるわけですが、当然ViewやControllerからロジックが集まってきたということは、その物量はなかなか結構な量となるはずです。

つまり、MVCでは開発者がまじめにMVCを運用・最適化すると、必然的にModelが肥大化するさだめにあるのです。

バックエンドアプリケーションを取り巻く昨今の事情

ここ数年の情勢の変化で、バックエンドアプリケーションに大きな影響を与えたものとして「サーバサイドレンダリング」を求められなくなった、というものが挙げられます。

これはいくつかの要因が重なりあった結果なのですが、主な要因は以下のようなものだと認識しています。

  1. Webフロントエンドにおける各種フレームワークの成熟(代表的なものにNext.jsやReactなど)
  2. ガラケーの終焉

Webフロントエンドにおける各種フレームワークの成熟

言うまでもなく、この7~8年間においてもっとも進化したソフトウェア分野のひとつとして、Webフロントエンドフレームワークが挙げられます。

今では当たり前のようにAPIに対してリクエストを送り、JSONレスポンスを受け取って、表示に必要な処理をこなしてくれるWebフロントエンド。ReactやVue.js、Next.jsの登場とそれらの相互作用によるすばやい進化によって、現在のような賢いWebフロントエンドが実現できるようになったと言えるでしょう。

また、これらのフレームワークとは別に、Webフロントエンドはサーバサイドレンダリングやサーバサイドジェネレーターの機能をも取り込むという進化を遂げました。

結果、バックエンドアプリケーションはもはやAPIとしての役割だけが期待されることとなり、現在に至ります。

ガラケーの終焉

ガラケー時代では、Webアプリケーションと言えばPerlかPHP、あるいは後期ではRubyで開発するのが相場でした。

多くのガラケーではJavaScriptは動作せず、動作したとしてもあまり実用的とは言えない程度のものでした。デザインの面でもできることは限られており、一度に表示できるページ容量には厳しい上限が課されていました。

このようなこともあり、フロントエンドとバックエンドの分業はあまり起こらず、むしろバックエンドエンジニアがフロントエンドも兼任するという風景が日常的でした。そしてMVCが採用され、今日まで概ねなんとなくうまく行っていたというわけです。

ところが、DoCoMoがiModeのサービス終了を発表した(実際の終了は令和8年3月末)こともあり、ガラケーコンテンツとしてのWebアプリケーションは軒並み終了を余儀なくされました。この時にバックエンドアプリケーションは、サーバサイドレンダリングを行う大きな理由を失いました。

MVCからレイヤードアーキテクチャーへ

さて、サーバサイドレンダリングという大役を失ったバックエンドアプリケーションですが、まだまだ役割は盛りだくさんです。

  • DBやデータストアとのやりとり
  • ユーザーデータの管理
  • データアクセスに際しての認証・認可
  • ビジネスロジックの実装・実行 などなど…

MVCでいうなら、Viewが空っぽでModelが山盛り、という状態でしょう。あれもModel、これもModel…

さすがに何もかもをModelで済ませていては解像度が低すぎます。また、Controllerについてもどのようなリクエストを期待し、どのようなレスポンスを返すのかが自明ではありません。

  • Model内で副作用の有無が混ざっている
  • Model内で多重継承が目立つ
  • Controller側で期待するリクエストおよびレスポンスが不明確

従来のPerlによるMVCではこのあたりに課題が生じがちでした。そこで私はこの半年ほど、レイヤードアーキテクチャーと関数型の要素を取り入れたアプローチでPerlのアプリケーションを設計・開発しています。

Mojolicious + レイヤードアーキテクチャー的アプローチ

まず、下地となるWebアプリケーションフレームワークにMojoliciousを選びました。充分に枯れていながら依存モジュールが少なく、利用方法がMVCにこだわらない点が素晴らしいです。メンテナンスも熱心に継続されているため、安心感があります。

次に、実際のレイヤードアーキテクチャとは異なりますが、それっぽいコンポーネント設計として以下のような層を設けます。

  • Router : リクエストを適切なロジックに処理させる層。
  • Handler : メッセージデータを受け付けて、レスポンスを返す層。
  • Service : Handlerから呼び出され、副作用を伴うビジネスロジックを抱える層。
  • Domain : HandlerおよびServiceから呼び出され、副作用を伴わないビジネスロジックおよび定数群を抱える層。
  • Message : リクエストおよびレスポンスをメッセージデータ構造に当て込む層。
  • Repository : Serviceから呼び出され、外部データの取得・保存を行う層。
  • Infra : ServiceおよびRepositoryから呼び出され、外部システムとの窓口となる仕組みを抱える層。

図解すると、以下のようになります。

わいとん式PerlレイヤードアーキテクチャR4の図

いったん、「わいとん式Perlレイヤードアーキテクチャ(R4)」と銘打ちました。R4は令和4年の意味です。略称を 「YPLA-R4」 とでもしておきましょうか。

ディレクトリ構成と各パッケージの命名規則

ディレクトリ構成は概ね以下のようなものとなります。

.
├── Makefile
├── app.pl
└── lib
    └── MyProj
        ├── Config.pm
        ├── Domain
        │   ├── CouponDomain.pm
        │   ├── UserCouponDomain.pm
        │   └── UserDomain.pm
        ├── Handler
        │   ├── UserAccountHandler.pm
        │   └── UserCouponHandler.pm
        ├── Infra
        │   ├── Database.pm
        │   ├── HTTPClient.pm
        │   └── Payment.pm
        ├── Message
        │   ├── Request
        │   │   ├── BuyCouponRequest.pm
        │   │   ├── CreateUserAccountRequest.pm
        │   │   ├── CreateUserCouponRequest.pm
        │   │   ├── UpdateUserAccountRequest.pm
        │   │   └── UseUserCouponRequest.pm
        │   └── Response
        │       ├── ErrorResponse.pm
        │       ├── UserAccountResponse.pm
        │       ├── UserCouponListResponse.pm
        │       ├── UserCouponResponse.pm
        │       └── UserListResponse.pm
        ├── Repository
        │   ├── CouponRepository.pm
        │   ├── UserCouponRepository.pm
        │   └── UserRepository.pm
        ├── Router
        │   └── Root.pm
        └── Service
            ├── CouponService.pm
            ├── MyExternalService.pm
            ├── PaymentService.pm
            └── UserAccountService.pm

特徴的なのが、基本的にパッケージ名が自己言及的な名称となっている点でしょう。 MyProj::Service::UserService のように、一目でどの層に属するのかがわかるような命名にするべきです。

Handlerのコード例

すでに見ての通り、このコンポーネント設計ではパッケージ名が結構長いものとなります。

たとえば MyProj::Service::UserServiceMyProj::Service::CouponService などというのはまずまず呼ばれそうなクラスですが、これを毎回打ち込むというのはやっていられないでしょう。

そんな時に便利なのが aliased モジュールです。これをつかうと、たとえば MyProj::Handler::UserCouponHandler で以下のように書くことができます。

package MyProj::Handler::UserCouponHandler
use strictures 2;
use experimental qw(try);
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Types::Common -types;
use Function::Parameters;

use aliased 'MyProj::Service::UserService';
use aliased 'MyProj::Service::CouponService';
use aliased 'MyProj::Service::UserCouponService';
use aliased 'MyProj::Service::PaymentService';

use aliased 'MyProj::Message::Request::BuyCouponRequest';
use aliased 'MyProj::Message::Response::ErrorResponse';
use aliased 'MyProj::Message::Response::UserCouponResponse';

### POST /coupon/buy
fun buy_coupon((InstanceOf ['Mojolicious::Controller']) $c) {

    ## aliasedによって MyProj::Message::Request::BuyCouponRequest は 
    ## BuyCouponRequest とだけ書けばOKとなる。
    my $req = undef;
    try {
        $req = BuyCouponRequest->new_by($c);
    catch ($err) {
        return ErrorResponse->bad_request($c, $err);
    }

    my $user_id   = $req->user_id;
    my $coupon_id = $req->coupon_id;
    
    my $user   = UserService->get($user_id) or 
        return ErrorResponse->bad_request($c, "no such user_id");

    my $coupon = CouponService->get($coupon_id) or
        return ErrorResponse->bad_request($c, "no such coupon_id");

    my $err    = PaymentService->buy_coupon($user, $coupon);
    if ($err) {
        return ErrorResponse->internal_server_error($c, $err->message) if
            $err->code >= 500;
        return ErrorResponse->conflict($c, $err->message);
    }

    my $user_coupon = UserCouponService->create($user, $coupon);
    
    return UserCouponResponse->new_by($user_coupon)->ok($c);
}

...

ただし aliased を導入することによってIDEの支援を受けづらい状態になりますので、留意してください。

Messageのコード例

Message層はRequestとResponseに分けられます。それぞれのコード例を示します。

Message層はすべて Moo ベースのパッケージとして定義されます。

まずはRequestの例として BuyCouponRequest です。

package MyProj::Message::Request::BuyCouponRequest;
use Moo;
use Types::Common -types;
use Function::Parameters;
use namespace::autoclean;

has user_id => (
    is            => 'ro',
    isa           => NonEmptyStr,
    required      => 1,
    documentation => '会員ID',
);

has coupon_id => (
    is            => 'ro',
    isa           => NonEmptyStr,
    required      => 1,
    documentation => 'クーポンID',
);

fun new_by($class, (InstanceOf ['Mojolicious::Controller']) $c) {
    return $class->new(
        user_id    => $c->req->json->{'user_id'},
        coupon_id  => $c->req->json->{'coupon_id'},
    );
}

1;

new_by($c) を定義することで、リクエストを解釈するロジックを統一できます。

次にResponseの例として UserCouponResponse です。

package MyProj::Message::Response::UserCouponResponse;
use Moo;
with 'Moo::Role::ToJSON';
use Types::Common -types;
use Function::Parameters;
use namespace::autoclean;

has user_id => (
    is            => 'ro',
    isa           => NonEmptyStr,
    required      => 1,
    documentation => '会員ID',
);

has coupon_id => (
    is            => 'ro',
    isa           => NonEmptyStr,
    required      => 1,
    documentation => 'クーポンID',
);

has used_at => (
    is            => 'ro',
    isa           => DateTime | Undef,
    required      => 0,
    documentation => '使用日時(null=未使用)',
); 

has created_at => (
    is            => 'ro',
    isa           => DateTime,
    required      => 1,
    documentation => '登録日時(=購入日時)',
);

fun new_by($class, (InstanceOf ['MyProj::Repository::UserCoupon'] $user_coupon)) {
    return $class->new(
        user_id    => $user_coupon->user_id,
        coupon_id  => $user_coupon->coupon_id,
        used_at    => $user_coupon->used_at,
        created_at => $user_coupon->created_at,
    );
}

fun ok($self, (InstanceOf ['Mojolicious::Controller']) $c) {
    return $c->render(
        json   => $self->TO_JSON, 
        status => 200,
    );
}

Responseでも new_by($user_coupon) を定義していますが、レスポンスに必要な値を一気に詰め込むためのクラスメソッドとして定義してあります。

また、Mojoliciousのレスポンスとして返せるように ok($c) をインスタンスメソッドとして定義してあります。

…ちょっとそろそろ書いていて息切れしてきました。全部説明すると分量がだいぶ多くなりますので、ひとまずこのあたりにしておきましょう。

登場した各CPANモジュールについては、以下のリンクからどういうものか見てもらうとよろしいです。

YPLA-R4の設計哲学

ここから先は、このコンポーネント設計の哲学についてちょっとだけ説明します。

関数型のエッセンスを取り入れ、OOPをやりすぎない

ここまで読まれた方の中には「OOPで書けばこんなにコードが冗長になる必要はないのに」と感じた人もいるかもしれません。

基本的にはOOPでもいいんですが、層を切って役割を分けることに意味があると思っています。

その方向性で色々試行錯誤した結果、OOP依存度を下げ名前空間を有効活用することで、パッケージごとの単純性を保つことができそうだという結論に至った次第です。

層ごとの依存先ルールを破らない

たとえばService層ですと、依存してよい先はDomainとRepository、それにInfraという制約があります。また、同一層内での依存は基本的にしないという取り決めでやっていくのがよろしいでしょう。

これは本来のレイヤードアーキテクチャーにおいても、層どうしの依存方向を単一方向にするきまりがあり、そこに倣っています。そうすることで、変更容易性・メンテナンス性を高めていくという狙いがあります。

リクエストとレスポンスの型を定義し、APIとしての明確性を重視する

APIを定義するうえで、OpenAPIのようなフォーマットによる定義書を書く必要性が生じることでしょう。

そうした時に、リクエストとレスポンスの型をMessage層にて定義してあるので、SchemaとしてOpenAPIに記述することもやりやすいかと思います。

将来的にはMoo basedなパッケージからOpenAPIのSchemaを出力するような仕組みなんかも用意したいですね。

ドメインロジックと副作用を徹底的に切り離す

最大のポイントがここで、業務ドメインに関するロジックから副作用を徹底的に取り除き、業務ドメインロジックそのものを副作用のない関数として定義する、という点です。

とにかく業務ロジックにバグを作りたくないので、徹底的に副作用を削り、テストを分厚くするべきです。

そのためにDomain層を設けており、Domainに対するテストがしっかりしていれば(カバレッジで90%以上)、致命的な不具合は回避できるでしょう。

さいごに

だいぶ長いエントリとなってしまったこともあり、最後の方は疲れの片鱗が垣間見えるような内容となってしまいました。

そういえば、この内容と非常に近い話題をこの前の吉祥寺.pm #31@kurotyann9696さんが発表していましたので、ぜひスライドを見ていただくのがよろしいかと思います。

YAPC::Kyoto 2023にていろいろやった話

YAPC::Kyoto 2023 最高でした

わいとんです。

皆さん、YAPC::Kyoto 2023 最高でしたね!

わたしは今回、Gold Sponsor企業の関係者および裏トークMCとしてYAPC::Kyoto 2023に参加させていただきました。起業後初のYAPCでしたが、やはり同窓会という雰囲気があり、わたしもまたPerl Mongerなのだな、という実感がわきました。

大井町.pmの宣伝動画をつくった

YAPC::Kyoto 2023の会場に入った皆さんなら「あぁー、あれね!」と言われること請け合いかと思いますが、↓の動画を作ったのはわたしです。

この動画の裏話は、もりたつ技商の@myfinder(まいんだー)さんがPerl Sponsorになったことから話が始まります。

まいんだーさんからある日「パールスポンサーは幕間CM動画を出せるらしい」「特に宣伝することもないしどうしよう」という相談を受け、あれこれ話しているうちに「じゃあ大井町.pmの宣伝しちゃう?」ということになってしまったのです。大井町のおじさん達による悪ノリのはじまりです。

動画の流れを色々話し合った結果、まいんだーさん若しくはわたしが何かをいう度に@soudai(そーだい)さんが「それな」「わかるー」という反応をしまくるというものにしようか、となったのですが、いざ蓋をあけてみたところ、あのような形に…

そして収録のために、溝の口のお好み焼き屋さんにて大井町.pmを開催したのですが、およそ2時間程度飲んだ食ったした後に「そろそろエンジンがあったまってきたな?」ということで写真撮影&音声収録を行ったという流れです。悪ノリ、どんどん加速していきます。

そして編集はわたしが担当することになりました。素材の映像・写真・音声を色々吟味していくうちに、「〇曜どうでしょう」っぽい雰囲気をまとわせると面白くなるだろう、と直感し、制作。

どうでしょうっぽいアレ

しかし、実際に一番時間と手間がかかったのは、一番最後の協賛企業表示でした。

GIMP2でこの画像をつくったんですけど、昭和のテレビ特有のシャドー現象や青色部分に多少でてしまう色ムラなどを表現するのに結構苦戦したんですよ。

協賛企業表示のアレ

で、まず会場のオーロラビジョンでこれが流れている様子を見たとき、「あ、ホントにやっちゃったんだw」と思わず口走ってしまったくらいには驚きました。

いや、事前に承知してたんですよ、そんなこと。でも実際に見てみると、200人以上が集まっているところでこの動画が流れているのはだいぶ面白い状況だなって思ったんですよね。

驚いた…

スペシャルサンクスとして@bonnuさんを挙げていますが、これは音声収録を@bonnuさんにお願いしており、素材として提供していただいたためです(大井町pmのslackにて受け取りを行いました)。@bonnuさん、ありがとうございました。

フェイスタオルを配った

弊社もGold Sponsorということもあって、ノベルティ配布ができるということでした。

京都といえば銭湯・朝湯だろうということで、配布するものはフェイスタオルにしたのですが、これがなかなか嵩張る。300枚も発注したので、まだ社内に30枚ほど在庫が残っているんですよ。誰かもらってあげてください。

そして、そのフェイスタオルは畳まれていない状態で届いたものだから、これまた大変。毎日すこしずつ心を込めてフェイスタオルを畳む日々が数日ほど続いたのです。

タオルを畳む日々

しかし、当日は配布用の枚数もそれなりに余ってしまったので、急遽現地でタオル配りおじさん業を敢行したのでした。まぁ面白おかしく配布できたので、ひとまず良かったと思っています。

裏トークでMCをやった

裏トークという企画、ここ数年のYAPCでは特にオンラインにおけるにぎやかしということでやってきたのですが、今年はオフライン、しかもYouTubeで我々の様子がうかがい知れる状態での敢行でした。

オンラインで自分らの様子が流れることについては特に気にならなかったんですが、オフラインイベントということもあって、様々な方が飛び入り参加するというスタイルが斬新かつ面白かったです。@dankogaiさんや@motemenさん、@kazeburoさん、@mackee_w(まこぴー)さん、@uzullaさん、@bonnuさん、@__papix__さん、@karupaneruraさん、その他多数の方に飛び入り参加していただきました。本当にありがとうございます。

1点、反省点というか何とかできないものか、と思う点として、我々MC陣がなかなかブースを離れられないため、もっとたくさんの人たちとコミュニケーションをとりたかったなーという思いがあります。

ちなみにわたしが個人的に好きだなーとなったトーク、本編では@mackee_w(まこぴー)さんの「デプロイ今昔物語 〜CGIからサーバーレスまで〜 」がとても良かったと思いますし、@moznionさんの「ソフトウェアエンジニアリングサバイバルガイド: 廃墟を直す、廃墟を出る、廃墟を壊す、あるいは廃墟に暮らす、廃墟に死す」は弊社若手エンジニアのお気に入りだったようでした。前日祭のRejectConでは@codehexさんの「Customer Experience Journey in NOT A HOTEL」がぶっ刺さりましたね。

学生さんと話した

弊社、なんと学生旅費支援プログラムのスポンサーもやっていました。そのため、学生さんとランチ休憩の時間帯にお話をする企画に参加しており、まぁなんというか、非常に雑な会社紹介LTをさせていただきました。

また、学生さんたちの質問に答えるビンゴ企画というものもあり、こちらではわたしの飾らない率直な回答をさせていただきました。

なかなか学生さんと語らう機会は少ないですので、こちらとしても学生さんが気にしている点等、大変勉強となりました。

前日祭LTバトルの査読をやった

うちの会社の@mini_bg_pro_N(あきた)さんが前日祭LTバトルで東側MVCを受賞したんですが、その査読をさせていただきました。

その中で、Perlに関する感想をが書いてあるスライドがあり、TIOVEのあのグラフを引用していたので、あきたさんには「ここを少しだけ説明したら飛ばすと笑いが取れそう」とアドバイス。

そうしたところ、あきたさん、適宜「飛ばし芸」を入れると面白いと悟ったのか、本番では彼自身の判断で時間が足りなくなる可能性も考慮し、Expressの解説も飛ばしてもうひと笑いを取るという職人技をやってのけたのでした。凄い。

世界一の笑顔を撮った

最高の朝食を食べた

さいごに

@ar_tama(あらたま)さん、ベストスピーカー賞おめでとうございます。わたしはトークを聴くことができなかったので、アーカイブが出たら絶対見るぞ!という気持ちで心待ちにしております。

そしてスタッフの皆さん、本当にお疲れさまでした&ありがとうございます。終わった後、スタッフの人たちともコミュニケーションをとる機会が得られたのはとても良かったです。

裏トークMCの仲間である@myfinderさんと@soudaiさん、お疲れさまでした。およそ12時間にわたる長丁場を我々は全部楽しんで過ごしましたね。それだけやったら、そりゃまぁ疲れると思いますので、風呂でも入ってしっかり体力回復しましょう。ついでに弊社のタオルも使ってやってくださいw

そして最後に、@__papix__さんと@azumakuniyukiさん、本当にお疲れさまでした。3年ぶりにオフライン開催のYAPCが復活できたのは、紛れもなくお二方の不屈の精神のおかげです。ありがとう。