Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

初心者向け: Scala CLIでScalaをはじめよう

この記事は Scala Advent Calendar 2023の19日目の記事です。19日って「あ〜今月が終わる」ってなるちょうど良い塩梅の日付ですね。

qiita.com

この記事では、Scalaの総合ツールであるScala CLIを利用する様子を紹介し、Scala CLIでScalaのちょっとしたアプリケーションを作れることを示します。

Scalaって動かすの大変なんでしょ?というありがちな疑問は、Scala CLIによって綺麗に打ち砕かれることでしょう。

対象とする読者

  • この記事の対象読者はScalaに興味がある人で、プログラミング自体の経験が少しはある人です。
  • 教育的配慮により、意図的に情報を省略していることがあります。中級者・上級者向けの情報ではないことを了承ください。

この記事で扱うこと・扱わないこと

この記事では、以下の内容について扱っています。

  • Scala CLIのインストール
  • REPL
  • Scala Script
  • イメージ作成

そして、以下の内容について扱いません。

  • Scala Nativeによるイメージ作成
  • JVMの詳細なインストール方法
  • Scalaとは何か

さらに初心者にはScastieがおすすめ

Scalaに興味があるけれどどのような言語なのかちょっと触ってみたいという方にはScastieがおすすめです。

scastie.scala-lang.org

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版は公式サイトで実行ファイルを入手できます。

scala-cli.virtuslab.org

# 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については以下の記事で説明しています。

blog.3qe.us

前提環境

この環境では、既にJDKがインストールされているものとします。JDKをインストールするにはOS標準で使えるJDKをインストールしたり、brewcsコマンドを利用したり、sdkmanなどを利用するとよいでしょう。

おすすめエントリはこれです。

nowokay.hatenablog.com

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)

実行結果はres0res1、・・・といった名前で保存され、後から使うことができます。

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 --helpscala-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ディレクティブと呼びます。

scala-cli.virtuslab.org

あるライブラリを使いたいときはScaladexを使うとUsingディレクティブをコピペできて便利です。

ScaladexにはScala CLI向けのコピペコーナーがある

良く使う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クライアントが降ってくるのでそこだけ気をつけてください。

sttp.softwaremill.com

#!/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ユーザだとも感じていたため、今回こうして比較的初心者向けの記事を書くことにしました。

あわせて読みたい

tanishiking24.hatenablog.com

(↑の記事が書かれた時点ではusing depusing libと書かれていました)

zenn.dev

blog.3qe.us

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?