🥰

JVM言語未経験者向けの Scala 入門

2022/03/10に公開

JS, Ruby, Python や golang など他の言語をそれなりに書いているけれど Scala(や JVM)を触ったことのない人向けの Scala のはじめかた.

最低限のセットアップ

  • Java 8 以上をインストール

Scala のアプリケーション・ライブラリは Java (JRE) があれば実行することができます. brew なり apt なりで所望の Java をインスコしましょう.

VCS で管理する必要のないファイル

次のファイル、ディレクトリはビルド時に生成されるものやLSPの設定ファイルなのでVCS で管理する必要はありません.

**/target
**/.bloop
**/.bsp
.bsp
project/project
project/metals.sbt

コードリーディングにオススメのプロジェクト

ある程度コードを書いたことがあるなら制御構文などはすぐに頭に入るはずなので、まずはコードリーディングからはじめてみるといいかもしれません.

Scala のライブラリの中でも cats 系のライブラリや zio 系のライブラリは関数型の思想が強いので他の言語、特にオブジェクト指向の考えが強い言語から入ってくる人にはあまりおすすめできません.

com-lihaoyi にホストされているプロジェクトはソースコードの量もそこまで多くなく Python のライブラリのインターフェースとよく似たインターフェースのライブラリが多いのでまずはここにあるライブラリのコードを読んでみるといいでしょう.

https://github.com/com-lihaoyi

virtuslab の scala-yaml も分量・複雑さがちょうどいいです.

https://github.com/VirtusLab/scala-yaml

ある程度 Scala のコードに慣れてきたら scalameta にホストされているライブラリを読むのがおすすめです. 特に Scala の language server の実装である metals はコード量はやや多いですが素直なコードで書かれているのでそれなりに分量のあるコードを読んでみる時間があるなら挑戦してみてください.
https://github.com/scalameta

スクリプティング

Scala はコンパイル言語なのでスクリプティングができない、あるいは難しいというイメージを持っている人がいるかもしれませんが、実際はスクリプティングも快適にできます. スクリプティングには .sc または *.worksheet.sc の拡張子のファイルと ammonite というライブラリを使います. Python や Ruby とほとんど変わらない感覚でシュッとコードを書いて動かせます.

セットアップ

  • ammonite をインストールする
  • sc ファイルを作る

まず ammonite をインストールするために coursier をインストールします.

curl -fL https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod +x cs
./cs setup
cs install ammonite

これで準備は OK です.
ターミナルで amm と入力すると ammonite の REPL が起動します.

exit と入力していったん REPL を出ます.

次のようなファイルを作成します.

hello.sc
@main
def greet() = println("Hello, World!")

これを amm コマンドに渡してみましょう. Helllo, World! と表示されるはずです.

amm hello.sc
Compiling hello.sc
Hello, World!

LSP を使う

スクリプティングにも補完やコンパイルエラーが欲しい人はVS Code の拡張機能 Metals をインスコしましょう.

VS Code の Metals という Scala の LSP の拡張機能をインストールしてあれば、*.sc ファイルを開いたタイミングで自動で ammonite をセットアップしてくれます. LSP による補完やコンパイルエラーの表示もちゃんと機能します.

スクリプトへライブラリの追加する

Python ライクなインターフェースで I/O を操作できる oslib と request-scala をインストールしてみましょう.

Scala や JVM のライブラリは (ネームスペース,ライブラリ名,バージョン) で指定します.

foo.sc
import $ivy.`com.lihaoyi::os-lib:0.8.0`
import $ivy.`com.lihaoyi::requests:0.7.0`
// 以下略

使い方はそれぞれのライブラリの README.md をざっと読んでみるといいでしょう.

プロジェクトの設定と慣習

ビルドツールの役割

JVM言語ではビルドツールを使うのが一般的ですが、Python, Ruby, JS などを使う開発者にはなじみがないかもしれません. 指定したバージョンに対応するコンパイラやビルドツールは依存するライブラリのダウンロード, ソースコードのコンパイル, テストやリリースなどのタスク定義と実行,etc を管理するための道具です.
正確ではないですが、JS ユーザーなら npm + webpack などのツールチェーンがまとまったもの, Rust ユーザーなら cargo と build.rs がまとまったもの, と考えてもらってもいいかもしれません.

Java では gradle や maven が利用されますが、 Scala では sbt がデファクトのビルドツールです. 公式ドキュメントのインストールの手順に従ってインスコしましょう.
https://www.scala-sbt.org/download.html

慣習

以下では ビルドツールとして sbt を使うことを前提に話をすすめます.

Scala のプロジェクトでは一般的に src/main/scala 以下に Scala のソースコードを, src/main/resources に設定ファイルなどの静的アセットを配置します. また、src/test/scala, src/test/resources にテスト用のソースコードとアセットを、src/main/java 以下に Java のソースコードを配置します. ライブラリのネームスペースと対応するように src/main/scala/namespace/libraryname/ 以下にソースコードを置くケースもあります.

