Scala3でマークダウンファイルを入力としてChatGPT APIを利用するツールを作った話
ドワンゴのN予備校という教育サービスでプログラミング講師をしている @sifue といいます。N高等学校/S高等学校のプログラミング講師もしており、学内のツール開発や運用などもしたりしています。
最近は生成AIが流行ったこともあって、Pythonだったり、UIが必要なものはどうしてもTypeScriptとReactで実装することも多いのですが、久しぶりにScalaを使っての開発をしてみました。
自身は、Scalaでの開発はニコニコ生放送のサービスを開発するときに使っていた他、N予備校内で提供している大規模Webアプリの教材やドワンゴが当初作成していたScalaテキストの作成などにも関わらせてもらいました。
その当時のScalaは2.12であったわけなのですが、その後2.13が出て、さらに今はScala3系になって3.3.1までバージョンが進み、開発環境が変わってすごく使い勝手がよくなったということもあってキャッチアップも兼ねて簡単なツールを作ってみることにしました。
今回作ったツールについて
すでに自身はChatGPTをSlackボットで使うChatgpt SlackbotというPython製のツールなどを公開して、ありがたいことにスターを50以上もらって運用していたりするのですが、入力出力をマークダウンで確認しながらChatGPTのAPIを使いたいと言うこともあり、今回マークダウンファイルを入力にできるChatGPTのコマンドラインツールを作ってみました。
ソースコードは、 sifue/gptmd にて公開しています。
また実行可能jar(Uber jar)の配布も行っていますので、OpenAIのAPIのキーを取得して、 config.yml と history.md さえ設置すれば
config.yml:
openai_api_key : sk-99999999999999999999999999999999999999
history_file: ./history.md
chatgpt_config:
model: gpt-3.5-turbo # gpt-3.5-turbo or gpt-4
max_tokens: 1000
temperature: 1
top_p: 1
timeout: 300 # seconds
history.md:
<!-- gptmd-system-begin -->
You are ChatGPT, a large language model trained by OpenAI.
Carefully heed the user's instructions.
Respond using Markdown in Japanese.
<!-- gptmd-system-end -->
<!-- gptmd-user-begin -->
ジョークを5個つ。以下のマークダウンの表の形式で教えてください。
| 質問 | 答え |
| --- | --- |
| {質問の内容} | {答えの内容} |
<!-- gptmd-user-end -->
このようなファイルをjarファイルと同じフォルダに置き、
java -jar gptmd-assembly-1.0.1.jar
のようなコマンドでJavaをインストールした環境でChatGPTのAPIをマークダンファイルを入力に実行することができるようになっています。
Scala3の開発環境構築
Scala2時代ははOpenJDK入れて、sbt入れて、といろいろ面倒だったのですが、最近では、Getting Started | Scala Documentation に沿って、Coursier(コーシル - フランス語で宅急便の意)のみをインストールし、セットアップコマンドを実行するだけで全てよしなにScala3の環境を構築してくれます。すごい。
macOSの場合、
brew install coursier/formulas/coursier && cs setup
で全ての環境が構築され、あとはScalaコマンドだけで
のようにREPL(対話実行環境)が利用できます。めちゃくちゃよくなってる。
VS CodeのMetalsでの開発体験
Scala2時代のJVMの開発といえばIntelliJ IDEAやEclipseで開発することも多かったのですが、今はVS Codeの方が軽量で便利になってきたと言うことで、VS Codeで構築しました。VS CodeでのScala開発の体験がすごく良いという記事は英語でも日本語でもみかけます。
VS Codeでは、Scala Metalsという言語サーバーがsbt経由でビルドしてくれると言うこともあって、IntelliJ IDEAなどに比べて驚くほど高速です。MetalsはIDEでよくある
- 補完
- インクリメンタルビルドと実行
- 定義に移動
- インポートの自動化
- ドキュメントやシグネチャのホバー表示
- 参照元表示
- ファジー検索
- クイックフィックス
- ブレークポイント付きデバッグ実行
このようなものに全て対応してくれるにも関わらず非常に軽量。
build.sbtを変更した時だけは流石にインポートに時間がかかりますが、とにかく実行が脅威的な速さです。
sbt runコマンドで実行する時はJVMやsbt自身の起動もあり、sbt new scala/scala3.g8
で作るテンプレートのhello worldですら
というわけで6秒もかかるわけなのですが、Metalsなら、@mainの上にあるrunボタンを押すだけで0.1秒ぐらいで実行されます。(あまりに高速すぎて録画できませんでした)
実行が6秒→0.1秒になるのが本当に革命的です。なお、これはある程度大きいプロジェクトになっていてもつねにMetalsが立ち上がっていると言うこともあってすごく快適に開発、デバッグすることができます。
これでScalaは関数型でJavaのライブラリも使えてすごく良いけどビルドが遅くて...みたいな人でもかなり開発しやすいのではないかなと思います。
Scala2→Scala3で変わった機能
Scala3で導入された機能は、Scala 3 Referenceに全てまとまっているのですが、本当にざっくりまとめると
- 交差型 (TypeScriptでの&、Scala3でもだいたい一緒)
- ユニオン型 (TypeScriptでの|、Scala3でもだいたい一緒)
- 型ラムダ (型パラメーターをかける無名型定義みたいなの、Haskellで書いたりするやつ)
- implicitを使わないで済む様々な機能 (Using句のローンパターン、 given句で型のコンテキストの標準値の設定、extension句での型変換など)
- ブレイスの代わりにインデントでも書ける機能(Pythonのような見た目になる。Optional Braces という)
- newが不要になったこと
- ワイルドカード型が_から?へ変更(Javaと一緒に)
これがかなりざっくりとしたまとめですが...普通のプログラマにはこの辺りが影響が大きいかなと思います。細かくみると特にメタプログラミングあたりは大きく変わっている印象を受けましたが、一般的なアプリケーション開発者は触れることは少なさそうです。なお、公式のNEW IN SCALA 3にも日本語でもっと正確にまとめてありますのでそちらを参照ください。
実際のScala3のコード
というわけで、import部分は端折りながら実際Scala 3で書くとどんな感じかと言うのを紹介します。
Mainクラスがシンプルに
@main def main: Unit =
val configLoader = ConfigLoader()
val config = configLoader.load()
val service = ChatGPTService(config)
service.chat()
service.shutdown()
メインクラスがこんなに短くなります! 最近のJavaでpublic static void main(String[] args)がなくなったよという記事が話題になって、
void main(String[] args){
System.out.println("hoge");
}
こうなるよ言われていますが、Scala3では、
@main def main = println("hoge")
同様のコードはこうなります。シンプルになりすぎて笑いました。 @main アノテーション素晴らしいです。
設定の読み込みのコード
これは config.yml を読み込んでcase classに入れ込む実装です。これもcirce-yamlと言うライブラリを使ってめちゃくちゃシンプルにかけます。
case class ChatGptConfig(
model: String,
max_tokens: Int,
temperature: Double,
top_p: Double,
timeout: Int
)
case class Config(
openai_api_key: String,
history_file: String,
chatgpt_config: ChatGptConfig
)
class ConfigLoader:
def load(): Config =
Using(Source.fromFile("config.yml")) { source =>
val json = parser.parse(source.mkString)
json
.leftMap(err => err: Error)
.flatMap(_.as[Config])
.valueOr(throw _)
}.getOrElse(throw new Exception("config.yml is not found"))
Using句を使ってファイルディスクリプターを閉じている他、 .flatMap(_.as[Config])
だけで、読み込まれてきたYMLがサクッと入れ子構造のcase classに収まるのは感動ですね。
最後に
今回Scala3を書いてみて思ったのが
- Metalsのおかげで立ち上げ、実行までが早くて開発体験がすこぶる良い
- Scalaの難所と言われていたimplicitを書かない選択肢ができた
- Pythonのようにインデントで書けるのでコードがすっきり見えるようになった
というのが感想です。Scala3素晴らしい。
最近Scala3は、「なっとく!関数型プログラミング」の出版で、関数型プログラミングの入門として評価されていると感じていますが、自身も手続型、オブジェクト指向から地続きで関数型プログラミング言語を知れる素晴らしい言語だと思います。また並行処理にも強いのでその部分も学べるのも大きいです。
Scalaは関数型で書けると言うこともあって、すっきりシンプルに書けるので大規模開発でメンテナンスするコード量を減らすのにすごく有能だと思っていますので、今後も大規模開発の選択肢の一つや関数型プログラミングの入り口の一つとして大きなバリューを発揮し続けるのではないかと思っています。特に今まであだとなっていた
- ビルドの遅さ
- implicitの魔
ここが解決したのがすごく大きいと思っています。みなさんもぜひScala3にチャレンジしてみてください。
なお、Scala3の極意書は、Scalaの作者Martin Odersky先生(Javaのジェネリクスを実装した先生)の本(いわゆるコップ本の元本)、
この2冊なので、まだ日本語が出ていませんが時間を取って読んでみたいなと思っています(もしかしたらどこかでコップ本の第5版が用意されているのかもしれませんが)。
Discussion