Scalaのいろいろな言語機能
apply
メソッドがあるもは、関数として呼び出すことができる。
object CustomerID {
def apply(name: String) = s"$name|${Random.nextLong()}"
}
val id = CustomerID("John")
unapply
メソッドは主に部分関数やパターンマッチングで使われる。
このメソッドがあるものを抽出子オブジェクトと言う。
unapply
メソッド:
- 引数を一つ取る?
- 戻り値は
Option
型 - これがあるとパターンマッチングの
case
節に使えるようになる
使用例:
import scala.util.Random
object CustomerID {
def apply(name: String) = s"$name--${Random.nextLong()}"
// idはmatchに使われている値
def unapply(id: String): Option[String] = {
val array = id.split("--")
array match {
case Array(name, _) => Some(name)
case _ => None
}
}
}
val id = CustomerID("John")
id match {
case CustomerID(name) => s"id name: $name" // nameはunapplyの戻り値
case _ => "not id"
}
Some
にタプルを入れると、パターンマッチング時に値を分解できる。
def unapply(id: String): Option[(String, String)] = {
val array = id.split("--")
array match {
case Array(name, random) => Some(name, random) // 二つを同時に返す
case _ => None
}
}
使用例:
case CustomerID(name, _) => s"id name: $name"
値を宣言する文では、以下のように記述できる。
val id = CustomerID("John")
val CustomerID(_, random) = id: @unchecked
println(s"random: $random")
内部ではunapply
が呼び出されている。
@unchecked
はWarning回避のためにつけている。
unapply
の結果がNone
だった場合MatchError
が投げられる、という警告が出るため。
unapply
の戻り値は、ただのテストの場合Boolean
にすることができる。
def unapply(id: String): Boolean = {
val array = id.split("--")
array.length == 2
}
例えばパターンマッチングでマッチしているかどうかの判定に使われる。
val id = CustomerID("John")
id match {
case CustomerID() => "id"
case _ => "not id"
}
抽出子オブジェクト、ちょっとTypeScriptのinfer
に似てる
type Awaited<T> = T extends Promise<infer U> ? U : never;
until
を使うとRange
を生成できる。
これはコレクションの一つなので、map
などのメソッドを使える。
(0 until 10).map(_ * 2)
unapply
で偶数と奇数をパターンマッチングする遊び
object Even {
def unapply(num: Int): Option[Int] =
if(num % 2 == 0) Some(num) else None
}
(0 until 10) foreach {
case Even(n) => println(n)
case _ => ()
}
for
内容表記を使うと、他言語のfor
文みたいなことができる。
for (num <- 0 until 10) println(num)
<-
の正体は謎。
for
内包表記は式なので、値が返ってくる。
その中身はコレクションで、yield
で追加できる。
for (num <- 0 until 10) yield num * 2
// これと同じ
(0 until 10).map(_ * 2)
yield
の後には値を書ける。
yield
を使わず、副作用を伴う処理を書くこともできる。
val arr = for (row <- 0 until 5; column <- 0 until 5)
yield if ((row + column) % 2 == 0) "black" else "white"
↑のように;
で区切るといくつかの値を使える。
このとき、後に書いたものが先に増える。
また、多次元配列にはならない。
特定の条件の場合のみループを回したい場合は、if
ガードが使える。
以下は左上から右下の座標を出力する例。
val arr = for (row <- 0 until 5;
column <- 0 until 5 if row == column)
yield (row, column)
クラスの型パラメータに変位指定することができる。
パラメータを持つクラスClass
があり、二つの型A
とB
の場合、変異指定によってClass[A]
とClass[B]
の関係を決めることができる。
- 非変(デフォルト): A = Bのみを許容する
- 共変(
+
): AがBのサブタイプならOK - 反変(
-
): BがAのサブタイプならOK
上限型境界では、型パラメーターに入れられる型を制限できる。
abstract class Animal {
def name: String
}
case class Dog(name: String, age: Int) extends Animal
case class Cat(name: String, color: String) extends Animal
// 上限型限界
class AnimalList[A <: Animal](animals: List[A]) {
def findAnimalName(name: String): Option[A] =
animals.find(_.name == name)
}
val list = new AnimalList(List(
Dog("ぽち", 3),
Dog("こむぎ", 2),
))
list.findAnimalName("こむぎ")
findAnimalName
の結果が存在するときのみ処理する:
list.findAnimalName("こむぎ") foreach {
dog => println(s"name: ${dog.name} age: ${dog.age}")
}
このforeach
はOption
型のメソッドであり、コレクションのものではない。
内部実装はこんな感じらしい。
option match {
case Some(x) => f(x)
case None => ()
}
これを使うことで、None
の時の処理を省略できる。
ちなみに、↑でdog
がDog
型だということがわかるのは、型パラメーターを使っているから。
これはコンストラクタでanimal: Animal
と指定していたらできない。
// 型は書かなくても動くけど、ミスってCatなんかを混ぜないように書いておいたほうがいい
val list = new AnimalList[Dog](List(
Dog("ぽち", 3),
Dog("こむぎ", 2),
))
以下の実装だと、AnimalList
にDog
しか格納されてないということが表現できない。
そのためdog.age
はエラーになる。
class AnimalList(animals: List[Animal]) {
def findAnimalName(name: String) =
animals.find(_.name == name)
}
list.findAnimalName("こむぎ") foreach {
dog => println(s"name: ${dog.name} age: ${dog.age}") // Error!
}
上限型境界、TypeScriptのextends
に似てる気がする。
内部クラスでは、クラスの中にクラスを書くことができる。
class Graph {
class Node
}
val graph1 = new Graph
val node1 = new graph1.Node
このとき、node1
はgraph1.Node
型となる。
Graph.Node
型ではないので注意。
例えば、Node
のファクトリーメソッドを用意して、グラフにノードを所属させる。
class Graph {
protected class Node
var nodes: List[Node] = Nil
def createNode = {
val n = new Node
nodes = n :: nodes
n
}
}
val graph1 = new Graph
val node1 = graph1.createNode
このとき、クラスの修飾子をprivate
にすると型エラーが出た。
なんで?
Scalaのコンパイラは、別のクラスで作られたインスタンスを混ぜることを許さない。
というのも、↑の例ではNode
型は自身のインスタンスにのみ適用される型だから。多分。
val graph1 = new Graph
val node1 = graph1.createNode // graph1.Node型
val node2 = graph1.createNode
node1.connect(node2) // OK
val graph2 = new Graph
val node3 = graph2.createNode // graph2.Node型
node3.connect(node2) // ERROR!
connect
周りの実装:
protected class Node {
var connectedNodes: List[Node] = Nil
def connect(node: Node) = {
connectedNodes = node :: connectedNodes
}
}
他のインスタンスで作られたものを許容したければ、親クラス名#内部クラス名
が使える。
protected class Node {
var connectedNodes: List[Graph#Node] = Nil
def connect(node: Graph#Node): Unit = {
connectedNodes = node :: connectedNodes
}
}
Nil
は空の配列を示す値。
List()
と同じ?
抽象型メンバーを使うと、メンバーの型を継承(ミックスイン)先で自由に決めることができる。
使い方は型パラメーターと被りそう。
「なんでもいい」を表現するときに型パラメータでAny
を使う必要がなくなる?
それとも、継承が禁止されてるクラスなら自由度が減る?
with
を使うと複合型を作れる。
複合型は型と型を混ぜ合わせたもの。
trait Printable {
def print(): Unit
}
trait Resettable {
def reset(): Unit
}
def printAndReset(obj: Printable with Resettable): Printable = {
obj.print()
obj.reset()
obj
}
似てる...
function printAndReset(obj: Printable & Resettable) {
this
を使うと自分型にアクセスできる?
名前の競合が起こったときに使えそう。
def check(username: String, password: String): Boolean = {
username == this.username
&& PasswordUtil.getHash(password) == getPasswordHash
}
自分型ってこういう使い方でいいのか...?
ここではユーザーの実装で実際のパスワードを隠蔽したかった
object PasswordUtil {
// ハッシュ値を返すメソッド
def getHash(password: String) = s"hash: $password"
}
trait Password {
protected val password: String
protected def getHash = PasswordUtil.getHash(password)
}
trait User_ {
this: Password =>
def getPasswordHash = getHash
protected val username: String
def check(username: String, password: String): Boolean = {
username == this.username
&& PasswordUtil.getHash(password) == getPasswordHash
}
}
class User(
protected val username: String,
protected val password: String
) extends User_ with Password {
}
(そもそもパスワードをどこかに保存すること自体が間違いか)
(というか目的が不明すぎて迷走してる間は否めない)
↑使用例
val user = new User("admin", "pass1")
user.getPasswordHash // hash: pass1
user.check("一般ユーザー", "my_pass") // false
implicit
を使うと暗黙の値を表現できる。
これはメソッドのパラメーターの前につける。
暗黙のパラメータは、Scalaが使える値を探してくれる。
暗黙の変換もあるらしい。
暗黙の変換は見境なく使われると落とし穴になり得るため、暗黙の変換の定義をコンパイルしている時にコンパイラは警告を出します。
メソッドの型パラメーターと再帰
def listOfDuplicates[A](value: A, count: Int): List[A] = {
if (count < 1)
Nil
else
value :: listOfDuplicates(value, count - 1)
}
使用例
println(listOfDuplicates[Int](3, 4)) // List(3, 3, 3, 3)
println(listOfDuplicates("La", 8)) // List(La, La, La, La, La, La, La, La)
再帰メソッドは型推論ができないので、自分で型を定義する必要がある。
TypeScript版
function listOfDuplicates<A>(value: A, count: number): A[] {
if (count < 1)
return []
else
return [value, ...listOfDuplicates(value, count - 1)]
}
console.log(listOfDuplicates(3, 4)) // [3, 3, 3, 3]
console.log(listOfDuplicates("La" as const, 8)) // [La, La, La, La, La, La, La, La]
(配列の長さを型に落とし込むのは諦めた)
演算子を自分で定義する
case class Node[T](elem: T) {
// Node + Node = Node.elemを入れたList
def +(newNode: Node[T]): List[T] = List(elem, newNode.elem)
}
val node1 = Node("Hello")
val node2 = Node("World!")
(node1 + node2).mkString(" ") // Hello World!
@
から始まるアノテーションを使うことができる。
例えば@deprecated
はメソッドやクラスなどが非推奨であることを示す。
Scalaにはパッケージ機能がある。
パッケージはscala/main/パッケージ名
ディレクトリに作るのが一般的。
また、ファイルの先頭にpackage パッケージ名
を書く。
例: user
パッケージでUser
ケースクラスを宣言し、sample
パッケージでインポートする
package user
case class User(username: String, password: String)
package sample
@main def hello(): Unit = {
import user.User
println("Hello World!")
val user1 = User("abc", "p@ssw0rd")
}
↑のように、import
をメソッド内などの好きな場所で使うことができる。
パッケージ名と値の名前を被せると、再帰的インポートとやらでエラーになるので注意。
トップレベルのメソッドや値もパッケージの一員となる。
なので、以下のcreateUser
とadmin
はインポートできる。
package user
def createUser(username: String, password: String): User = {
val id = 1 // auto inclement
User(id, username, password)
}
val admin = createUser("admin", "p@ssw0rd")
おわり!