Closed54

Scalaのいろいろな言語機能

nanasinanasi

applyメソッドがあるもは、関数として呼び出すことができる。

object CustomerID {
  def apply(name: String) = s"$name|${Random.nextLong()}"
}

val id = CustomerID("John")
nanasinanasi

unapplyメソッドは主に部分関数やパターンマッチングで使われる。
このメソッドがあるものを抽出子オブジェクトと言う。

nanasinanasi

unapplyメソッド:

  • 引数を一つ取る?
  • 戻り値はOption
  • これがあるとパターンマッチングのcase節に使えるようになる
nanasinanasi

使用例:

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"
}
nanasinanasi

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"
nanasinanasi

値を宣言する文では、以下のように記述できる。

val id = CustomerID("John")
val CustomerID(_, random) = id: @unchecked
println(s"random: $random")

内部ではunapplyが呼び出されている。

@uncheckedはWarning回避のためにつけている。
unapplyの結果がNoneだった場合MatchErrorが投げられる、という警告が出るため。

nanasinanasi

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"
}
nanasinanasi

抽出子オブジェクト、ちょっとTypeScriptのinferに似てる

type Awaited<T> =  T extends Promise<infer U> ? U : never;
nanasinanasi

untilを使うとRangeを生成できる。
これはコレクションの一つなので、mapなどのメソッドを使える。

(0 until 10).map(_ * 2)
nanasinanasi

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 _ => () 
}
nanasinanasi

for内容表記を使うと、他言語のfor文みたいなことができる。

for (num <- 0 until 10) println(num)

<-の正体は謎。

nanasinanasi

for内包表記は式なので、値が返ってくる。
その中身はコレクションで、yieldで追加できる。

for (num <- 0 until 10) yield num * 2

// これと同じ
(0 until 10).map(_ * 2)
nanasinanasi

yieldの後には値を書ける。
yieldを使わず、副作用を伴う処理を書くこともできる。

val arr = for (row <- 0 until 5; column <- 0 until 5)
  yield if ((row + column) % 2 == 0) "black" else "white"
nanasinanasi

↑のように;で区切るといくつかの値を使える。
このとき、後に書いたものが先に増える。
また、多次元配列にはならない。

nanasinanasi

特定の条件の場合のみループを回したい場合は、ifガードが使える。
以下は左上から右下の座標を出力する例。

val arr = for (row <- 0 until 5;
               column <- 0 until 5 if row == column)
  yield (row, column)
nanasinanasi

クラスの型パラメータに変位指定することができる。
パラメータを持つクラスClassがあり、二つの型ABの場合、変異指定によってClass[A]とClass[B]の関係を決めることができる。

  • 非変(デフォルト): A = Bのみを許容する
  • 共変(+): AがBのサブタイプならOK
  • 反変(-): BがAのサブタイプならOK
nanasinanasi

反変はこの逆。
例えば、Printer[Cat]としてPrinter[Animal]が使えるという特殊な状況があるとき、これを使うことができる。
これ使い道あるん?

nanasinanasi

上限型境界では、型パラメーターに入れられる型を制限できる。

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("こむぎ")
nanasinanasi

findAnimalNameの結果が存在するときのみ処理する:

list.findAnimalName("こむぎ") foreach {
  dog => println(s"name: ${dog.name} age: ${dog.age}")
} 

このforeachOption型のメソッドであり、コレクションのものではない。
内部実装はこんな感じらしい。

option match {
   case Some(x) => f(x)
   case None    => ()
}

これを使うことで、Noneの時の処理を省略できる。

nanasinanasi

ちなみに、↑でdogDog型だということがわかるのは、型パラメーターを使っているから。
これはコンストラクタでanimal: Animalと指定していたらできない。

// 型は書かなくても動くけど、ミスってCatなんかを混ぜないように書いておいたほうがいい
val list = new AnimalList[Dog](List(
  Dog("ぽち", 3),
  Dog("こむぎ", 2),
))
nanasinanasi

以下の実装だと、AnimalListDogしか格納されてないということが表現できない。
そのため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!
}
nanasinanasi

内部クラスでは、クラスの中にクラスを書くことができる。

class Graph {
  class Node
}

val graph1 = new Graph
val node1 = new graph1.Node

このとき、node1graph1.Node型となる。
Graph.Node型ではないので注意。

nanasinanasi

例えば、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にすると型エラーが出た。
なんで?

