高階関数の引数あれこれ
カリー化した関数定義で、引数に渡した高階関数に
別の引数リストの結果を渡すとき、下記のように定義すると
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)
filter
やmap
、foreach
のようにコレクションAPIを利用すると、Option
型がNone
の場合は処理されない。
val value: Option[String] = None value.foreach(println)
Hello Slick! チュートリアルで学ぶSlickの基本作法
Typesafe Activatorのチュートリアルとして用意されている、Hello Slick!のソースコードを元に、Slickの基本的な使い方を紹介する。
- Hello Slick! - Typesafe Activator | Typesafe http://typesafe.com/activator/template/hello-slick
activator ui
コマンドを実行し、ウェブブラウザに表示されたチュートリアルの一覧の中から、hello-slick
を選択すると、ソースコードが展開される。これを元に解説を進める。
このチュートリアルで利用するファイル
メインスレッドはHelloSlick
になるので、まずは一連の処理内容を確認する。
まず、DB接続用のドライバをインポートする。 JDBCの代わりになるようなものと思って良い。
import scala.slick.driver.H2Driver.simple._
Slickでは、テーブルのスキーマに対してクエリを実行するためのインタフェースとしてTableQuery
というクラスを用意している。
実際のクエリを発行するには、このTableQuery
を生成する。
例として、Suppliers
とCoffees
というテーブルへのアクセスを準備しよう。
val suppliers: TableQuery[Suppliers] = TableQuery[Suppliers] val coffees: TableQuery[Coffees] = TableQuery[Coffees]
ここで型引数として渡しているSuppliers
とCoffees
はTables.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
としているのは、list
やhead
メソッドが、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のコレクションフレームワークに用意されているfilter
、map
、foreach
などのメソッドを利用するイメージである。
// 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をかけて、タプルをパターンマッチすることで、その内容を取得する事もできる。
map
やfileter
などのコレクション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
を受け取るようにしている。