ScalaTest がよくわからなかったので調べた

Scalaの勉強をしていて下記のようなコードが出てきた

https://dwango.github.io/scala_text/test.html#fn_3

import org.scalatest._

class CalcSpec extends FlatSpec with DiagrammedAssertions {

  val calc = new Calc

  "sum関数" should "整数の配列を取得し、それらを足し合わせた整数を返すことができる" in {
    assert(calc.sum(Seq(1, 2, 3)) === 6)
    assert(calc.sum(Seq(0)) === 0)
    assert(calc.sum(Seq(-1, 1)) === 0)
    assert(calc.sum(Seq()) === 0)
  }
}

文字列 should 文字列 in { } ってなんなのか?

とりあえずIDEでコードを書いて should の定義へジャンプしてみると...

https://github.com/scalatest/scalatest/blob/3.0.x/scalatest/src/main/scala/org/scalatest/words/ShouldVerb.scala#L144

    def should(right: String)(implicit svsi: StringVerbStringInvocation): ResultOfStringPassedToVerb = {
      svsi(leftSideString, "should", right, pos)
    }

※ FlatSpec が FlatSpecLike を継承していて、
FlatSpec が ShouldVerb (とかいろいろ) を継承しているのでShouldVerbにアクセス出来ている

カリー化

まず、def should(right)(svsi): ResultOfStringPassedToVerb = {} というのはカリー化

https://sites.google.com/site/scalajp/home/documentation/scala-by-example/chapter5/section2

scala> def func(a: Int)(b: Int): Int = (a + 3) * b

scala> func(3)(4)
res1: Int = 24

scala> val f3 = func(3)(_)
f3: Int => Int = <function1>

scala> f3(5)
res3: Int = 30

上記のように func(a: Int)(b: Int) みたいな感じで定義した関数は func(3)(4) のような形で呼び出すことができる

また、 val f3 = func(3)(_) とすると、1つめの引数を3としたうえで、2つめの引数だけを受け取る関数にすることができる

したがってf3(5) は func(3)(5) と同じ結果になる

shouldはこのように should(foo)(bar) のような形で呼び出すことができる関数らしい

ドットとカッコの省略

scalaではメソッド呼び出しの時の'.'を省略できるらしい (hoge.func() は hoge func()とかける)

引数が1つだけのメソッドを呼び出すときはカッコを省略できるらしいが、このときはかならずドットも省略しておかなければならないらしい (hoge func 3 はOKだけど hoge.func 3 はだめ)

引数が1つだけのメソッドを呼び出すときは() の代わりに {} をつかってもいいらしい

http://qiita.com/tag1216/items/d0c5970f840c9ff30925

scala> class Foo {
     |   def func1(a: Int): Int = a * 3
     |   def func2(a: Int)(b: Int): Int = (a + 3) * b
     | }
defined class Foo

scala> val foo = new Foo()
foo: Foo = $iwC$$iwC$Foo@4bdd6fe0

scala> foo.func1(2)
res9: Int = 6

scala> foo func1(2)
res10: Int = 6

scala> foo func1 2
res11: Int = 6

scala> foo.func1 2
<console>:1: error: ';' expected but integer literal found.
       foo.func1 2
                 ^
scala> foo.func1 { 4 }
res0: Int = 12

scala> foo func1 { 4 }
res1: Int = 12




scala> foo.func2(3)(4)
res12: Int = 24

# func2ではこの手の省略はだめらしい

scala> foo func2(3)(4)
<console>:23: error: Int(3) does not take parameters
              foo func2(3)(4)
                          ^
scala> foo func2(3) 4
<console>:1: error: ';' expected but integer literal found.
       foo func2(3) 4
                    ^

scala> foo func2 3 4
<console>:1: error: ';' expected but integer literal found.
       foo func2 3 3
暗黙の型変換

後ろのほうはあとで考えるとして、 文字列 should ... というのは "abc".should のドットを省略しているのだろう

でもStringにshouldなんてメソッドはない

scalaでは暗黙の型変換というものが利用できるらしい

https://dwango.github.io/scala_text/implicit.html

scala> class MyString(s: String) {
     |   def twice(): String = s + s
     | }
defined class MyString

scala> implicit def convertToMyString(s: String): MyString = new MyString(s)
warning: there were 1 feature warning(s); re-run with -feature for details
convertToMyString: (s: String)MyString

scala> "def".twice()
res3: String = defdef

上記のようにimplicitをつけて関数を定義しておくと、引数の型のオブジェクトを必要に応じて変換してくれる

ここでは"def"という文字列をMyString型にこっそり変換してくれるのでString型でtwiceというメソッドがあるように見えるようになる

これと同様の変換がshouldの前の文字列に対しても行われているのでは?とおもって探したら下記が見つかった

