この記事は Scala Advent Calendar 2023の19日目の記事です。19日って「あ〜今月が終わる」ってなるちょうど良い塩梅の日付ですね。
この記事では、Scalaの総合ツールであるScala CLIを利用する様子を紹介し、Scala CLIでScalaのちょっとしたアプリケーションを作れることを示します。
Scalaって動かすの大変なんでしょ?というありがちな疑問は、Scala CLIによって綺麗に打ち砕かれることでしょう。
- 対象とする読者
- この記事で扱うこと・扱わないこと
- さらに初心者にはScastieがおすすめ
- Scala CLIって?
- 前提環境
- REPLを実行してみよう
- Scala Scriptをはじめよう
- Scala Script FAQ
- Scalaアプリケーションをパッケージしてみよう
- Scala Scriptをsbtプロジェクトに変換する
- まとめ
- 動機
- あわせて読みたい
対象とする読者
- この記事の対象読者はScalaに興味がある人で、プログラミング自体の経験が少しはある人です。
- 教育的配慮により、意図的に情報を省略していることがあります。中級者・上級者向けの情報ではないことを了承ください。
この記事で扱うこと・扱わないこと
この記事では、以下の内容について扱っています。
- Scala CLIのインストール
- REPL
- Scala Script
- イメージ作成
そして、以下の内容について扱いません。
- Scala Nativeによるイメージ作成
- JVMの詳細なインストール方法
- Scalaとは何か
さらに初心者にはScastieがおすすめ
Scalaに興味があるけれどどのような言語なのかちょっと触ってみたいという方にはScastieがおすすめです。
Scastieはブラウザ上で動作するScalaの実行環境で、Scalaのバージョンの指定、ライブラリの利用、シンタックスハイライトと基本的なコード補完を利用できます。
Scala CLIって?
Scala CLIは、ポーランドを主拠点としてScala開発を行っているVirtusLab社が開発・維持しているScala用のCLIツールキットです。
scala-cli
という単一のコマンドでScala開発に便利な機能を利用できます。
% scala-cli Welcome to Scala 3.3.0 (19.0.2, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala>
また、Scala CLIは次期の公式Scalaコマンドランナーとして採用されることが決定しており、デファクトスタンダードの地位を確立しています。
Scala CLIで何ができるの?
Scala CLIで利用できる機能には以下のようなものがあります:
- REPL
- プロジェクトを作成せずに直接Scalaのコードを実行できます。サンプルコードを試したいときや、コンパイルが通るか確認したいとき、ちょっとした作業を行いたいときに便利です。
- またREPLでは外部ライブラリを利用できます。
- Scalaコード・Scala Script(後述)の実行
scala-cli
コマンドにScala Script(後述)や実行可能なScalaファイルを渡すとそれを直接実行できます。プロジェクト設定は必要ありません。- FAQ
- コンパイラ設定は必要ないの?
- 必要ありません。必要になったらファイル内に書くことができます
- 外部ライブラリに依存したいんだけど?
- 特別なファイルを作成する必要はありません。ファイル内に特殊なコメントを記述するだけで全部完結します。Scala CLIが自動的にライブラリの依存性を解決するため、ライブラリを使うためだけにsbtなどを使う必要はありません。
- コンパイラ設定は必要ないの?
- Scalaコードのパッケージング
- JAR
- GraalVM
- Scala.js
- Scala Native (ここでは割愛します)
- 他いろいろ (ここでは割愛します)
Scala CLIをインストールする
Scala CLIをインストールするにはいくつかの方法がありますが、Linux(Unix) / macOSではワンライナーを利用するのが一番簡単です。Windows版は公式サイトで実行ファイルを入手できます。
# Linux % curl -sSLf https://scala-cli.virtuslab.org/get | sh
# macOS
% brew install Virtuslab/scala-cli/scala-cli
また、cs
(Coursier)がインストールされている環境では、cs install scala-cli
でインストールすることも可能です。
% cs install scala-cli
Coursierについては以下の記事で説明しています。
前提環境
この環境では、既にJDKがインストールされているものとします。JDKをインストールするにはOS標準で使えるJDKをインストールしたり、brew
やcs
コマンドを利用したり、sdkman
などを利用するとよいでしょう。
おすすめエントリはこれです。
REPLを実行してみよう
Scala CLIでREPLを起動するには、scala-cli repl
と入力します。単にscala-cli
を起動するだけでも、REPL機能が立ち上がります。
% scala-cli repl % scala-cli # 同じ
REPLが起動すると、scala>
というプロンプトが表示されます。
% scala-cli Welcome to Scala 3.3.0 (19.0.2, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala>
Ctrl+D
を入力したり、:exit
と入力してエンターキーを押すと終了できます。
REPLではScalaのコードを1行ずつ入力できます。コンパイラが丁寧に実行結果をメッセージを出してくれるので便利です。
scala> Seq(1,2,3,4,5).map(_ * 2) val res0: Seq[Int] = List(2, 4, 6, 8, 10)
実行結果はres0
、res1
、・・・といった名前で保存され、後から使うことができます。
scala> res0.mkString("-") val res1: String = 2-4-6-8-10
REPLでは:typeなどの便利なコマンドが使えます。:helpでコマンドの一覧を表示できるので使ってみましょう。
scala> :type res0
Seq[Int]
また、REPL起動時に-Sオプションを使うとScalaのバージョンを指定できます。
% scala-cli -S 2.13 Welcome to Scala 2.13.11 (OpenJDK 64-Bit Server VM, Java 19.0.2). Type in expressions for evaluation. Or try :help. scala>
その他のオプションはscala-cli --help
やscala-cli --help repl
を入力すると確認できます。
Scala Scriptをはじめよう
この節では、Scala CLIを使ってScalaのコードを実行する様子を紹介します。
前述しましたが、Scala CLIはScalaコードを直接実行する能力を持っています。この機能を使うことで、ちょっとしたスクリプト言語の代わりにScalaを使うことができます。
一般的なスクリプト言語と比較すると、Scalaは以下のような特長を備えています:
- 関数型言語とオブジェクト指向のハイブリッドに由来する強力な表現力
- 生産性を安全に上げることができます
- 柔軟で静的な型付け
- 実行前に間違いに気付くことができます
- 実行中にクラッシュするリスクを極限まで減らすことができます
- 便利なコレクションメソッド
- ループ処理やグループ分けを手作りする必要はありません
- 安定した高いパフォーマンス
- 長年使われてきた実績のあるJVMで動作します(他の基盤にもコンパイル可)
普通のScalaファイルを実行する
まずは軽く、普通のScalaファイルを実行してみます。
例えば、source.scala
を以下のような内容で作成してみましょう:
// source.scala // 一般的なScalaプログラムと同様、エントリポイントとして`App`を継承したオブジェクトを作成している object Main extends App { println("Hello, Scala!") }
このソースコードを実行するにはscala-cli source.scala
を実行します:
% scala-cli source.scala Compiling project (Scala 3.3.0, JVM) Compiled project (Scala 3.3.0, JVM) Hello, Scala!
何事もなく実行できましたね。
Scala Script
Scalaファイルを発展させた形式として、Scala CLIはScala Scriptというファイルを実行できます。Scala Scriptは以下のような特長を持っています:
Main
メソッドを用意しなくてもよい。- ファイル中に処理系のバージョンや依存するライブラリを書くことができる。
- shebangを付けることでシェルから直接実行できる。
- 便利なライブラリが標準搭載されている。
Scala Scriptの拡張子は.sc
か.scala.sc
が主に使われます。
Scala Scriptの一例がこちら:
//> using scala "3.3.0" //> using dep "com.softwaremill.sttp.client4::core:4.0.0-M8" println("Hello, Scala Script!") // 試しにsttpライブラリを使ってHTTPリクエストする import sttp.client4.quick.* val body = quickRequest.get(uri"https://example.com/").send().body println(body)
このファイルを実行するには、scala-cli source.sc
を実行します:
% scala-cli source.sc Compiling project (Scala 3.3.0, JVM) Compiled project (Scala 3.3.0, JVM) Hello, Scala Script! <!doctype html> <html> <head> <title>Example Domain</title> ......
Main
オブジェクトを作る必要なく、直接書いたScalaコードを実行できましたね。使ってみると分かるのですが、とても便利で爽快です。
Scalaのバージョンやライブラリ指定は//> using
という特殊なコメントを冒頭に設置することで行います。これをScala Scriptの用語でUsingディレクティブと呼びます。
あるライブラリを使いたいときはScaladexを使うとUsingディレクティブをコピペできて便利です。
良く使うUsingディレクティブは以下の通りです:
- dep
- ライブラリに依存するときに使います。
- 各スクリプトは dep に記載したライブラリを参照します。参照するライブラリがローカルにキャッシュされていない場合は、ScalaCLIが自動でダウンロードしてくれます
- 他のファイルとのライブラリ干渉は起こりません。
- scala
- Scalaのバージョンを指定できます。
- 省略すると最新のScalaが使われます。
- toolkit
- これから説明します。
Scala Toolkitを使う
Scala Scriptでは、Scala Toolkitと呼ばれる、スクリプトでよく使う定番の操作を行うための便利なライブラリ群を利用することでスクリプト言語並みの手軽さを提供しています。Scala Toolkitは特別な扱いを受けており、特別な構文ですぐに利用できるようになっています。
Scala Toolkitを使うと、複数のライブラリが自動的に利用できるようになります。執筆現在、以下のライブラリが使えるようになります:
os-lib
- ファイル操作を扱う汎用ライブラリ
sttp
- HTTP通信を扱うライブラリ
ujson/upickle
- JSONを扱うライブラリ
MUnit
- テストライブラリ
Scala Toolkitを利用するには、スクリプト冒頭の部分に//> using toolkit latest
と記述します。
//> using scala 3.3.0 //> using toolkit latest // argsに引数が自動的に入っている val txt = os.read(os.pwd / args(0)) print(txt) val j = ujson.read("""{"foo": 42}""") println(j("foo").num) println("Hello, Toolkit!") import sttp.client4.quick.* val body = quickRequest.get(uri"https://example.com/").send().body println(body)
このスクリプトを実行すると、ファイルの読み出し、JSONのパース、HTTPリクエストが実行されます。
# 引数を渡すには -- で区切る % scala-cli toolkit.sc -- dummytext.txt Compiling project (Scala 3.3.0, JVM (19)) Compiled project (Scala 3.3.0, JVM (19)) THIS IS DUMMY TEXT 42.0 Hello, Toolkit! <!doctype html> <html> <head> <title>Example Domain</title> ......
とても便利ですね!
Scala Script FAQ
ここでよくある問いに答えていきましょう。
スクリプトを直接実行したい
これまではスクリプトをscala-cli
コマンドに直接渡していました。これを./foo-bar-script.sc
のように実行するにはどうしたらよいでしょうか。
#!/usr/bin/env -S scala-cli shebang -S 3 --quiet println("We've used shebang feature!")
このように冒頭に#!/usr/bin/env -S scala-cli shebang -S 3 --quiet
を追加することでシェルスクリプト風に直接実行できるようになります。
# 実行権限付けとく % chmod u+x shebang.sc % ./shebang.sc We've used shebang feature!
各オプションの意味は以下の通りです:
-S 3
- Scala 3以降を使う
--quiet
- コンパイル中のメッセージを表示しない
引数を渡す
Scala Scriptでは、引数はargs
リストに勝手に格納されます。args(0)
のように書くと各引数を取り出せます。
#!/usr/bin/env -S scala-cli shebang -S 3 --quiet println(s"We have args: ${args.mkString("[", ",", "]")}")
% ./args.sc foo bar buzz We have args: [foo,bar,buzz]
JSONを読み書きしたい
Toolkitにも含まれているuJson/uPickleを利用するのが一番楽です。
#!/usr/bin/env -S scala-cli shebang -S 3 --quiet //> using toolkit latest // フィールドを読む val j = ujson.read("""{"foo": 42}""") println(j("foo").num) // JSONに変換する import upickle.default.* println(write(Seq(1, 2, 3)))
% ./json.sc 42.0 [1,2,3]
ファイルを読み書きしたい
Toolkitにも含まれているos-libを利用するのが一番楽です。めちゃくちゃ出来が良いです。読み書きに加えて、ファイルの削除、ディレクトリ作成など、ほぼ何でも可能です。
使い方も簡単で、たぶん公式GitHubのREADMEを読めば十分です。
#!/usr/bin/env -S scala-cli shebang -S 3 --quiet //> using toolkit latest val content: String = os.read(os.pwd / args(0))
HTTPアクセスしたい
Toolkitにも含まれているsttp
を利用するのが一番楽です。執筆時点では、using toolkit latest
するとバージョン4のsttpクライアントが降ってくるのでそこだけ気をつけてください。
#!/usr/bin/env -S scala-cli shebang -S 3 --quiet //> using toolkit latest // GETする import sttp.client4.quick.* val body = quickRequest.get(uri"https://example.com/").send().body println(body)
Scalaアプリケーションをパッケージしてみよう
Scala CLIの目玉機能として、Scalaコードを実行可能なJARファイル(Uberjar)やJavaScriptなどにパッケージするというものがあります。実際に使ってみましょう。
package機能を使うには、あらかじめscala-cli config power true
しておきます。
% scala-cli config power true
JARに変換する
特に何もオプションを付けずにscala-cli package ファイルを実行すると、ScalaをJAR形式に変換し、JVMがあれば直接実行可能な形式に変換されます。
% scala-cli package args.sc Compiling project (Scala 3.3.1, JVM (19)) Compiled project (Scala 3.3.1, JVM (19)) Wrote /home/windymelt/temp/args, run it with ./args
% ./args foo bar buzz We have args: [foo,bar,buzz]
これだけでも十分便利ですね。
GraalVMでネイティブイメージに変換する
GraalVMを利用すると、JVMが無い環境でもScalaコードを動作させることができます。また、JVM特有の起動時間の遅さが解消できるため、AWS LambdaのイメージやCLIツールの作成などに適しています。
GraalVMを利用してネイティブイメージを作成するには、--graal
オプションを付けます。また、執筆時点ではJVMのバージョンは17が上限になっているようです。
#!/usr/bin/env -S scala-cli shebang -S 3 --quiet //> using jvm 17 println(s"We have args: ${args.mkString("[", ",", "]")}")
% scala-cli package --graal args.sc Compiling project (Scala 3.3.1, JVM (17)) Compiled project (Scala 3.3.1, JVM (17)) ======================================================================================================================== GraalVM Native Image: Generating 'args' (executable)... ======================================================================================================================== [1/7] Initializing... (4.4s @ 0.23GB) Version info: 'GraalVM 22.3.1 Java 17 CE' Java version info: '17.0.6+10-jvmci-22.3-b13' C compiler: gcc (suse, x86_64, 13.2.1) Garbage collector: Serial GC 1 user-specific feature(s) - com.oracle.svm.polyglot.scala.ScalaFeature [2/7] Performing analysis... [******] (10.3s @ 1.52GB) 3,380 (75.04%) of 4,504 classes reachable 4,068 (52.78%) of 7,708 fields reachable 15,360 (37.79%) of 40,650 methods reachable 28 classes, 135 fields, and 541 methods registered for reflection 58 classes, 58 fields, and 52 methods registered for JNI access 4 native libraries: dl, pthread, rt, z [3/7] Building universe... (1.5s @ 0.66GB) [4/7] Parsing methods... [*] (0.8s @ 1.42GB) [5/7] Inlining methods... [***] (0.5s @ 1.84GB) [6/7] Compiling methods... [***] (6.3s @ 0.92GB) [7/7] Creating image... (1.6s @ 1.40GB) 4.83MB (36.34%) for code area: 8,892 compilation units 8.21MB (61.82%) for image heap: 106,816 objects and 5 resources 249.31KB ( 1.83%) for other data 13.28MB in total ------------------------------------------------------------------------------------------------------------------------ Top 10 packages in code area: Top 10 object types in image heap: 686.54KB java.util 1.03MB byte[] for code metadata 465.90KB java.lang.invoke 1000.81KB java.lang.String 339.23KB java.lang 991.96KB byte[] for java.lang.String 268.11KB java.text 928.86KB byte[] for general heap data 216.40KB java.util.regex 752.77KB java.lang.Class 202.87KB java.util.concurrent 445.41KB java.util.HashMap$Node 148.18KB java.math 264.06KB com.oracle.svm.core.hub.DynamicHubCompanion 114.89KB com.oracle.svm.core.genscavenge 214.63KB java.util.HashMap$Node[] 103.69KB java.util.logging 179.74KB java.lang.String[] 101.85KB java.util.stream 155.67KB java.util.concurrent.ConcurrentHashMap$Node 2.18MB for 135 more packages 1.71MB for 892 more object types ------------------------------------------------------------------------------------------------------------------------ 1.2s (4.2% of total time) in 18 GCs | Peak RSS: 3.56GB | CPU load: 9.55 ------------------------------------------------------------------------------------------------------------------------ Produced artifacts: /home/windymelt/temp/args (executable) /home/windymelt/temp/args.build_artifacts.txt (txt) ======================================================================================================================== Finished generating 'args' in 27.0s. Wrote /home/windymelt/temp/args, run it with ./args
JSに変換する
Scala.jsというJS向けのコンパイラを利用するとScalaをJavaScriptに変換できます。これによりNode.jsなどでScalaを動作させることができ、様々な環境でScalaを利用できます。
ただし、JavaScriptはJVMで動作しないため、Java APIやリフレクションなどのJVMに依存したライブラリはコンパイルできません。例えば執筆現在はos-lib
はScala.jsでコンパイルできません。
Scala.jsを利用してJSファイルを作成するには、--js
オプションを付けます。また、執筆時点ではこの機能はScala Scriptではなく通常のScalaモジュールに対してしかうまく動かないようです。
object Main extends App: println(s"Hello, JS!")
生成したファイルはNodeで実行できます:
% scala-cli package --js js.scala Compiling project (Scala 3.3.1, Scala.js 1.14.0) Compiled project (Scala 3.3.1, Scala.js 1.14.0) Wrote /home/windymelt/temp/Main.js, run it with node ./Main.js % node ./Main.js Hello, JS!
Scala Scriptをsbtプロジェクトに変換する
コードが大きくなってきたと感じたら、sbtプロジェクトに変換することもできます。
% scala-cli config power true % scala-cli export Hello.scala --sbt
まとめ
この記事では、Scala初心者向けに便利なScala CLIについて紹介しました。これを機会としてScalaに親しんでもらえれば嬉しいです。僕は普段からScala CLIを用いて日頃のツールを作って便利に暮らしております。1ファイルで書けるのって良いよね。
動機
Scala CLIを紹介する記事はいくつかあるのですが、それらの記事はある程度Scalaに親しんでいる中級者向けという印象を持っていました。Scala CLIはある程度Scalaに熟達していても効果を発揮しますが、一番このツールが役立つのは初学者のScalaユーザだとも感じていたため、今回こうして比較的初心者向けの記事を書くことにしました。
あわせて読みたい
(↑の記事が書かれた時点ではusing dep
はusing lib
と書かれていました)