feature image

2024年3月13日 | ブログ記事

いきなりScalaを書いた

これは新歓ブログリレー2024 5日目の記事です。少し遅れてしまいました...

やったこと

「知らない言語でtraQ BOT作るの楽しそうだな」と思ったので急に作りました。

GitHub - H1rono/BOT_Tartaglia: 俺が払うよ
俺が払うよ. Contribute to H1rono/BOT_Tartaglia development by creating an account on GitHub.
リポジトリ

出来上がったものがこちら

原神に出てくるタルタリヤというキャラクターの名台詞、「俺が払うよ」を発するだけのBOTです。

orega-harauyo

本当にこれだけです。

書くこと

この記事では、いきなりScalaを使ってやったこと、感じたことをつらつらと書いていきます。なお、私は(自称)Rustaceanなので使用する語彙は大体Rustの文脈でよく使われるものです。

環境構築

Nix Flakeのセットアップ

まずはやはりこれですね。この時点でformatterやLSPなども調査をして、以下の構成を組みました。

該当commit: ec19b90

あと、overlayを設定したのに効いていないことに後で気づいて直しました。958ec62

れっつえすびーてぃー

雑にsbt initして一番基礎的に見えるテンプレを選びました。

GitHub - scala/scala-seed.g8: Giter8 template for a simple hello world app in Scala.
Giter8 template for a simple hello world app in Scala. - scala/scala-seed.g8
そう、これこれ

んですが、最初からテストが用意されてるんですね。あと、ディレクトリが深い。src/main/scala/pkg-name/Main.scalaってなんだよ!!!!

該当commit: 9ac7955

metals導入

rustだったらrust-toolchain.tomlにrust-anaylzerの存在を記述して終わりだったんですが、metalsの場合はそう簡単な問題ではありませんでした。

というのも、build backendとしてbloopやsbtなどが選べるんですね。デフォルトはbloopでしたが余計なdepsはつけたくないのでsbtにしようと試みました。が、sbtに切り替えてみてもprojects/metals.sbt(metalsが生成するsbtの設定ファイル)にbloopの依存関係が記述されるので、諦めてbloopを採用しました。

該当commit: b84fca8

ところでこのmetalsの出力がとんでもないんですよね。↓はlsd --treeの出力から一部抜き取ったものです。

BOT_Tartaglia
├── .bloop
├── project
│   ├── .bloop
│   ├── project
│   │   ├── .bloop
│   │   ├── project
│   │   │   ├── project
│   │   │   │   └── target
│   │   │   │       └── config-classes
│   │   │   └── target
│   │   │       ├── config-classes
│   │   │       ├── scala-2.12
│   │   │       └── streams
│   │   └── target
│   │       ├── config-classes
│   │       ├── scala-2.12
│   │       └── streams
│   └── target
│       ├── config-classes
│       ├── scala-2.12
│       └── streams
└── target
    ├── global-logging
    ├── scala-2.12
    ├── streams
    └── task-temp-directory

metalsのドキュメント通りに設定したはずなんですが、何か間違えたかな...

formatterとlinter

これなしでは始まりませんね。rustfmtとclippy, ではなくscalafmtとscalafixを導入しました。設定ファイルはそれぞれ.scalafmt.conf, .scalafix.confを使用し、HOCONというフォーマットで記述します。特殊文法だあ...

scalafmtの導入commit: 0cd9023
scalafixの導入commit: b48e6dd

scalafmtの導入で知ったんですが、Scalaはバージョンごとにdialectと呼ばれるほどの差異があるっぽいですね。

dialectと表現してるのはscalafmtだけかも?まあということで、Scalaのバージョン問題が発生

ところで

sbtはrustup + cargo? と思った話です。

sbt initで生成されたものの中で、build.sbtに以下のような記述があります。

ThisBuild / scalaVersion     := "2.13.12"

つまり、sbtはscalaのバージョンも管理している...? sbtが提供するのはプロジェクトでの依存ライブラリ管理とそれらをまとめてビルドする方法のみだ、という思い込みがあったのでこれは予想外でした。cargoだけじゃなくてrustupもこなせるのね、という印象です。

Scalaのバージョンどうする

Configuration · Scalafmt #Scala Dialects より


Available dialects are:

さて、どれにしましょうか。とりあえずsbt系はよくわからんので除外して、scala211, scala212, scala213, scala3のいずれかで考えます。とりあえずGitHubリポジトリを見にいきます。

