DACエンジニアブログ:アドテクゑびす界

DACのエンジニアやマーケター、アナリストが執筆するアドテクの技術系ブログです。

Scala入門 基礎編「Scalaの書き方を理解しよう」 - PHP使いからScala使いへ転身!

はじめに

みなさんこんにちは、プロダクト開発本部の亀梨です。 普段はXmediaOneというメディアプランニング・広告運用管理・トラッキング・マーケティング分析を行う 統合プラットフォームの開発を担当しています。

さて、今回はScala入門第二弾として、Scalaの書き方を紹介する基礎編をお送りします。 Scala初心者のわたくしが、言語習得のために得た知識をここにまとめていきたいと思います。 主に参考にした記事及び書籍は以下の通りでっす。

また適宜コードの実行例を記していきますので、実際に動かしてみたい方は 事前にScalaおよびsbtをインストールしておいていただければと思います。 詳しい方法は前回の記事「Scala入門 準備編」をご覧ください。

基礎

変数

変数は「val」と「var」の二種類。valは後から変更できない変数で、副作用が起きないため使用を推奨されています。

  • val = イミュータブル(変更不可)
  • var = ミュータブル(変更可)
// valはイミュータブル
scala> val msg = "Hello, world!"
msg: String = Hello, world!

scala> msg = "Goodbye cruel world!"
<console>:8: error: reassignment to val
      msg = "Goodbye cruel world!"

// varはミュータブル
scala> var greeting = "Hello, world!
greeting: String = Hello, world!

scala> greeting = "Leave me alone, world!"
greeting: String = Hello, world!

演算子

Scalaは基本型を操作する演算子を豊富に提供しています。

算術演算子

scala> 1.2 + 2.3
res: Double = 3.5

scala> 3 - 1
res: Int = 2

scala> 'b' - 'a'
res: Int = 1

scala> 2L * 3L
res: Long = 6

scala> 11 / 4
res: Int = 2

scala> 11 % 4
res: Int = 3

scala> 11.0f / 4.0f
res: Float = 2.75

scala> 11.0 % 4.0
res: Double = 3.0

関係演算子と論理演算子

scala> 1 > 2
res: Boolean = false

scala> 1 < 2
res: Boolean = true

scala> 1.0 <= 1.0
res: Boolean = true

scala> 3.5f >= 3.6f
res: Boolean = false

scala> 'a' >= 'A'
res20: Boolean = true

scala> val untrue = !true
untrue: Boolean false

scala> val toBe = true
toBe: Boolean = true

scala> val question = toBe || !toBe
question: Boolean = true

scala> val paradox = toBe && !toBe
paradox: Boolean = false

演算子の優先順位

    (他のすべての特殊文字)
  1. * / %
  2. + -
  3. :
  4. = !
  5. < >
  6. &
  7. ^
  8. \|
  9. (すべての英字)
  10. (すべての代入演算子)

関数

関数の主な定義は以下の通りです。

  • 関数定義はdefで始まる
  • 関数名は慣例的にキャメルケースを使用(Javaと同じ)
  • 引数の定義は[変数名]: [データ型]の形式(複数可)
  • 引数のデータ型は省略不可
  • 返り値のデータ型は引数の後に: [データ型]の形式で行う
  • 返り値のデータ型は省略可能
  • 関数の定義と関数本体のブロックを=で繋ぐ必要がある
  • 関数本体が1行のみの場合は本体ブロックの中括弧{}は省略可
  • 本体ブロックで最後に実行された式が返り値となるのでreturnは不要
// 基本形
def max(x: Int, y: Int): Int = {
  if (x > y)
    x
  else
    y
}

// 省略した書き方(関数が一文のみから構成される場合)
def max(x: Int, y: Int) = if (x > y) x else y

引数の無い関数では、括弧を省略することができます

scala> def three() = 1 + 2
three: ()Int

scala> three()
res2: Int = 3

scala> three
res3: Int = 3

匿名(無名)関数

他の言語同様、Scalaでも匿名(無名)関数をつくることができます

