FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

IntelliJ IDEA + AmmoniteでScalaスクリプトの開発を始めよう

この記事はScala Advent Calendar 201820日目です。

ブログに初投稿します、エンジニアの門脇(@blac_k_ey)です。
広告運用周りの開発を担当しています。

最近、自分の中でAmmoniteが熱いのでこの度ブログを書いてみることにしました。

今回はIntelliJ IDEAでAmmoniteを使ったScalaスクリプトの開発環境を構築する方法をお伝えします。

Ammoniteとは?

https://ammonite.io/ammonite.io

Ammoniteは、lihaoyiさんが開発した高機能なScalaREPLおよび、Scalaスクリプトの実行環境です。

なぜScalaスクリプトを書くのか?

これは自身の所感なのですが、以下の利点が上げられます。

…などでしょうか。
まぁ、やっぱり好きな言語で書きたいっていうのが大きいですね(笑)

IntelliJのサポート

blog.jetbrains.com

blog.jetbrains.com

IntelliJはAmmoniteを用いたScalaスクリプトの開発をサポートしています。

しかし、上記の記事にある操作を実行しても、依存するライブラリが依存するライブラリの補完が効かなかったり、そもそも import $ivy を認識してくれない時があったりなど、少々使いにくさが目立ってしまっています。1

今回はsbtを利用してその不便さを回避、快適なScalaスクリプト開発ができる環境を目指します。

前準備

必要なものは以下です。なるべく最新の環境を揃えるのがよいでしょう。

この記事を読んでいる方ならIntelliJとsbtはインストールされているかと思いますので割愛します。
Ammoniteの最新版は以下のコマンドでをインストールできます。
(2018年12月時点、v1.5.0)

$ sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.5.0/2.12-1.5.0) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

Macをお使いの方はbrewでもインストールできます。

$ brew install ammonite-repl

インストールが完了したらまずはREPLを立ち上げてみましょう。

$ amm
Loading...
Welcome to the Ammonite Repl 1.5.0
(Scala 2.12.8 Java 1.8.0_191)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@

※fishを使っている方はうまく動作しないかもしれません。2bashを介して実行するのがよいでしょう。

$ bash -c amm

プロジェクトを作成する

giter8テンプレートを用意しました。

github.com

sbt new コマンドでプロジェクトを作成できます。

$ sbt new NomadBlacky/ammonite-intellij.g8
[info] Loading settings for project global-plugins from idea.sbt,build.sbt ...
[info] Loading global plugins from /Users/t_kadowaki/.sbt/1.0/plugins
[info] Set current project to scala (in build file:/Users/t_kadowaki/projects/scala/)
[info] Set current project to scala (in build file:/Users/t_kadowaki/projects/scala/)

A minimal Scala Scripts project.

name [Scala Scripts Project]:
scala_version [2.12.8]:
ammonite_version [1.5.0]:

Template applied in /Users/t_kadowaki/projects/scala/./scala-scripts-project

$ tree scala-scripts-project/
scala-scripts-project/
├── build.sbt
├── my-script.sc
└── project
    └── build.properties

1 directory, 3 files

作成されたファイルは以下のようになっているかと思います。

bulid.sbt

// It uses to import dependencies to IntelliJ.
lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      scalaVersion := "2.12.8"
    )),
    libraryDependencies ++= Seq(
      "com.lihaoyi" % "ammonite" % "1.5.0" cross CrossVersion.full
      // Please add dependencies to use in Scala Scripts.
      // "com.sample" %% "scala-library" % "0.1.0"
    )
  )

my-script.sc

// If you want to add a dependency, please add the same one to build.sbt.
// import $ivy.`com.sample::scala-library:0.1.0`

@main
def hello(thing: String = "World"): Unit = {
  println(s"Hello, $thing!")
}

プロジェクトを作成したらIntelliJ上でプロジェクトを開きます。
(sbtプロジェクトとしてインポートされます。)

File -> Open -> scala-scripts-project を選択