scala3 a.k.a. dotty!? わけわからん... というわけでとりあえず除外。

Nixを使用している関係上、nixpkgsでscala 2.xの中でもxを陽に指定できるものを選びたいですね。ということでNixOS Searchを覗いてみます。

dialectとNixパッケージ名の対応をまとめると↓です。

dialect パッケージ名
scala211 scala_2_11
scala212 scala_2_12
scala213 scala

ということで採用: scala212

該当commit: ac41f78, e816db3

環境構築終わり

ようやくScalaを書けます。17時に始めてここまでで大体23時です。長かった...

何作るんだっけ

Scalaの環境構築が予想外に重たかったので、ここでもう一度作るものを確認しておきます。

HTTPモードのtraQ BOTサーバーを作ります。詳細な説明はBOT Consoleにあります。

traQ-bot-console/src/docs/bot/http-server.md at 15eb1e050d30bc4a7b1440b0307957cf92d4f4c9 · traPtitech/traQ-bot-console
Contribute to traPtitech/traQ-bot-console development by creating an account on GitHub.

↑などの内容を要約するとこうです。

つまり、BOT_Tartagliaには以下のような実装が必要です。

作るべきものがわかったので、ようやく本格的に書き始めることができます。

Let's write Scala

play framework vs ...

さてまずはお決まりのライブラリ選定ですね。今回作るのはフロントエンドもDBもないHTTPサーバーなので、大それた機能は必要ありません。といったことを考えながらplay-samplesを覗いてみましたが、どうにも高機能すぎる気がします。HTTPクライアントを別で用意する必要がありそうなのも微妙なところです。

...といったことを考えながらGoogle検索を繰り返していたら、http4sの紹介記事を見つけました。

Scala の http クライアントを紹介する

http4s は typelevel エコシステムの シンプルな http(サーバー、クライアント)ライブラリです

Rustでいうところのhyperのようなものでしょうか。HTTPサーバーもクライアントもこれ一つで事足りるのは嬉しいので、今回はこのライブラリを採用しました。

該当commit: 0515724

ちなみに、他にもHTTPサーバーのフレームワークは存在するようですね。

Play framework to build application with no UI and need to accept requests using REST and ipc and/or message queues
I have to build a component that runs in a jvm, uses MongoDB as database and doesn’t need a UI. It will be integrated into other products. I’m planning to build this using scala and related tools.…

exampleをコピペ

http4s.g8/src/main/g8 at 30a5cb5f60345e7d51d7cc6c141badee3483ff95 · http4s/http4s.g8
giter8 template for bootstrapping http4s services. Contribute to http4s/http4s.g8 development by creating an account on GitHub.
コピペ元のテンプレート
// https://github.com/http4s/http4s.g8/blob/30a5cb5f60345e7d51d7cc6c141badee3483ff95/src/main/g8/src/main/scala/%24package__packaged%24/%24name__Camel%24Server.scala#L14-L39
// 一部改変

  def run[F[_]: Async: Network]: F[Nothing] = {
    for {
      client <- EmberClientBuilder.default[F].build
      helloWorldAlg = HelloWorld.impl[F]
      jokeAlg = Jokes.impl[F](client)

      // Combine Service Routes into an HttpApp.
      // Can also be done via a Router if you
      // want to extract segments not checked
      // in the underlying routes.
      httpApp = (
        Routes.helloWorldRoutes[F](helloWorldAlg) <+>
        Routes.jokeRoutes[F](jokeAlg)
      ).orNotFound

      // With Middlewares in place
      finalHttpApp = Logger.httpApp(true, true)(httpApp)

      _ <- 
        EmberServerBuilder.default[F]
          .withHost(ipv4"0.0.0.0")
          .withPort(port"8080")
          .withHttpApp(finalHttpApp)
          .build
    } yield ()
  }.useForever

わけわからんfor式ですね。<-ってなんやねん!!!!とりあえずクライアント部分は消してHelloWorldRoutesだけ持ってこようと頑張ったのですが、それだけで1時間ほどを要してしまいました。

該当commit: 518c48d

forってなんだ

これがわからないと進まないと思ったので早めに潰します。色々ggったら↓を見つけました。

Scalaのfor式の使い方を解説
【Scalapedia】Scalaのfor式の使い方を解説
Scalaのfor式による関数合成を「線路」で理解する
「for式による関数合成」は、Scalaを学び始めた方にとって難しく感じられるトピックの1つなのではないでしょうか。 この記事では、Scala初学者の方や教育を行う方を対象に、for式による関数合成を「

