戦略 Scala 日記

素人プログラマの思考のセンス

高階関数の引数あれこれ

カリー化した関数定義で、引数に渡した高階関数に 別の引数リストの結果を渡すとき、下記のように定義すると java.lang.AssertionErrorとなる。

def func1(x: Int)(g: Int => Int): Int = g(x)

ここで注意したいのは、1つ目の引数リストの結果を一度 ローカル変数に持ち、これを公開関数に渡すことで、定義ができる。

def func2(x: Int)(g: Int => Int): Int = {
  val _x = x
  g(x)
}

同じように、1つ目の引数にMapを渡し、2つ目の引数リストにマップを受取る関数を渡す、 カリーかされた関数を下記のように定義する。

def func3(map: Map[String, List[Int]])(f: Map[String, List[Int]] => Int) = {
  val _map = map
  f(_map)
}

では1から10までの奇数と偶数の整数リストをもつMapを定義し、これを渡して最小の奇数(つまり1)を返す関数を定義し、 定義したfunc3を使って、結果を得る。

val argMap = Map("odd" -> List(1, 3, 5, 7, 9), "even" -> List(2, 4, 6, 8, 10))

func3(argMap)(m => m("odd").head)

このようにすれば、先頭の1が返ってくる。 さて、このとき高階関数の側はMapを受取ることが自明であるから、プレースホルダ(_)を利用することができる。 また、ライブラリなどを利用する場合、受取るList部分がList以外のコレクション型であった場合もListとして扱えるように、asInstanceOf[]を利用すると安全である。

func3(argMap)(_("odd").asInstanceOf[List[Int]].head)

Optionにはifを使うなfilterを使え

ウェブアプリケーションを作っていると、下記のコードのvalue変数のように、値を受取るOption型の変数の中に値が入っている場合、かつ空文字列ではない場合のみ処理をしたいというケースはよくある。

val value: Option[String] = Some("1234")
if (value.isDefined && value.get.nonEmpty) {
  println("value: " + value.get)
}

上のコードを実行すると、valueの値がNoneの場合と、Some("")の場合は、ifブロック内の処理は行われない。 ただし、このような場合Scalaの得意とするパターンマッチを使ったほうがよい。 これをmatch caseを使って書き換えてみる。

val value: Option[String] = Some("1234")
value match {
  case Some(x) if x.nonEmpty => println("value: " + x)
  case _ => 
}

しかし、今回のように、valueが条件に一致しない場合はなにも処理をしないような場合は、case matchを使っても冗長になる。 こういったケースでは、ifを使って分岐をするより、filterに処理する条件を渡したほうがコードの見通しが良くなる。

val value: Option[String] = Some("1234")
value.filter(_.nonEmpty).foreach(println)

filtermapforeachのようにコレクションAPIを利用すると、Option型がNoneの場合は処理されない。

val value: Option[String] = None
value.foreach(println)

Hello Slick! チュートリアルで学ぶSlickの基本作法

Typesafe Activatorのチュートリアルとして用意されている、Hello Slick!のソースコードを元に、Slickの基本的な使い方を紹介する。

activator uiコマンドを実行し、ウェブブラウザに表示されたチュートリアルの一覧の中から、hello-slickを選択すると、ソースコードが展開される。これを元に解説を進める。

このチュートリアルで利用するファイル

  • CaseClassMapping.scala
  • HelloSclick
  • InvokerMethods
  • Tables.scala

メインスレッドはHelloSlickになるので、まずは一連の処理内容を確認する。

まず、DB接続用のドライバをインポートする。 JDBCの代わりになるようなものと思って良い。

import scala.slick.driver.H2Driver.simple._

Slickでは、テーブルのスキーマに対してクエリを実行するためのインタフェースとしてTableQueryというクラスを用意している。 実際のクエリを発行するには、このTableQueryを生成する。

例として、SuppliersCoffeesというテーブルへのアクセスを準備しよう。

val suppliers: TableQuery[Suppliers] = TableQuery[Suppliers]
val coffees: TableQuery[Coffees] = TableQuery[Coffees]

ここで型引数として渡しているSuppliersCoffeesTables.scalaのなかで、Tableとして定義している。

次に、DBに接続部分。 h2データベースのアドレスをjdbc:h2:mem:helloとして設定している。 ドライバとしてorg.h2.Driverを使うことを明示的に示している。

val db = Database.forURL("jdbc:h2:mem:hello", driver = "org.h2.Driver")

このデータベースに対して、実際にクエリを投げる部分がここから先の部分である。 withSessionのブロックに実際のクエリ生成と、実行の処理を記述する。 また、implicit sessionとしているのは、listheadメソッドが、sessionを暗黙の引数として受け取るため、ここでimplicitキーワードを付けている。

