前提
- Javaの学習を一通り終えた投稿者が、Javaと相互運用性のある関数型プログラミング言語 Scala について印象を述べる記事
- 感想等ありましたら、お気軽にコメントください
概要
- オブジェクト指向言語兼、本格的な関数型言語
なぜScalaなのか
互換性
- JVM上で動くため、Javaと互換運用性がある
簡潔性
- Javaでかく場合の半分以下のコード量になる
MyClass.java
class MyClass {
private int age;
private String name;
public MyClass(int age, String name) {
this.age = age;
this.name = name;
}
}
以下と同じ
MyClass.scala
class MyClass(var age: Int, var name: String) {}
- 上記だけ見ると省略できすぎて
- ちょっと魔法っぽい
String型
- Javaのjava.lang.String型が使われている
定数、変数
- 型推論機能がある
val hoge = "huga" // 代入不可
var hoge = "huga" // 代入可
関数
- パラメータ、戻り値の型を指定する
def max(x: Int, y: Int): Int = {
if (x > y) x else y
}
def max(x: Int, y: Int): Int = if (x > y) x else y // 1行で済むなら中括弧を省略できる
リスト連結
- Listオブジェクトはリストを連結できる
:::
というメソッドを持っている - また、
::
で、リスト先頭に要素を追加できる
val oneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoThreeFour = oneTwo ::: threeFour
val zeroOneTwoThreeFour = 0 :: oneTwoThreeFour
タプル
- 便利なコンテナオブジェクトで、
List[String], List[Int]
のように制限なく文字列や数値を配列要素として挿入できる
trait
- javaでいうインターフェースのようなもの
- クラス、オブジェクトはtraitを継承できるが、traitはインスタンス化ができない
trait Iterator[A] {
def hasNext: Boolean
def next(): A
}
継承 extends
- traitのメソッドにアクセスできる
class IntIterator(to: Int) extends Iterator[Int] {
実装 override
ー traitで抽象化されたメソッドを実装する
trait Iterator[A] {
def hasNext: Boolean
def next(): A
}
class IntIterator(to: Int) extends Iterator[Int] {
private var current = 0
override def hasNext: Boolean = current < to
override def next(): Int = {
if (hasNext) {
val t = current
current += 1
t
} else 0
}
}
val iterator = new IntIterator(10)
iterator.next() // returns 0
iterator.next() // returns 1
余談
- Javaでは継承と実装を同時にやる際、クラス定義時にextendsとinclementsを書かなければいけなかったが、
- scalaでは定義時、extendsするだけで良いのかな?
mixin
- 継承せずに継承したかのように振る舞い、メソッド等を使える
- クラスは一つしかスーパークラスを持つ事ができないが、ミックスインは複数持てる
- ミックスインされるクラスはtraitで作成する
abstract class A {
val message: String
}
class B extends A {
val message = "I'm an instance of class B"
}
trait C extends A {
def loudMessage = message.toUpperCase()
}
class D extends B with C
val d = new D
println(d.message) // I'm an instance of class B
println(d.loudMessage) // I'M AN INSTANCE OF CLASS B
なぜミックスインなのか
- Javaでは菱形継承問題という多重継承の課題があった
- それを改善するためにこの仕様を考えたのかもしれない。(違ってたらコメントください)
メソッドをネストする
def factorial(x: Int): Int = {
def fact(x: Int, accumulator: Int): Int = {
if (x <= 1) accumulator
else fact(x - 1, x * accumulator)
}
fact(x, 1)
}
println("Factorial of 2: " + factorial(2)) // 2
println("Factorial of 3: " + factorial(3)) // 6
カリー化
-
複数の引数を受け取る関数を
-
1つの引数を受け取り、残りの引数を受け取る関数を返す関数に変換すること
- ちょっとややこしすぎない?
-
1つの引数を受け取り、残りの引数を受け取る関数を返す関数に変換すること
val add = {(x:Int, y:Int, z:Int) => x + y + z}
val addCurried = add.curried
val addCurriedWithX = addCurried(1)
val addCurriedWithXY = addCurriedWithX(2)
println(addCurriedWithXY(3)) //この時点で6が返される
-
ステフィンカリーがどうしても頭によぎるカリーになれるならなりたいっす
ケースクラス
- 不変なデータを作るのに適しているクラス
- インスタンス化するときにnewを使わない
- ケースクラスがオブジェクトの生成を行うapplyメソッドを標準で保有するため
case class Message(sender: String, recipient: String, body: String)
val message1 = Message("guillaume@quebec.ca", "jorge@catalonia.es", "Ça va ?")
println(message1.sender) // guillaume@quebec.ca が出力されます
message1.sender = "travis@washington.us" // この行はコンパイルされません
参照ではなく、構造で比較される
- 引数に入れた値が一緒ならインスタンスのアドレスが異なっても等値扱い
- ==は等値でequals()は等価だとJavaで習った
case class Message(sender: String, recipient: String, body: String)
val message2 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
val message3 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
val messagesAreTheSame = message2 == message3 // true
コピー
- copyメソッドを使うことで簡単にケースクラスのインスタンスの(浅い)コピーを作ることができる
- 必要に応じて、コンストラクタ引数を変更することもできる
case class Message(sender: String, recipient: String, body: String)
val message4 = Message("julien@bretagne.fr", "travis@washington.us", "Me zo o komz gant ma amezeg")
val message5 = message4.copy(sender = message4.recipient, recipient = "claire@bourgogne.fr")
message5.sender // travis@washington.us
message5.recipient // claire@bourgogne.fr
message5.body // "Me zo o komz gant ma amezeg"
マッチング
パターンガード
- パターンの後ろにif文をつける
def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = {
notification match {
case Email(sender, _, _) if importantPeopleInfo.contains(sender) =>
"You got an email from special someone!"
case SMS(number, _) if importantPeopleInfo.contains(number) =>
"You got an SMS from special someone!"
case other =>
showNotification(other) // 特別なものではなく、オリジナルのshowNotification関数に委譲します。
}
}
val importantPeopleInfo = Seq("867-5309", "jenny@gmail.com")
val someSms = SMS("867-5309", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("jenny@gmail.com", "Drinks tonight?", "I'm free after 5!")
val importantSms = SMS("867-5309", "I'm here! Where are you?")
println(showImportantNotification(someSms, importantPeopleInfo))
println(showImportantNotification(someVoiceRecording, importantPeopleInfo))
println(showImportantNotification(importantEmail, importantPeopleInfo))
println(showImportantNotification(importantSms, importantPeopleInfo))
- これ他の言語でもあった?
型のみのマッチング
- 以下のように型のみでマッチングする事ができる
abstract class Device
case class Phone(model: String) extends Device {
def screenOff = "Turning screen off"
}
case class Computer(model: String) extends Device {
def screenSaverOn = "Turning screen saver on..."
}
def goIdle(device: Device) = device match {
case p: Phone => p.screenOff
case c: Computer => c.screenSaverOn
}
シールドクラス
- traitまたはクラスにsealdをつけると、
- 全てのサブタイプは同一ファイル内で宣言されなければならなくなる。
sealed abstract class Furniture
case class Couch() extends Furniture
case class Chair() extends Furniture
def findPlaceToSit(piece: Furniture): String = piece match {
case a: Couch => "Lie on the couch"
case b: Chair => "Sit on the chair"
}
シングルトンオブジェクト
- 一つのクラスから一つのインスタンスしか作れない。
package logging
object Logger {
def info(message: String): Unit = println(s"INFO: $message")
}
上記で作ったinfoは、下記のように使われる
import logging.Logger.info //メソッド名まで記述して、呼び出せるようにする
class Project(name: String, daysToComplete: Int)
class Test {
val project1 = new Project("TPS Reports", 1)
val project2 = new Project("Website redesign", 5)
info("Created projects") // Prints "INFO: Created projects"
}
コンパニオンオブジェクト
- クラスと同じ名前のオブジェクト
- 逆にそのクラスはオブジェクトのコンパニオンクラスと呼ばれる
- コンパニオンオブジェクト、コンパニオンクラスは自信のコンパニオンのプライベートメンバーにアクセスできる。
import scala.math._
case class Circle(radius: Double) {
import Circle._
def area: Double = calculateArea(radius)
}
object Circle {
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}
val circle1 = Circle(5.0)
circle1.area
class Email(val username: String, val domainName: String)
object Email {
def fromString(emailString: String): Option[Email] = {
emailString.split('@') match {
case Array(a, b) => Some(new Email(a, b))
case _ => None
}
}
}
val scalaCenterEmail = Email.fromString("scala.center@epfl.ch")
scalaCenterEmail match {
case Some(email) => println(
s"""Registered an email
|Username: ${email.username}
|Domain name: ${email.domainName}
""".stripMargin)
case None => println("Error: could not parse email")
}
抽出子
-
unapplyメソッドが定義されているオブジェクトのこと
- 受け取ったインスタンスを分解して、 何かを抽出する役割がある
-
パターンマッチングと部分関数で頻繁に使われる
User.scala
class User(private val firstname: String, private val lastname: String)
object User {
def unapply(user: User) = Option((user.firstname, user.lastname))
}
上記のUserコンパニオンオブジェクトの抽出子で、Userインスタンスを受け取り、苗字と名前に分解している
Main.scala
object Main extends App {
val user = new User("剛次", "赤石")
user match {
case User(firstname, lastname) => println(lastname + " " + firstname)
case _ => println("Not user.")
}
}
- 上記のMainオブジェクトのマッチングで
User(firstname, lastname)
というcase指定があるが、これはunapplyメソッドを呼び出している?らしい- ちょっと難解では
for内包表記
- シンプルにリストを操作するための書き方
-
for (enumerators) yield e
という形をとる - yieldをつけると
List[String]
型を返し、つけないとUnit
型を返す
case class User(name: String, age: Int)
val UserBase = List(User("taro", 40),
User("joro", 28),
User("Saburo", 18),
User("kyoko", 25))
val twentySomethings = for (user <- UserBase if (user.age >= 20 && user.age <= 30))
yield user.name
twentySomethings.foreach(name => println(name))
可変長引数と再帰データ型
- 型の後に*をつけることによって、適当な個数の引数を任意にとれる
case class Person(name: String, age: Int, child: Person*)
val child1 = Person("Child1", 5)
val child2 = Person("Child2", 3)
val parent = Person("Parent", 30, child1, child2)
変位指定
- クラスの型パラメータがどのように継承されるかを決める仕組み
- クラスの再利用性を高め、型システムをより強力にするための重要な機能
共変
前談
- ぱっと見、以下の仕様と似ていると思った
- Javaの変数宣言の型で、親のクラス型を指定しても良い
- インスタンス化は子クラス
- Javaの変数宣言の型で、親のクラス型を指定しても良い
public class Human {
...
}
public class Man extends Human {
...
}
Human ron = new Man();
ManはHumanだよね(is-aの原則)ってことでコンパイルエラーにならない
本題
- List[A]がList[B]のサブタイプというようなことを利用して、仮引数にList[A]を指定しても、実引数でサブタイプ元のList[B]を指定できる、という仕様
- 合ってる自信ない(間違ってたらご指摘ください)
abstract class Animal {
def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
object CovarianceTest extends App {
def printAnimalNames(animals: List[Animal]): Unit = {
animals.foreach { animal =>
println(animal.name)
}
}
val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))
printAnimalNames(cats) // 実引数でサブタイプ元のAnimalを指定してるが、仮引数でCatを指定
// Whiskers
// Tom
printAnimalNames(dogs) // 実引数でサブタイプ元のAnimalを指定してるが、仮引数でDogを指定
// Fido
// Rex
}
反変
- 共変とちょうど対になる性質
- 型パラメータを持ったクラスG、型パラメータAとBがあったとき、A が B を継承しているときにのみ、
val : G[A] = G[B]
が成り立つ
内部クラス
- クラスの中にクラスを定義できる
Unit型
- 値が無いことを表す型で、値をただ一つのみ持つ
- 値として意味がないことを示す
def printInt(x:int): Unit = println(x)
遅延評価
- 大雑把に言うと、必要になるまで計算を遅らせる機能
- 通常は、変数や関数の結果は宣言されたときにすぐに計算されるが、遅延評価では実際に値が必要になるまで計算を行わない
- 使いたい場合は、lazyキーワードを使う
lazy val lazyValue = {
println("計算しています...")
42
}
println("まだ計算していません")
println(lazyValue) // この時点で初めて計算が行われる
println(lazyValue) // 既に計算済みなので再度計算しない
- 遅延評価は、キャッシュされるため一度計算した結果は再評価されない
- 望ましくない場合は、再計算が必要な状況に応じて工夫する必要がある
まとめ
- 抽出子と変位指定がわかりづらかったので間違っている場合はご指摘ください。