Java では 1 ファイル 1 クラスという構成にすることが多いようですが Scala の場合モジュールの粒度はさまざまで、一つのファイルに複数のクラスが存在するケースも多々あります. また Scala 3 からはトップレベルでの関数定義ができるようになりより柔軟性が増しました.

マルチモジュール構成の場合は moduleA/src/main/scala, moduleB/src/main/scala という風にそれぞれのモジュール用のディレクトリを作成します.

プロジェクトのビルドに使うビルドスクリプトはプロジェクトルートの build.sbt に、プラグインは project/plugins.sbt に書きます.

project/plugins.sbt
addSbtPlugin("..." %% "..." % "...")

モジュールごとに build.sbt を書くこともできますが、Scala のプロジェクトでは一般的にプロジェクトルートにある build.sbt ですべてのモジュールを管理します.

ビルドスクリプトで使う関数などは project ディレクトリ以下に *.scala ファイルとしてまとめることもできます.

実行可能なアプリケーションを書く場合は、def main(args:Array[String]): Unit のシグニチャのエントリーポイント,つまり main 関数が必要です.

ただし object Foo extends App というふうに App トレイトを継承したオブジェクトが存在する場合は main メソッドを定義しなくても Foo のボディに記述された内容が main 関数の内容として扱われます.

object Foo {
  def main(args:Array[String]):Unit = println("hello world")
}
object Foo extends App {
  println("hello world")
}

Scala 3 の場合は main アノテーションをつけて書くこともできます.

@main def greet() = println("hello, world!")

ライブラリの宣言

sbt では % をつかって次のようにライブラリを指定します.

libraryDependencies ++= Seq(
  "namespace 0" %% "library name 0" % "version 0",
  "namespace 1" %% "library name 1" % "version 1",
  ...
)

%% は使用する scala のバージョンに対応するライブラリを取得することを意味しています.
つまり Scala 2.13.x が指定されている場合は 2.13.x 用にリリースされた Scala のライブラリを取得します.

Scala からは Java のライブラリを呼び出すこともできます. Java のライブラリを使う場合は Scala のバージョンは関係ないので次のように書きます. %%% になっていることに注意しましょう.

libraryDependencies ++= Seq(
  "namespace 0" % "library name 0" % "version 0",
  ...
)

Scala は JavaScript や native コードにコンパイルすることもできます. クロスプラットフォームに対応したライブラリを利用するには次のように書きます. %%% を見ての通り、 % が3つになっていることに注意しましょう.

libraryDependencies ++= Seq(
  "namespace 0" %%% "library name 0" % "version 0",
  ...
)

ローカルでのデバッグ

一般的にデバッグはテストを通して行いますが、ライブラリやアプリケーションによってはアプリケーションを実行しながらデバッグしたいケースもあると思います. たとえば GUI アプリケーションを書く場合はテストだけで動作確認をするのは難しいです.

その場合、publishLocal コマンドを使うことでローカル環境にアプリケーションを publish することができます.

build.sbt
scalaVersion := "2.13.8"
lazy val lib = project.in(file("."))
  .settings(
    organization := "com.example",
    name := "foo",
    version := "0.0.1-SNAPSHOT"
  )

このようなビルドファイルがあるとき sbt publishLocal コマンドを実行することでsrc/main/scala 以下に置かれたアプリケーション/ライブラリをローカルにリリースすることができます.

このアプリケーション/ライブラリは他のプロジェクトなどから libraryDependencies += com.example %% foo % 0.0.1-SNAPSHOT や ammonite スクリプトから import $ivy.`com.example::foo:0.0.1-SNAPSHOT` という形で呼び出すことができます.

余談ですが JVM 系のライブラリのリリースは pre-release として x.y.z-snapshot....や x.y.z-pre, 正式なリリースとして x.y.z という形式になっていることが多いです. 一般的に snapshot リリースは sonatype の snapshot 用のレポジトリから、リリースは maven central から配布されています.

アプリケーションのデプロイ

  1. fat jar を生成して JRE がある環境(オンプレ/クラウド/コンテナ/etc)で動かす
  2. Scala.js にコンパイルして node がある環境で動かす
  3. native な executable ファイルを生成する
    • scala native を使う
    • graalVM の native image を使う

他の言語とのやりとり

JVM から他の言語とやり取りするには次の方法があります.

  1. Process を使って stdio 経由でデータを受け渡しする
  2. ffi を使って ABI 経由でデータを受け渡しする
    • JNI: ABI で他のプログラムとデータをやり取りできる. JVM と ネイティブをつなぐためのコードを書く必要がある
    • project panama: Java 17 以上で利用できる experimental な API.
    • JRA: ネイティブ側のバインディングをほとんど書かなくていいがパフォーマンスが落ちる.

ffi はネイティブのコードを書かないといけないデメリットがありますが、クロスプラットフォームでハイパフォーマンスなアプリケーションを作りたい場合は検討するといいでしょう.

Discussion