プロジェクトを開いたら、Scalaスクリプトの拡張子である .sc をAmmoniteであることを認識させる必要があります。
(デフォルトはWorksheetとして扱われる)

File -> Settings -> Language & Frameworks -> Scala -> Worksheet
Always Worksheet -> Always Ammonite に変更

f:id:Nomad_Blacky:20181219194818p:plain

これで準備完了です!

Hello Ammonite!

まずはテンプレートから展開されたスクリプトを実行してみます。

$ amm my-script.sc
Compiling /Users/t_kadowaki/projects/scala/scala-scripts-project/my-script.sc
Hello, World!

$ amm my-script.sc
Hello, World!

$ amm my-script.sc --thing Ammonite
Hello, Ammonite!

2回実行してみました。
1回目と比べて2回目は Compiling ... と表示がなく、起動が少し速いことが分かるかと思います。

これはAmmoniteがコンパイルしたスクリプトバイトコードをキャッシュしているからです。

また、関数の引数である thing: Stringコマンドラインオプションとして渡せるのがわかります。

@main
def hello(thing: String = "World"): Unit = {
  println(s"Hello, $thing!")
}

スクリプト@mainアノテーションが書かれていますがこれは何でしょうか。
これはスクリプトのエントリーポイントを示しています。

スクリプトを以下のように書き換えてみましょう。

@main
def hello(thing: String = "World"): Unit = {
  println(s"Hello, $thing!")
}

@main
def fizzbuzzJson(i: Int): Unit = {
  val fizzbuzz = (1 to i).map { i =>
    (i % 3, i % 5) match {
      case (0, 0) => "FizzBuzz"
      case (0, _) => "Fizz"
      case (_, 0) => "Buzz"
      case _ => i.toString
    }
  }
  val json = ujson.Obj("fizzbuzz" -> fizzbuzz)
  println(json)
}

実行してみます

$ amm my-script.sc
Need to specify a subcommand to call when running my-script.sc

Available subcommands:

  hello
    --thing  String (default World)

  fizzbuzzJson
    --i      Int
    
$ amm my-script.sc fizzbuzzJson 15
{"fizzbuzz":["1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz","13","14","FizzBuzz"]}

@main を複数定義するとサブコマンドのように振る舞うことができます!

fizzbuzzJsonJSON文字列を作成していますが、これにはuJsonというライブラリを使っています。
Ammoniteにはスクリプトを実装するのに役立つ様々なライブラリが依存関係に含まれています。

…など

スクリプトにライブラリ依存を追加する

import $ivy を使うとライブラリをスクリプトの依存に含めることが可能です。
ここでは試しにスクレイピングライブラリであるscala-scraperを追加してみます。

import $ivy.`net.ruippeixotog::scala-scraper:2.1.0`

ここでIntelliJ$ivy を認識してポップアップを出してくれると良いのですが、場合によってはうまく動作しません。3

そこで、sbtの libraryDependencies に加え、IntelliJでインポートすることでそれを回避します。

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      scalaVersion := "2.12.8"
    )),
    libraryDependencies ++= Seq(
      "com.lihaoyi" % "ammonite" % "1.5.0" cross CrossVersion.full,
      "net.ruippeixotog" %% "scala-scraper" % "2.1.0"
    )
  )

sbtプロジェクトとしてインポートしたらコードを書いてみましょう

f:id:Nomad_Blacky:20181219194848g:plain

補完がちゃんと効いていますね!


以上、駆け足になってしまいましたが、IntelliJScalaスクリプトを開発環境を整える方法をお伝えしました。
ぜひ一度、AmmoniteによるScalaスクリプトの威力を感じてみてください。

スクリプトを書く際のTipsや実際の活用例などは、次回以降お伝えできればと思っています。

少し複雑なスクリプトを書く時の選択肢にScalaがあることを思い出していただければ幸いです!

Scalaは静的型付けスクリプト言語です!!


  1. 自分だけでしょうか…

  2. Issueが上がっています

  3. 自分だけでしょうか………