ここからの解説は全くの私見であるため、何かしら間違っている可能性があります。:pray:

まず、<-.mapの糖衣構文的に束縛が行われます。<-が複数登場した場合は直積的に展開され、場合に応じて.flattenないし.flatMapの糖衣ともなる場合があります。

scala> val names = Seq("Scala", "H1rono K")
names: Seq[String] = List(Scala, H1rono K)

scala> for {
     |   name <- names
     |   n <- name.split(" ")
     | } yield s"Hello, $n!"
res0: Seq[String] = List(Hello, Scala!, Hello, H1rono!, Hello, K!)

これはSeq[_]だけではなくOption[_]Either[E, _]などのF[_]の形を取るもので、.map, .flatMap, etcを適用できるか否かによってfor式で使える構文の幅が変わります。

このF[_]にはMonadなりFunctorなりの適切な名称があるのでしょうが、私はそういった議論に疎いためここではF[_]と一貫して呼びます

scala> for {
     |   home <- sys.env.get("HOME")
     |   hoge <- sys.env.get("HOGE")
     | } yield s"HOME=$home; HOGE=$hoge"
res0: Option[String] = None

scala> for {
     |   home <- sys.env.get("HOME")
     |   user <- sys.env.get("USER")
     | } yield s"HOME=$home; USER=$user"
res1: Option[String] = Some(HOME=/Users/kh; USER=kh)

これはRustの?演算子(またはtry!マクロ)が対応しているように見えます。

fn env_info() -> Option<String> {
    use std::env::var;
    let home = var("HOME")?;
    let hoge = var("HOGE")?;
    Some(format!("HOME={home}; HOGE={hoge}"))
}

なるほど、ここまでわかると結構使える気になりますね。

cats

このF[_]をIOなどの副作用にも適用したのがCats Effectライブラリです。

