この記事は、Qiita AdventCalender2023 ラクスパートナーズ7日目の記事です。
Javaを3年、Scalaを2年業務で触れたので、その知見をアウトプットする良い機会だと思い執筆しています。
JavaとScalaは同じJVMの上で動作する言語ですが、言語特性が大きく異なり学習に苦労しました。
その経験から Java -> Scalaの言語仕様の差異や躓きポイントについて書き、
「Scalaも楽しそうだな。」
と少しでも興味を持っていただけるきっかけになればと考えています。
バージョンについて
Javaは11、Scalaは2系を基準に執筆しています。(私が触れたバージョンが左記なので...)
Java11以降やScala3の機能は私の知る限り併せて記載しますが、何卒ご容赦ください。
Scalaとは
詳しい説明は公式サイトに書いてあるので、Javaエンジニア向けのポイントに絞ってお話します。
-
JVM言語であること
JVM(Java Virtual Machine) 上で動作するため、Javaのライブラリが使用できます。
Javaとの互換性を持っているので、Javaの知見を活かしやすいと考えます。 -
関数型言語
Scalaは関数型言語のひとつです。
Javaでもバージョン8からはラムダ式が導入され、関数型プログラミングが記述できるようになったのでイメージしやすいかもしれません。
この記事では関数型言語が何たるかは省略します。 -
オブジェクト指向
Scalaはオブジェクト指向でもあります。
関数型プログラミングとオブジェクト指向が融合する言語設計になっています。
Scalaという命名自体が「スケーラブルな言語」を意味していることから柔軟な言語といえます。
非常に簡素ではありますが、次項より具体的にScalaの特徴について説明します。
Java と Scalaの違い
簡単な変数や、定数、メソッドの宣言方法をご紹介します。
1. 変数や構文の記述方法
定数の宣言
// java
final String hoge = "hoge";
// scala
val hoge = "hoge"
変数の宣言
// java
String hoge = "hoge";
// scala
var hoge = "hoge"
メソッドの宣言
// java
public String method(String str) {
return str;
}
// scala
// 型名はコロンを使って変数の右側に定義する (typescriptと同じ)
def method(str: String): String = str
// 波括弧やreturn を省略できるのが特徴
def method(): String = "hoge"
// 引数が空の場合、中括弧も省略できる
def method: String = "hoge"
2. クラスの記述方法
Scalaのクラス定義は至ってシンプルです。
Java8や11であればクラス定義は、一般的に以下のように行うと思います。
public class User {
private String id;
private String name;
private int age;
public User(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return this.age;
}
public void setName(String age) {
this.age = age;
}
}
Java 16 からはrecord
も登場しており、クラスの記述方法はある程度簡略化されたかもしれませんが、依然として上記のように記述している現場も多いことかと思います。
一方、Scalaの場合は以下のように定義することができます。
case class User(id: String, name: String, age: Int)
// インスタンスを生成
val user = User("user-id", "user1", 20)
// 以下のようにアクセスできる
user.id // user-id
user.name // user1
この定義でgetter, setter, コンストラクタなどが生成されます。
Javaの冗長な記述から開放されるのは大きなメリットだと思います。
3. インターフェースの記述方法
Scalaにはinterface
はありませんので、紹介します。
Javaの場合
public interface Car {
void run();
}
Scalaの場合
trait Car {
def run(): Unit
}
Javaのinterface
に似た概念がScalaのtrait
(トレイト)です。
trait
はミックスイン(クラス合成)が可能なため、クラスに対してextends
やwith
句を使い複数のtrait
を記述することができます。
またtrait
にはメソッドも定義可能なため、リッチインターフェースとして使用できます。
trait Animal {
def cry(): Unit = {
println("cry.")
}
}
trait HasLegs
// Animal と hasLegs trait をミックスインしてクラスを作成する
class Dog extends Animal with HasLegs
val dog = new Dog()
dog.cry() // cry.
// メソッドの override もできる
class Cat extends Animal with HasLegs {
override def cry(): Unit = {
println("にゃー")
}
}
val cat = new Cat()
cat.cry() // にゃー
4. シングルトンオブジェクト
ScalaにはJavaのstatic
がありません。
代わりにシングルトンオブジェクトを作成するobject
という機能があります。
object MyObject {
def print(): Unit = {
println("Hello world!")
}
}
MyObject.print() // Hello world!
Javaにはない Scalaの強力な構文
簡単な言語仕様の説明を終えたところで、JavaにはないけれどScalaではよく使う構文などをご紹介します。
1. match 式
match式は一般的な言語におけるSwitch文のようなものです。
ただしScalaの場合はこのmatch式が非常に強力で頻出します。
以下に簡単な例を掲載します。
// switch文のようにケースを書くことができる
def convertTo(str: String) = {
str match {
case "foo" => "hoge"
case "bar" => "fuga"
case "bazz" => "piyo"
case _ => ""
}
}
// 出力
convertTo("foo") // hoge
convertTo("hello") // 空文字
// クラスでマッチすることができる
abstract class Sample
case class Foo(str: String) extends Sample
case class Bar(number: Int) extends Sample
case class Bazz(bool: Boolean) extends Sample
def convertTo(sample: Sample): String = {
sample match {
case Foo(str) => str
case Bar(number) => number.toString()
case Bazz(bool) => bool.toString()
case _ => ""
}
}
// ちなみに match式の結果を変数に持つこともできる
def convertTo(sample: Sample): String = {
val s = sample match {
case Foo(str) => str
case Bar(number) => number.toString()
case Bazz(bool) => bool.toString()
case _ => ""
}
s // scala は return を省略できるので、 return s と同義
}
2. for-yield 構文
Scalaのfor-yield 構文は、複数のCollection(配列のクラス)を組み合わせて新しいCollectionを生成するためのものです。
構文の記述方法が特殊なので、最初みたときは軽い拒否反応を起こすので紹介します。
以下は簡単な例です。
val numbers = List(1, 2, 3, 4, 5)
val letters = List('a', 'b', 'c', 'd', 'e')
val result = for {
number <- numbers // numbers をforループする
letter <- letters // letters をforループする
} yield (number, letter)
// 結果を出力
println(result)
このコードでは、numbersとlettersという2つのリストを組み合わせて、それぞれの要素のペアを生成しています。
for-yield構文は、numbersの各要素に対してlettersの全ての要素とのペアを生成し、その結果を新しいリストとして返します。この結果は、resultに格納されます。
矢印が右辺に向く(->
)のではなく、左辺に向かって<-
と記述するのが特徴です。
ちなみに上記のfor-yield構文は、シンタックスシュガー(糖衣構文)であり内部的には下記と等価のコードです。
val results = numbers.flatMap(n => letters.map(l => (n, l)))
// Javaだとこんな感じ
var results = numbers.stream().flatMap(n -> letters.map(l -> new SimpleEntry(n, l));
// もしくは以下
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Character> letters = Arrays.asList('a', 'b', 'c', 'd', 'e');
List<SimpleEntry<Integer, Character>> result = new ArrayList<>();
for (Integer n : numbers) {
for (Character l : letters) {
result.add(new SimpleEntry<>(n, l));
}
}
Javaの感覚で読めないScalaのコード
前項までは、簡単にScalaの構文や定義方法について触れました。
Javaと互換性のある言語なので、それほど難しくはなかったかと思います。
しかし、それだけではScalaコードを見てもよく分からないかもしれません。(私がそうでした)
そこで下記2つを覚えておけば劇的に理解しやすくなると思うので、紹介します。
1. 省略記法
突然ではありますが、下記を御覧ください。
val list1 = List(1, 2)
val list2 = List(3, 4, 5)
val list3 = list1 ::: list2
:::
とはなんでしょうか。
演算子のように見えますが、演算子ではなくメソッドです。
Scalaは自分で新たに演算子のような定義を生み出すことができます。魅力のひとつであり、初学者が躓くポイントでもあると思います。
上記は流れを見ていただければ何となく分かるかと思いますが、リスト同士の結合です。
list1
とlist2
を結合して新たにlist3
を生成しています。
どうしてこんな演算子のように記述することができるのでしょうか。
これがまさに省略が施されたメソッドなのです。
では省略をせずに表現するとどうなるか確認してみましょう。
val list1 = List.apply(1, 2)
val list2 = List.apply(3, 4, 5)
val list3 = list1.:::(list2)
まず、:::
を見てみると、list1
のメソッドであることがわかりました。
Scalaではメソッドの.
や()
を省略することができます。
省略する場合は、list1 ::: list2
とスペースを空けて記述します。
これが演算子のように見えていた正体です。
また、見慣れぬapplyメソッドも出現しました。
何気ない定義にも省略記法はあります。
Scalaにおいて最も代表的な省略が、このapply
です。
applyメソッドとは?
Scalaではcase classでgetterやsetter, コンストラクタを生成できると書きました。
case classを使った場合、使わなかった場合を比較したいと思います。
case class User(id: String, name: String, age: Int)
// 通常のクラスで書く場合
class User(id: String, name: String, age: Int)
object User {
def apply(id: String, name: String, age: Int): User = new User(id, name, age)
}
クラス名と同名のobject
はコンパニオンオブジェクトと呼ばれ同名のクラスに対して特別な権限を持ちます。
詳細は本記事では説明しませんが、apply
メソッドが定義されているということがポイントです。
(case class自体は他にもtoString()
やequals()
を自動生成してくれたり、Productを継承したりする)
Scalaではapply
は特別な概念で省略することができます。
つまり、
val list1 = List(1, 2)
は Listクラスのapply
メソッドを省略して定義していたのです。
メソッドの記述方法いろいろ
自由な書き方ができるのがScalaの魅力です。
とはいえ、やりすぎると可読性が下がるので注意が必要です。
val list = List(1, 2, 3, 4, 5)
// 結果を2倍にして返す
// 下記はすべて同じ意味
list.map(l => l * 2)
list.map(_ * 2) // _を使って変数を省略することができる
list map { l => l * 2 } // 中括弧のかわりに波括弧を使えます。ブロック式のように見せることができます。
list map { _ * 2 }
2. implicit
Scalaにはimplicit
というJavaにはない機能があります。
implicit = 暗黙的な
という意味でScala2においては様々な用途で使われます。
この概念が難しく、はじめは勉強しても訳がわかりませんでした。
implicit は主に3つの機能を提供します。
簡単に各機能について紹介したいと思います。
(Scala2を基準に書きます。Scala3の機能についても各項で触れます。)
- 暗黙の型変換 (implicit conversion)
- 暗黙の変換 (Enrich My Libraryパターン)
- 暗黙のパラメーター (implicit parameter)
1. 暗黙の型変換とは
Scalaの暗黙の型変換は、ある型から別の型へ自動的に変換する機能を提供します。
以下にその例を示します。
// String => Int の暗黙の変換を定義
implicit def stringToInt(s: String): Int = s.toInt
val x: Int = "123" // StringからIntへ自動的に変換される
println(x + 1) // 124
Scalaコンパイラは型が合わない場合、即座にコンパイルエラーにするのではなく、型が合う変換メソッドがないかどうかを探してくれます。
上記の例では、String
からInt
に変換するメソッドをimplicit で定義しているため正常にコンパイルすることが出来ます。
このように暗黙の型変換によって、明示的な変換メソッドやヘルパーメソッドを定義しなくともよくなります。
しかし反対になぜ変換されているかが視認しづらくなりコードの可読性が落ちるというデメリットもあります。
ちなみに上記はScala3ではimplicitの責務ではなくなりました。
givenキーワードを用いてConversionを定義します。
given stringToInt: Conversion[String, Int] with
def apply(s: String): Int = s.toInt
// 変数名は省略できる
given Conversion[String, Int] with
def apply(s: String): Int = s.toInt
// apply も省略できる
given Conversion[String, Int] = (s: String) => s.toInt
この変更によって意図がより明確に伝わるようになりました。
2. 暗黙の変換 (Enrich My Libraryパターン)
Enrich My Libraryパターンは、Scalaの暗黙のクラスと暗黙の変換を使用して、既存のクラスに新しいメソッドを追加するテクニックです。
このパターンは、既存のクラスのコードを変更することなく、新しい機能を追加することができます。ライブラリを任意に拡張したりできるので便利な機能だと思います。
// class にimplicit を記述する
implicit class RichInt(val self: Int) {
def times[A](f: => A): Unit = {
for (_ <- 1 to self) f
}
}
// この拡張メソッドを使って、何かを5回繰り返すことができる
5 times {
println("Hello, world!")
}
Int型に自分で作ったtimes メソッドを足すことができました。
既存クラスには無いメソッドのはずなのにどうして呼び出すことができるんだろうと感じたときはenrich my librayパターンが隠れているかもしれません。
Scala3では、implicit
ではなくextension
に変わりました。
extension (x: Int)
def times[A](f: => A): Unit = {
for (_ <- 1 to x) f
}
5.times {
println("Hello, world!")
}
拡張する、ということがよりわかりやすくなったと思います。
3. 暗黙のパラメーターとは
引数を暗黙的に受け取る機能です。
引数に明示的に値を指定しなくとも、スコープ内に適切な型と暗黙の値が存在すれば自動的に呼び出されます。
// 暗黙の値を定義
implicit val defaultName: String = "太郎"
// 暗黙のパラメータを持つ関数
def greet(implicit name: String): Unit = {
println(s"こんにちは、${name}!")
}
// 暗黙のパラメータを明示的に指定して関数を呼び出す
greet("花子") // こんにちは、花子!
// 暗黙のパラメータを指定せずに関数を呼び出す(スコープ内の暗黙の値が使用される)
greet // こんにちは、太郎!
よく使われるとしたら、DBのコネクションなどが例でしょうか。
例えば下記は、ScalikeJDBCというScalaでJDBCを扱うためのライブラリです。
import scalikejdbc._
// Connection Poolの設定
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.singleton("jdbc:mysql://localhost:3306/database", "user", "password")
// トランザクション内でのDB操作
DB.localTx { implicit session =>
// INSERT文の実行
sql"insert into members (name) values (${"花子"})".update.apply()
// SELECT文の実行
val members: List[Member] =
sql"select * from members".map(rs => Member(rs)).list.apply()
}
DB.localTxのsession
はクエリの実行などデータベース操作の各機能を提供します。
implicit session
によって、明示的に記述せずコードを実行することができます。
implicit を用いない場合は下記になります。
import scalikejdbc._
// Connection Poolの設定
Class.forName("com.mysql.jdbc.Driver")
ConnectionPool.singleton("jdbc:mysql://localhost:3306/database", "user", "password")
// トランザクション内でのDB操作
DB.localTx { session =>
// INSERT文の実行
sql"insert into members (name) values (${"花子"})".update.apply()(session)
// SELECT文の実行
val members: List[Member] =
sql"select * from members".map(rs => Member(rs)).list.apply()(session)
}
このようにsqlのたびにsessionを記述しなければならなくなります。
implicitパラメーターは冗長性を排除し、上記の例であれば開発者は”SQLの実行”という関心事にさえ注目すればよいことがわかります。
Scala3 ではわかりにくいのでgiven/using
に変更されました。
given
で指定した値をusing
で使用することが出来ます。
はじめの例を置き換えると下記になります。
def greet(using name: String): Unit = {
println(s"こんにちは、${name}!")
}
given defaultName: String = "太郎"
greet // こんにちは、太郎!
implicit まとめ
Scala2ではimplicit が様々な用途で使われるためコードの読みにくさが多少なりともあると思います。
既存ライブラリの拡張や冗長性の排除、暗黙的な型解決など知らないと躓く機能が満載です。
コードの可能性は広がりますが、慣れるまでは意味が分からなさすぎて苦労する場面もあるかもしれません。
Scala3ではかなり改善しているので併せて紹介しました。
最後に
Scala は関数型言語であり、癖のある書き方も多いのでとっつきにくさはあるかもしれませんが、使っていて楽しい言語だと思います。
メソッドやクラスを拡張してどんどん自分の世界を作り上げていける楽しさがあります。
一方、柔軟すぎるあまり演算子のような記号がたくさん並ぶソースコード(++
や:=
など)が多く、検索エンジンで調べてもヒットしにくいというデメリットもあります。
本記事が少しでもScalaを理解するきっかけになれば幸いです。