scala> (x: Int) => x + 1
res2: (Int) => Int = <function1>

この関数はxという名前のIntに1を加えます

scala> res2(1)
res3: Int = 2

匿名関数を渡したり、valに保存することができます

scala> val addOne = (x: Int) => x + 1
addOne: (Int) => Int =

scala> addOne(1)
res4: Int = 2

関数が複数の式で構成されている場合は、{}を使用します

def timesTwo(i: Int): Int = {
  println("hello world")
  i * 2
}

可変長引数も使えます

def capitalizeAll(args: String*) = {
  args.map { arg =>
    arg.capitalize
  }
}

scala> capitalizeAll("rarity", "applejack")
res2: Seq[String] = ArrayBuffer(Rarity, Applejack)

型推論

Scalaでは型を明示的に指定しなくても、セットされた値から自動的に型を推測、判断し設定しています。 ただ、可読性や保守性を考えると基本的に型は明示的に設定すべきです。

scala> val msg = "Hello, world!"
msg: String = Hello, world!
     ^^^^^^ 型推論によって自動的に型が割当てられている

制御構文

他の言語と同様、while、foreach、forがあります。

whileによるループ

var i = 0
while (i < args.length) {
  if (i != 0)
    print(" ")
  print(args(i))
  i += 1
}
println()

foreachとforによる反復実行

// foreach
args.foreach(arg => println(arg))

// 型を明示したパターン
args.foreach((arg: String) => println(arg))

// 引数を明示せず省略して書くパターン
args.foreach(println)
// for
for (arg <- args)
println(arg)

※ <-記号を「in」と読めば、「for arg in args」ということになる

// 初めと終わりを指定するパターン
for (i <- 0 to 2)
print(greetStrings(i))

基本(データ)型

整数リテラル

Byte, Short, Int, Long, Charをまとめて整数値型と呼びます。 そしてFloat, Doubleを合わせて数値型と呼びます。

基本型 範囲
Byte 2の補数表現の8ビット符号付き整数(-2の7乗以上、2の7乗-1以下)
Short 2の補数表現の16ビット符号付き整数(-2の15乗以上、2の15乗-1以下)
Int 2の補数表現の32ビット符号付き整数(-2の31乗以上、2の31乗-1以下)
Long 2の補数表現の64ビット符号付き整数(-2の63乗以上、2の63乗-1以下)
Char 16ビット符号なしUnicode文字(0以上、2の16乗-1以下)
String Charのシーケンス
Float 32ビットのIEEE 754単精度浮動小数点数
Double 64ビットのIEEE 754単精度浮動小数点数
Boolean trueまたはfalse
Int
scala> val dec = 31
dec: Int = 31
Long

整数リテラルの末尾にLまたはlが付きます

scala> val tower = 35L
tower: Long = 35
Short、Byte

Short及びByte型は、Intリテラルを代入した場合、値がそれらの型の範囲内に収まっていれば、リテラルの型はShortやByteとして扱われます

scala> val littie: Short = 367
little: Short = 367

scala> val littler: Byte = 38
littler: Byte = 38
浮動小数点数リテラル

Float, Double = 小数点が含まれている、末尾にオプションのEまたはeが付いている Float = 末尾にFまたはfが付いています Double = Floatのオプションが付いていない、または末尾にDが付いている

scala> val big = 1.2345
big: Double = 1.2345

scala> val little = 1.2345F
little: Float = 1.2345

scala> val anotherDouble = 3e5 // or 3e5D
anotherDouble: Double = 300000.0
文字リテラル

シングルクォートで一文字を囲んだもの

scala> val a = 'A'
a: Char = A
文字列リテラル

ダブルクォートで文字を囲んだもの

scala> val hello = "hello"
hello: String = hello
複数行文字列リテラル