Scalaのcatsについて知っておきたい9つのtips(和訳記事) - Qiita
はじめにScalaでの開発でcatsを使っているおり、使えそうなので和訳しました。(ただしところどころ日本語的に訳すのが難しい場合は、訳さなくても理解するのに影響がなければ無視し、それ以外は意訳や…
Cats EffectのIO入門 - Qiita
Cats Effect: IOというCats Effectが提供する公式ドキュメントを参考にCats EffectのIOモナドを触ってみたので、その内容をベースにIOモナドについて書いてみました。…

どういうことかと言うと、printlnをすんなり書けなくなります。

scala> import cats.effect._
import cats.effect._

scala> val proc: IO[Unit] = for {
     |   home <- std.Env[IO].get("HOME")
     |   _ <- std.Console[IO].println(s"HOME=$home")
     | } yield ()
proc: cats.effect.IO[Unit] = IO(...)

scala> import cats.effect.unsafe.implicits.global
import cats.effect.unsafe.implicits.global

scala> proc.unsafeRunSync()
HOME=Some(/Users/kh)

損では?と思ったかもしれませんが、この制限が加わることでF[_]で包まれていない文脈では副作用が発生しないことが保証されます。

http4sはtypelevelエコシステムのライブラリなので、もちろんcats effectに依存しています。ここまできてようやく、「送信されたリクエストをstdoutにダンプするエンドポイント」といったものも作れるようになりました。

trait DumpReq[F[_]] {
  def dump(req: DumpReq.Req[F]): F[Status]
}

object DumpReq {
  final case class Req[F[_]](req: Request[F]) extends AnyVal

  def impl[F[_]: Async: Console]: DumpReq[F] = new DumpReq[F] {
    def dump(req: Req[F]): F[Status] = for {
      _ <- Console[F].println(req.req)
      body <- req.req.as[String]
      _ <- Console[F].println(body)
    } yield Status.NoContent
  }
}

該当commit: 991eab4

Eitherを入れたい

ここまでで副作用をF[_]の形で包むことができ、包まれた文脈での操作もはfor式で簡潔に記述できるとわかりました。このFには実際はIOという型が当てはまります。これはプログラムのエントリポイントで与えられています。

// https://github.com/H1rono/BOT_Tartaglia/blob/991eab4beb617fd33ff783f7393c7ed19aeb266d/src/main/scala/h1rono/Main.scala#L7

  val run = BotServer.run[IO]

ところで、Either[E, _]Option[_]F[_]の一種です。RustでResult, Optionに慣れた身としては寧ろこちらの方を積極的に使っていきたいところです。しかし、F[_]Fは既にIOが占有してしまっています。どうすればいいのか?

Composing multiple different monad types in a for-comprehension
Previous Title: Composing DBIOs in for-comprehension I don’t understand, why the following code does not even compile. What I Want To Do / Context For each entry in a list of ticket sale entries…

In your case, if you want to keep the whole thing in a for comprehension, you can try the OptionT monad transformer from Cats: https://typelevel.org/cats/datatypes/optiont.html.

ということで、これもcats製のライブラリが回答を用意していました。ありがとう...

該当commit: 07ef930

雑にthrowなどしていたところがOptionなりEitherなりで包まれて、かなりいい感じになりました。

今何の文脈だっけ?

ここまででF[_]FにはIO, EitherT, OptionTのいずれかを使用する場面のみ紹介してきました。しかし、実はもう一人Fに当てはまる役者が存在したのです。その名も...

Resource

これは何かというと、HTTPサーバー・クライアントを作成(ビルド)する際に包まれるやつです。つまり、exampleコードで紹介した部分です。

  def run[F[_]: Async: Network]: F[Nothing] = {
    for {
      // Resource[IO, Client] のような型が返され、
      // Resource[_, Client] が F[_] に対応する
      client <- EmberClientBuilder.default[F].build

しかし型引数のFにはIOが与えられており、for式で包まれているものと辻褄が合いません。何が暗躍しているのか?

    for {
      ...
    } yield ()
  }.useForever

.useForeverお前か!!!!ということに気が付かずに時間を溶かしました。何も考えずに読み書きすると型が見えなくなってしまいますね。

該当commit: 5b4c976

そしてついに

俺が払うよ · H1rono/BOT_Tartaglia@c2f400b
俺が払うよ. Contribute to H1rono/BOT_Tartaglia development by creating an account on GitHub.
完成!!!!

ここまで長かった... 調べ始めてからこのcommitまで、休みを挟みつつ約2日かかりました。

その他困りごと

ここまでで一応やりたいことはできましたが、道中で色々と困ったので記しておきます。誰か助けてください。

マクロが使えない

BOT_Tartaglia/src/main/scala/h1rono/TraqClient.scala at 958ec62 · H1rono/BOT_Tartaglia #L69

      Uri.fromString(s"https://q.trap.jp/api/v3$path").toOption.get

ここでuriマクロを使いたかったんですが、何故かできませんでした。

scala-native-packager動いてない

BOT_Tartaglia/project/plugins.sbt at 958ec62 · H1rono/BOT_Tartaglia #L3

addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16")

この1行を書くだけでsbt universal:packageBinでパッケージのビルドができるようになるって聞いたんですが何か足りてないんですかね。リポジトリのREADME読んでないのが悪い、はい...

nixでパッケージビルド

とりあえずflake.nixにはdevShellのみ用意しましたが、パッケージビルドはどうやるんでしょうか?typelevel/typelevel-nixとかを使うのかな?

なんかレスポンスが遅い

orega-harauyo2

返事1つだけなのに、体感で30秒ほどかかっています。本当に謎

感想

scala, catsを通して関数型言語の本気に少し触れられたような気がします。途中あまりにもわからなくて、関数型言語もわからないのにプログラマーを自称することなどとても許されないと感じました。春休みも残り半分を切りましたが、Haskellの勉強をやってみようかなと思います。それと、終始GitHubのコード検索に助けられました。

最後に、ここまでで開いたタブ一覧(残っているもの)を置いておきます。


明日の担当は@hijiki51さんと@ankoさんです。お楽しみに!

H1rono_K icon
この記事を書いた人
H1rono_K

趣味プログラマー(大学生)

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2024年3月22日
traPグラフィック班の活動紹介2024
haru10 icon haru10
2023年7月15日
2023 春ハッカソン 06班 stamProlog
H1rono_K icon H1rono_K
2024年3月17日
⑨でもわかる8bitアレンジ講習会
vPhos icon vPhos
2024年3月11日
思想の強いゲーム制作をしよう!
Kirby0717 icon Kirby0717
2023年9月13日
ブログリレーを支えるリマインダー
H1rono_K icon H1rono_K
2024年3月25日
ちょっとわかる!!!!!【Web Speed Hackathon2024 参加記】
mehm8128 icon mehm8128
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記