nanasinanasi

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
    }
  }
nanasinanasi

他のインスタンスで作られたものを許容したければ、親クラス名#内部クラス名が使える。

protected class Node {
    var connectedNodes: List[Graph#Node] = Nil
    def connect(node: Graph#Node): Unit = {
      connectedNodes = node :: connectedNodes
    }
  }
nanasinanasi

抽象型メンバーを使うと、メンバーの型を継承(ミックスイン)先で自由に決めることができる。
使い方は型パラメーターと被りそう。

nanasinanasi

「なんでもいい」を表現するときに型パラメータでAnyを使う必要がなくなる?
それとも、継承が禁止されてるクラスなら自由度が減る?

nanasinanasi

withを使うと複合型を作れる。
複合型は型と型を混ぜ合わせたもの。

trait Printable {
  def print(): Unit
}

trait Resettable {
  def reset(): Unit
}

def printAndReset(obj: Printable with Resettable): Printable = {
  obj.print()
  obj.reset()
  obj
}
nanasinanasi

似てる...

TypeScript
function printAndReset(obj: Printable & Resettable) {
nanasinanasi

thisを使うと自分型にアクセスできる?
名前の競合が起こったときに使えそう。

def check(username: String, password: String): Boolean = {
    username == this.username
    && PasswordUtil.getHash(password) == getPasswordHash
  }
nanasinanasi

自分型ってこういう使い方でいいのか...?
ここではユーザーの実装で実際のパスワードを隠蔽したかった

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 {
}

(そもそもパスワードをどこかに保存すること自体が間違いか)
(というか目的が不明すぎて迷走してる間は否めない)

nanasinanasi

↑使用例

val user = new User("admin", "pass1")
user.getPasswordHash // hash: pass1
user.check("一般ユーザー", "my_pass") // false
nanasinanasi

implicitを使うと暗黙の値を表現できる。
これはメソッドのパラメーターの前につける。

暗黙のパラメータは、Scalaが使える値を探してくれる。

nanasinanasi

暗黙の変換もあるらしい。

暗黙の変換は見境なく使われると落とし穴になり得るため、暗黙の変換の定義をコンパイルしている時にコンパイラは警告を出します。

nanasinanasi

メソッドの型パラメーターと再帰

def listOfDuplicates[A](value: A, count: Int): List[A] = {
  if (count < 1)
    Nil
  else
    value :: listOfDuplicates(value, count - 1)
}
nanasinanasi

使用例

println(listOfDuplicates[Int](3, 4))  // List(3, 3, 3, 3)
println(listOfDuplicates("La", 8))  // List(La, La, La, La, La, La, La, La)
nanasinanasi

再帰メソッドは型推論ができないので、自分で型を定義する必要がある。

nanasinanasi

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]

(配列の長さを型に落とし込むのは諦めた)

nanasinanasi

演算子を自分で定義する

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!
nanasinanasi

@から始まるアノテーションを使うことができる。
例えば@deprecatedはメソッドやクラスなどが非推奨であることを示す。

nanasinanasi

Scalaにはパッケージ機能がある。

パッケージはscala/main/パッケージ名ディレクトリに作るのが一般的。
また、ファイルの先頭にpackage パッケージ名を書く。

nanasinanasi

例: userパッケージでUserケースクラスを宣言し、sampleパッケージでインポートする

src/main/scala/user/User.scala
package user

case class User(username: String, password: String)
src/main/scala/sample/sample.scala
package sample

@main def hello(): Unit = {
  import user.User
  
  println("Hello World!")
  val user1 = User("abc", "p@ssw0rd")
}
nanasinanasi

↑のように、importをメソッド内などの好きな場所で使うことができる。

nanasinanasi

パッケージ名と値の名前を被せると、再帰的インポートとやらでエラーになるので注意。

nanasinanasi

トップレベルのメソッドや値もパッケージの一員となる。
なので、以下のcreateUseradminはインポートできる。

package user

def createUser(username: String, password: String): User = {
  val id = 1 // auto inclement
  User(id, username, password)
}

val admin = createUser("admin", "p@ssw0rd")
nanasinanasi

パッケージオブジェクトは廃止される?(参考
トップレベルで値などを直接書けるようになったからっぽい

でも、だとしたらなんでチュートリアルにあるんだ...?
更新が追いついてない?それとも実際には廃止予定はない?

このスクラップは1ヶ月前にクローズされました