"""で囲むことで複数上の文字を表現できます

scala> var strings = """aaa
| bbb
| ccc"""
strings: String =
aaa
bbb
ccc

scala> println(strings)
aaa
bbb
ccc
シンボルリテラル

Scalaのシンボルは、シングルクォーテーション「'」に続く英文字で構成された認別子を使って表します。

scala> val s = 'asymbol
s: Symbol = 'aSymbol

Scala> val nm = s.name
nm: String = aSymbol
Booleanリテラル

trueとfalseの2個のリテラル

scala> val bool = true
bool: Boolean = true

scala> val fool = false
fool: Boolean = false

文字列補間

Scalaは柔軟な文字列補間の仕組みを備えており、それによって文字列リテラルを加工して式に含めることができます。

S補間子

文字列の中に変数の値を呼び出すことができます

val name = "reader"
println(s"Hello, $name!") // 出力:Hello, reader!

scala> s"The answer is ${6 * 7}."
res(): String = The answer is 42
raw補間子

S補間子と違い、文字列リテラルのエスケープシーケンスを認識しません

println(raw"No¥¥¥¥escape!") // 出力:NO¥¥¥¥escape!
f補間子

書式を指定して出力します

scala> f"${math.Pi}%.5f}"
res1: String = 3.14159

UNIT

関数が意味する値を返さない場合の結果型(returnしていない)

scala> def greet() = println("Hello, world!)
greet: ()Unit

配列(Array)

配列は順序を保持し、重複を含むことができ、変更可能、異なる型の値を保持できる

scala> val numbers = Array(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
numbers: Array[Int] = Array(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)

scala> numbers(3) = 10

リスト(List)

リストは順序を保持し、重複を含むことができ、不変、異なる型の値を保持できる

scala> val numbers = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
numbers: List[Int] = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)

scala> numbers(3) = 10
:9: error: value update is not a member of List[Int]
numbers(3) = 10

重要なListメソッドとその使い方

コード 意味
List() または Nil 空リスト
List("Cool", "tools", "rule") "Cool", "tools", "rule" の3個の値を持つ新しいList[String]を作る
val thrill = "Will" :: "fill" :: "until" :: Nil "Will", "fill", "until" の3個の値を持つ新しいList[String]を作る
List("a", "b") ::: List("c", "d") 2個のリストを連結する("a", "b", "c", "d"の4個の値を持つ新しいList[String]を返す)
thrill(2) thrillリストの添字2(先頭は0)の要素を返す("until"が返される)
thrill.count(s => s.length == 4) thrillの中で長さが4の要素の数を返す(2が返される)
thrill.drop(2) 先頭の2要素を取り除いたthrillリストを返す(List("until")が返される
thrill.dropRight(2) 末尾の2要素を取り除いたThrillリストを返す(List("Will")が返される)
thrill.exists(s => S == "until") thrillの中に値が"until"の要素があるかどうかを返す(trueを返す)
thrill.filter(s => s.length == 4) thrillに含まれる長さが4のすべての要素を順に並べたリストを返す(List("Will", "fill")が返される)
thrill.forall(s => s.endsWith("l") thrillリストのすべての要素について、末尾の文字が"l"になっているかどうかを返す(trueを返す)
thrill.foreach(s => print(s)) thrillリストに含まれる1つ1つの文字列を対象としてprint文を実行する("Willfilluntil"と表示される)
thrill.foreach(print) 上の省略版
thrill.head thrillリストの戦闘要素を返す("Will"が返される)
thrill.init thrillリストから末尾の要素を除いた残りのリストを返す(List("Will", "fill")が返される)
thrill.isEmpty thrillリストが空かどうかを返す(falseが返される)
thrill.last thrillリストの最後の要素を返す("until"が返される)
thrill.map(s => s + "y") thrillリストの各要素に"y"を追加したものから構成されるリストを返す(List("Willy", "filly", "untily")が返される
thrill.mkString(", ") 引数に渡した区切り文字でリストの要素を結合した文字列を作る("Will, fill, until"が返される)
thrill.filterNot(s => s.length == 4) thrillリストに含まれる長さが4以外のすべての要素を順に並べたリストを返す(List("until")が返される)
thrill.reverse thrillリストのすべての要素を逆に並べたリストを返す(List("until", "fill", "Will")が返される)
thrill.sortWith((s, t) => s.charAt(0).toLower < t.charAt(0).toLower) thrillリストのすべての要素から、先頭文字を小文字にしてアルファベット順を決めて、その順序に従ったリストを作って返す(List("fill", "until", "Will")が返される)
thrill.tail thrillリストから戦闘要素を取り除いたものを返す(List("fill", "until")が返される)

Set(集合)

Setは順序を保持せず、重複を持たない

scala> val numbers = Set(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
numbers: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)

// イミュータブルな集合の作成・初期化・操作
var jetSet = Set("Boeing", "Airbus")
jetSet += "Lear" // jetSet = jetSet + "Lear" の略記法
println(jetSet.contains("Cessna")) // false

// ミュータブルな集合の作成・初期化・操作
import scala.collection.mutable
val movieSet = mutable.Set("Hitch", "Poltergeist")
movieSet += "Shrek"
println(movieSet)

Tuple(タプル)

リストと同様にイミュータブルだが、リストとは異なり、違う型の要素を持つことができる

// タプルを作って使う
val pair = (99, "Luftballons")
println(pair._1)
println(pair._1)

※上記の場合の型は「pair2[Int, String]」となる。

ケースクラスとは異なり、名前付きアクセサーはない。 代わりに、位置に基づいて名前が付けられ、0ベースではなく1ベースのアクセサーがある。

scala> hostPort._1
res0: String = localhost

scala> hostPort._2
res1: Int = 80

タプルはパターンマッチングにうまく収まります。

hostPort match {
  case ("localhost", port) => ...
  case (host, port) => ...
}

Map(マップ)

// イミュータブルマップの作成・初期化・操作
val romanNumeral = Map(
1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"
)
println(romanNumeral(4))

// ミュータブルマップの作成・初期化・操作
import scala.collection.mutable
val treasureMap = mutable.Map[Int, String]()
treasureMap += (1 -> "Go to island.")
treasureMap += (2 -> "Find big X on ground")
treasureMap += (3 -> "Dig.")
println(treasureMap(2))

クラスとオブジェクト

概要をまとめると以下の通りです。

    • クラス定義の中には、フィールド(field)とメソッド(method)を配置する
    • フィールドはオブジェクトを参照する変数で、valまたはvarで定義する

<liメソッドは実行可能なコードを格納するもので、defを使って定義する。

アクセス修飾子

Scalaでは明示的にアクセス修飾子を指定しなければ、メンバーは公開(public)となる。つまりデフォルトが公開なので明示的に書かなくて良いです。 また、フィールドの前に「private」というアクセス修飾子を挿入すると非公開となります。 privateを宣言した非公開フィールドにアクセスできるのは、同じクラス内で定義されたコードだけになります。

class ChecksumAccumulator {
  private var sum = 0
}

// 以下はsumが非公開なのでコンパイラーを通らない
val acc = new ChecksumAccumulator
acc.sum = 5

メソッド

メソッドに渡されるパラメーターはvarではなくvalなので、再代入ができません。

def add(b: Byte): Unit = {
  b = 1 // bはvalなので、コンパイラーを通らない
}

コーディングスタイル

基本形
  • 文末のセミコロンはオプションのため記述不要
class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte): Unit = {
    sum += b
  }
  def checksum(): Int ={
    return ~(sum &amp; 0xFF) + 1
  }
}
省略形
  • returnを明示しない
  • 中括弧を省略
  • 結果型を省略
class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte) = sum += b
  def checksum() = ~(sum &amp; 0xFF) + 1
推奨形
  • returnを明示しない
  • 型は省略せず(型推論を使わず)書く
class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte): Unit = { sum += b }
  def checksum(): Int = ~(sum &amp; 0xFF) + 1

コンストラクタ

コンストラクタはクラス内のメンバーフィールドを初期化するのに使います

class Calculator(brand: String) {
  /**
   * A constructor.
   */
  val color: String = if (brand == "TI") {
    "blue"
  } else if (brand == "HP") {
    "black"
  } else {
    "white"
  }

  // An instance method.
  def add(m: Int, n: Int): Int = m + n
}

シングルトンオブジェクト

Scalaはクラスが静的メンバー(static member)を持てない代わりに、シングルトンオブジェクト(singleton objects)を持ちます。 特徴は以下の通りです。

  • シングルトンオブジェクトの定義は、classキーワードの代わりにobjectキーワードを使う
  • シングルトンオブジェクトがクラスと同じ名前を持つとき、そのクラスの「コンパニオンオブジェクト」と呼ばれる
  • クラスとコンパニオンオブジェクトは互いの非公開メンバーにアクセスできる

オブジェクト

オブジェクトは、クラスの単一のインスタンスを保持するために使用されます。

object Timer {
  var count = 0

  def currentCount(): Long = {
    count += 1
    count
  }
}

// 使い方
scala&gt; Timer.currentCount()
res0: Long = 1

継承

他の言語と同様、継承が使えます

class ScientificCalculator(brand: String) extends Calculator(brand) {
def log(m: Double, base: Double) = math.log(m) / math.log(base)
}

メソッドの上書きも可能です

class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {
def log(m: Int): Double = log(m, math.exp(1))
}

抽象クラス

抽象クラスは、いくつかのメソッドを定義して実装していないクラスを定義することができます。 代わりに、抽象クラスを継承するサブクラスがこれらのメソッドを定義します。 抽象クラスのインスタンスを作成することはできません。

scala> abstract class Shape {
| def getArea():Int // subclass should define this
| }
defined class Shape

scala> class Circle(r: Int) extends Shape {
| def getArea():Int = { r * r * 3 }
| }
defined class Circle

scala> val s = new Shape
:8: error: class Shape is abstract; cannot be instantiated
val s = new Shape
^

scala> val c = new Circle(2)
c: Circle = Circle@65c0035b

トレイト

Scalaのトレイトはクラスに比べて以下のような特徴があります。

  • 1つのクラスでは、withキーワードを使用して複数のトレイトを継承することができます
  • 直接インスタンスすることができない
  • コンストラクタの引数を取ることができない
trait Car {
  val brand: String
}

trait Shiny {
  val shineRefraction: Int
}
class BMW extends Car {
  val brand = "BMW"
}
class BMW extends Car with Shiny {
  val brand = "BMW"
  val shineRefraction = 12
}

パッケージ

パッケージ内でコードを整理することができます。 ファイルの先頭に、そのパッケージ内にあるすべてのファイルが宣言されます。

値と関数は、クラスやオブジェクトの外に置くことはできません。 オブジェクトは、静的関数を編成するのに便利なツールです。

package com.twitter.example

object colorHolder {
  val BLUE = "Blue"
  val RED = "Red"
}

パッケージ内のメンバーに直接アクセスできます。

println("the color is: " + com.twitter.example.colorHolder.BLUE)

パターンマッチング

Scalaでは便利なパーツに match があります

val times = 1

// valueにマッチ
times match {
  case 1 =&gt; "one"
  case 2 =&gt; "two"
  case _ =&gt; "some other number"
}

// if文での判定もできる
times match {
  case i if i == 1 =&gt; "one"
  case i if i == 2 =&gt; "two"
  case _ =&gt; "some other number"
}

様々なタイプのマッチング

さまざまな型の値を別々に扱うためにmatchを使うことができます。

def bigger(o: Any): Any = {
  o match {
    case i: Int if i  i - 1
    case i: Int =&gt; i + 1
    case d: Double if d  d - 0.1
    case d: Double =&gt; d + 0.1
    case text: String =&gt; text + "s"
  }
}

以上

というわけで、ズラッとScalaの書き方についてまとめてみました。 ただのまとめっぽくなってしまいましたが、Scalaの雰囲気を感じ取ってもらえればと思います。

次回は、実際にScalaで何か作っていく内容にして、より実践的にしていこうかと思います。 それではまた次回!