db.withSession { implicit session =>
  // 最後まで続く
}

定義済のテーブル(データ構造)をDB上に生成する。 ここで見慣れない演算子として、++というddlが作るクラスのメソッドを利用している。

(suppliers.ddl ++ coffees.ddl).create

下記が、行を追加するSlick特有の構文になる。

suppliers += (100, "String1", "String String1")
suppliers += (200, "String2", "String String2")

このように、TableQueryのインスタンスに対して、タプルを渡している。 これで新たに行が追加される。

また、JDBCのバッチ挿入機能を利用する場合は下記のように、タプルをSeqにまとめて これを連結することで、クエリが実行される。

val coffeesInsertResult: Option[Int] = coffees ++= Seq (
  ("Colombian",      101, 7.99, 0, 0),
  ("French_Roast",    49, 8.99, 0, 0),
  // (...)
)

追加された行数を取得する場合は、次のようにします。 結果はOptionで返ってくるため、foreachを利用している。

coffeesInsertResult foreach { numRows =>
  println(s"Inserted $numRows rows into the Coffees table")
}

クエリの作成と実行

ここからは実際にクエリを発行する処理を見ていくが、クエリの作成の流れは次のようなイメージである。 collection_apiの部分にはScalaのコレクションフレームワークに用意されているfiltermapforeachなどのメソッドを利用するイメージである。

// Selectを発行し結果を得る
val result = table.collection_api.list

// クエリを文字列として取得する
val query = table.collection_api.selectStatement

// クエリをクラスとして取得する
val query = table.collection_api
// 例えば
val query = table.filter(_ => _)

SELECT

次に、SELECTを発行するやり方を確認する。 例えば、テーブルから全件を取得するような処理はどうしたらよいか。

次のように、カラムの値のタプルをリストで受け取る変数を用意し、TableQuery[Suppliers]として定義しているテーブルsuppliersに対して、listメソッドを実行する。

val allSuppliers: List[(Int, String, String, String, String, String)] =
  suppliers.list

例えば、Coffeesテーブルに対して全件を取得するクエリを確認する方法は下記のようになる。

println("Generated SQL for base Coffees query:¥n" + coffees.selectStatement)

db.withSessionの中では、TableQuery[A]で生成しているテーブル(ここではTableQuery[Coffees])は、Seq[Tuple]のようなものなので、foreachをかけて、タプルをパターンマッチすることで、その内容を取得する事もできる。 mapfileterなどのコレクションAPIを適用するとクエリを取得するがforeachはIO処理というイメージなので、直感的である。

イメージとしては、レコードはひとつのタプルでまとまっていると考えて良い。 INSERTの時もタプルを利用したことを思い出すと、合点がいく。

coffees foreach { case (name, supID, price, sales, total) =>
  // println(name + " " + supID + " " + ...)
}

あたかもDBのテーブルが、コレクションとして存在しているかのようにこれらを利用できるところが、Slickの利点である。

SELECT WHERE

次に条件を指定し、WHERE句を用いたSELECT文を発行する方法を確認する。

コレクションAPIを使う場合は、実行結果ではなくクエリのクラスが返ってくることに注意したい。 コレクションAPIの実行結果としてコレクションが返ってくるようなイメージである。

val filterQuery: Query[Coffees, (String, Int, Double, Int, Int), Seq] =
  coffees.filter(_.price > 9.0)

SQL文を確認する。

println("Generated SQL for filter query:¥n" + filterQuery.selectStatement)

実際のクエリは下記のようになる。

select x2."COF_NAME", x2."SUP_ID", x2."PRICE", x2."SALES", x2."TOTAL" from "COFFEES" x2 where x2."PRICE" > 9.0

実行結果リストが欲しい場合は最後にlistを実行する必要がある。

println(filterQuery.list)

// List((Espresso,150,9.99,0,0), (French_Roast_Decaf,49,9.99,0,0))

UPDATE

同じような要領で、Update文を作成し、実行する。 例えば、salesの項目をいっせいにアップデートするようなクエリを作成するには次のようにする。

val updateQuery: Query[column[Int], Int, Seq] = coffees.map(_.sales)

println("Generated SQL for Coffees update:¥n" + updateQuery.updateStatement)
// update "COFFEES" set "SALES" = ?

val numUpdateRows = updateQuery.update(1)

println(s"Updated $numUpdatedRows rows")
// Updated 5 rows

DELETE

レコードの削除をかけるDELETE文を発行する処理。

val deleteQuery: Query[Coffees,(String,Int,Double,Int,Int),Seq] =
  coffees.filter(_.price < 8.0)