https://github.com/scalatest/scalatest/blob/3.0.x/scalatest/src/main/scala/org/scalatest/words/ShouldVerb.scala#L231

  implicit def convertToStringShouldWrapperForVerb(o: String)(implicit position: source.Position): StringShouldWrapperForVerb =
    new StringShouldWrapperForVerb {
      val leftSideString = o.trim
      val pos = position
    }
  }

Stringを受け取って、shouldが定義してあった StringShouldWrapperForVerb へ変換するimplicitな関数なので多分間違いないが、(implicit position: source.Position) が気になる

implicitな引数

http://qiita.com/tagia0212/items/f70cf68e89e4367fcf2e#2-3-implicit%E5%BC%95%E6%95%B0%E3%81%A8implicit%E5%A4%89%E6%95%B0

引数にimplicitを付けておくと、その引数を渡さなくても予めimplicitをつけておいた変数をその引数としてつかってくれるらしい

scala> implicit val num: Int = 123
num: Int = 123

scala> def impargFunc(implicit a: Int): Int = a * 2
impargFunc: (implicit a: Int)Int

scala> impargFunc(3)
res4: Int = 6

scala> impargFunc()
<console>:27: error: not enough arguments for method impargFunc: (implicit a: Int)Int.
Unspecified value parameter a.
              impargFunc()
                        ^

scala> impargFunc
res6: Int = 246

implicitな引数を利用するときはカッコごと省略しなければならないらしい

先ほどの implicit def convertToStringShouldWrapperForVerb(o: String)(implicit position: source.Position): StringShouldWrapperForVerb も source.Position な変数がどこかでimplicitをつけて定義されているはずだ

implicitな引数に具体的な値を渡さなかった場合,コンパイラはimplicitが付いている値・関数から型の合うものを探し,渡してくれる。(探す範囲はその呼び出しのスコープ内からです。)

なので変数でなくてもいいらしい

ShouldVerb.scala からimportされている箇所をさがすとどうやら

https://github.com/scalatest/scalatest/blob/3.0.x/scalactic/src/main/scala/org/scalactic/source/Position.scala#L57

がimplicitなPositionとして利用されるらしい

implicit def here: Position = macro PositionMacro.genPosition

内容まではよくわからないが一旦、ここで定義されているPositionが利用されるらしい、ということにしてこれについては切り上げよう

ここまでのまとめ

shouldの正体を追いかけて

def should(right: String)(implicit svsi: StringVerbStringInvocation): ResultOfStringPassedToVerb

はStringを暗黙の型変換でStringShouldWrapperForVerbに変換し、そのメソッドとして呼び出されているようだ、ということがわかった

うえのほうでほったらかしていたが、svsiもimplicitな引数なのでどこかでimplicitな値 or 関数が定義されていてそれが利用されるのだろう

ということで、

文字列 should 文字列 in { }

の2つめの文字列までについては
文字列.should(文字列) であり、この型は ResultOfStringPassedToVerbであることがわかった

ということで、次の in は多分 ResultOfStringPassedToVerbのメソッドなのだろう

これについて見ていく

in

inの定義にジャンプしてみたらResultOfStringPassedToVerbではなく、InAndIgnoreMethods というクラスで定義されていた

https://github.com/scalatest/scalatest/blob/3.0.x/scalatest/src/main/scala/org/scalatest/FlatSpecLike.scala#L1397

一瞬面食らったが、よくみたら暗黙の型変換が行われていた

protected implicit def convertToInAndIgnoreMethods(resultOfStringPassedToVerb: ResultOfStringPassedToVerb): InAndIgnoreMethods =
    new InAndIgnoreMethods(resultOfStringPassedToVerb)

ということで、改めてinの定義をみてみる

    def in(testFun: => Any /* Assertion */)(implicit pos: source.Position): Unit = {
      registerTestToRun(verb.trim + " " + rest.trim, "in", List(), testFun _, pos)
    }

posについてはshouldの時と全く同じ

testFun の型は => Any となっている

これはjavaでいうところのSupplier(つまり引数をとらず、戻り値を返す関数)なのかなと思ったがどうも違うらしい

http://www.ne.jp/asahi/hishidama/home/tech/scala/def.html#h_call_by_name

scala> def func(f: => Any) = f
func: (f: => Any)Any

scala> func({ 123 })
res7: Any = 123

scala> func(222)
res8: Any = 222

scala> func({ val a = 123; val b = "abc"; 333 })
res10: Any = 333

"名前渡しにすると、関数(関数リテラル)も渡せるし、普通の値も渡せるようになる。
名前渡しした関数は、実際に使われるまで評価(実行)されない。"

ということらしい

まとめ

長々と書いたが

文字列 should 文字列 in { }

文字列.should(文字列).in({})

だった

慣れればこういうのを見てもどれがメソッド呼び出しでどれが引数なのか、とかすっとわかるようになるのかしら