println("Generated SQL for Coffees delete:¥n" + deleteQuery.deleteStatement)
// delete from "COFFEES" where "COFFEES"."PRICE" < 8.0

val numDeletedRows = deleteQuery.delete

println(s"Deleted $numDeletedRows rows")
// Deleted 1 rows

特定のカラムを取得するSELECT

結果として得られるリストは、例えばカラムがStringの場合はList[Sting]となる。

val justNameQuery: Query[Column[String], String, Seq] = coffees.map(_.name)

println("Generated SQL for query returning just the name:¥n" + justNameQuery.selectStatement)
// select x2."COF_NAME" from "COFFEES" x2

println(justNameQuery.list)
// List(French_Roast, Espresso, Colombian_Decaf, French_Roast_Decaf)

ORDER BY

ORDER BYによるソート結果を返す処理

val sortByPriceQuery: Query[Coffees,(String,Int,Double,Int,Int),Seq] =
  coffees.sortBy(_.price)

println("Generated SQL for query sorted by price¥n" + sortByPriceQuery.selectStatement)
// select x2."COF_NAME", x2."SUP_ID", x2."PRICE", x2."SALES", x2."TOTAL" from "COFFEES" x2 order by x2."PRICE"

println(sortByPriceQuery.list)
// List((French_Roast,49,8.99,1,0), (Colombian_Decaf,101,8.99,1,0), (Espresso,150,9.99,1,0), (French_Roast_Decaf,49,9.99,1,0))
G

SELECT文の結合(UNION)

クエリを合成する。つまり、複数の条件をメソッドチェーンでつなぐ。

val composedQuery: Query[Column[String], String, Seq] =
  coffees.sortBy(_.name).take(3).filter(_.price > 9.0).map(_.name)

println(composedQuery.selectStatement)
// select x2."COF_NAME" from (select x3."TOTAL" as "TOTAL", x3."COF_NAME" as "COF_NAME", x3."PRICE" as "PRICE", x3."SUP_ID" as "SUP_ID", x3."SALES" as "SALES" from "COFFEES" x3 order by x3."COF_NAME" limit 3) x2 where x2."PRICE" > 9.0

println(composedQuery.list)
// List(Espresso)

この例の場合では、filter(_.price > 9.0).map(_.map)以外の実行結果に対して、再度SELECT文を投げるクエリが作られる。

JOIN

JOIN句の生成には、for内包表記を利用する。 (実際に実行されるクエリはJOIN句を利用しないクエリになっている)

ここで、c.supplierとしているが、これはTable.scalaで定義しているCoffeesテーブルに事前に外部キーとして記述している。

class Coffees(tag: Tag) extends Table[(String, Int, ...)](tag, "COFEES") {
  
  // カラム定義
  def supID: Column[Int] = column[Int]("SUP_ID")
  // ...
  
  def supplier: ForeignKeyQuery[Suppliers, (Int, String, ...)] =
    foreignKey("SUP_FK", supID, TableQuery[Suppliers])(_.id)
}

この外部キーの設定を事前に定義した後に、JOIN句の生成を下記のように行う。

val joinQuery: Query[(Column[String],Column[String]), (String, String), Seq] = for {
  c <- coffees if c.price > 9.0
  s <- c.supplier
} yield (c.name, s.name)

println("Generated SQL for the join query:¥n + joinQuery.selectStatement")
// select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2, "SUPPLIERS" x3 where (x2."PRICE" > 9.0) and (x3."SUP_ID" = x2."SUP_ID")

println(joinQuery.list)
// List((Espresso,The High Ground), (French_Roast_Decaf,Superior Coffee))

計算を含むクエリを作成する

計算を含むクエリを作成する場合は、listではなく、runメソッドを実行して結果を得る。 結果はOptionで返ってくるのが基本となる。

val maxPriceColumn: Column[Option[Double]] = coffees.map(_.price).max

println("Generated SQL for max price column:¥n" + maxPriceColumn.selectStatement)
// select x2.x3 from (select max(x4.x5) as x3 from (select x6."PRICE" as x5 from "COFFEES" x6) x4) x2

println(maxPriceColumn.run)
Some(9.99)

SQL文を利用する

最後に、SQL文を直接記述するケースを扱う。 JDBCのStaticQueryをライブラリとして読み込む必要がある。

import scala.slick.jdbc.StaticQuery.interpolation

このように、クエリの中に変数をおき、動的にクエリを作ることもできる。

val stete = "CA"
val plainQuery = sql"select SUP_NAME from SUPPLIERS where STATE = $state".as[String]

println(plainQuery.getStatement)
// select SUP_NAME from SUPPLIERS where STATE = ?

println(plainQuery.list)
// List(Acme, Inc., Superior Coffee, The High Ground)

さてここまで、クエリの作成と実行方法を先に確認してきた。 その実行対象となるテーブルはどのように定義されているのかを次に確認したい。

Table定義

Tables.scalaを参照。 インポートが必要なライブラリは下記。

import scala.slick.driver.H2Driver.simple._
import scala.slick.lifted.{ProvenShape, ForeignKeyQuery}

基本構造としては、Tableクラスに渡す型が、カラムの型をタプルにしたものとなる。 また、Tableに対して、tagを渡すこともどうじに行う。 実装は、各カラム名をつけたメソッドを用意し、columnのインスタンスを返すようにする。

*は、すべてのカラムを関数として持つタプルを返すのだが、 この時返る型は、ProvenShapeとなるようだ。 columnはファクトリメソッドである。

はじめに、SUPPLIERSテーブルを生成するコードは次のようになる。 カラムは"id, name, street, city, state, zip"の6つである。

class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") {
  def id: Column[Int] = column[Int]("SUP_ID")
  def name: Column[String] = column[String]("SUP_NAME")
  def street: Column[String] = column[String]("STREET")
  def city: Column[String] = column[String]("CITY")
  def state: Column[String] = column[String]("STATE")
  def zip: Column[String] = column[String]("ZIP")

  def * : ProvenShape[(Int, String, String, String, String, String)] =
    (id, name, street, city, state, zip)
}

先ほどJOIN句の生成で利用した、外部キーの設定もここで行う。 外部キーの生成は次のような記法を用いる。

class Coffees(tag: Tag)
  extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
    def name: Column[String] = column[String]("COF_NAME", O.PrimaryKey)
    def supID: Column[Int] = column[Int]("SUP_ID")
    def price: Column[Double] = column[Double]("PRICE")
    def sales: Column[Int] = column[Int]("SALES")
    def total: Column[Int] = column[Int]("TOTAL")

    def * : ProvenShape[(String, Int, Double, Int, Int)] =
      (name, supID, price, sales, total)

    def supplier: ForeignKeyQuery[Suppliers, (Int, String, String, String, String, String)] =
      foreignKey("SUP_FK", supID, TableQuery[Suppliers])(_.id)
}

形式知にして使う、暗黙のimplicit

「暗黙のXX」と訳される、Scalaの"implicit"キーワード。 しかし、その動作をちゃんと理解しないままでも、

Scalaのimplitiキーワード(暗黙のXX)が使われるのは次の2つの場合。

  • 暗黙の型変換
  • 暗黙の引数

これだけなのだが、初学者にとってややこしく感じるのは、これらが実装において意識をしなくとも暗黙のうちに動いてしまうからではないだろうか。

暗黙の型変換

暗黙の型変換の場合は、型が要求される状況で異なる型を渡したとしてもこれを自動的に変換するというもの。 例えば次の用に、String型を要求する変数に、Int型の9999を代入することはできない。

scala> val str: String = 9999
<console>:7: error: type mismatch;
 found   : Int(9999)
 required: String
       val str: String = 9999
                         ^

ここで、Int型の値をString型に変換する関数にimplicitキーワードをつけて予め定義しておく。こうすることで暗黙の型変換が行われ、9999がStringに自動的に変換され先のstrに代入可能となる。

scala> implicit def intToString(value: Int): String = value.toString
warning: there was one feature warning; re-run with -feature for details
intToString: (value: Int)String

scala> val str: String = 9999
str: String = 9999

暗黙の引数

こちらは関数(もしくはメソッド)の引数を予め定義しておくことで、メソッドを呼び出すときにその引数リストを省略することができるというもの。 引数にデフォルト値を設定する場合と、同じように予め値を定義する役割なのだが、関数(メソッド)の呼び出し時にその値が決定するという点で異なる。引数のデフォルト値は関数の定義時にその値が決定される。

状況によって値が変わるそんな引数を束縛するメソッドとはどんな場合があるか

class Tax {
  def tax(implicit rate: Int) = rate * 0.01
} 

class VAT {
  implicit val vat = 5
  
  def calc = {
    val tax = new Tax
    tax.tax     // 暗黙の引数を取り込み、0.05が返される
    tax.tax(10) // 明示的に引数に10を渡しているので、0.1が返される
  }
}

暗黙の引数を引数に用いる場合

例えば下記は、ScalaのORMであるSlickのデータベースへの接続を持った状態での一連の処理内容を記述するような場合に使う記述である。

val result = db.withSession {  implicit session =>
  query.list // <- takes session implicitly
}

ここで、listは暗黙の引数としてsessionを受け取るので、withSessionが受け取る関数引数は、implicit sessionを受け取るようにしている。