Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
6 contributors

Users who have contributed to this file

@hakobe @daiksy @yashigani @pokutuna @teufelj @fand
1501 lines (1069 sloc) 39.4 KB

Scalaによるプログラミングの基礎

事前課題

https://github.com/hatena/Hatena-Intern-Exercise2016/tree/master/scala

Scala 参考文献

書籍
  • Scalaスケーラブルプログラミング第3版 (ISBN:4844381490)
    • 言語作者が書いた本で、まずはこれ
  • Scala関数型デザイン&プログラミング(ISBN:4844337769)
    • 関数型プログラミングの知見をまなびたければ
  • Scala逆引きレシピ(ISBN:4798125415)
    • はじめに読むのはおすすめしなけど、困った時に手元にあると助かる
ウェブリソース

準備


開発環境

  • ライブラリはどうやっていれるの?
  • 自分で書いたコードのビルドはどうやるの?
  • コンソールで軽く動かしてみたい
  • Scalaや依存ライブラリーのバージョンの固定はどうやるの?

sbtのインストール

  • がんばってJDKをインストールしよう!
  • そしてsbtをインストールしよう!
$ brew install sbt

Hello, World!

build.sbt

name := "hello"

scalaVersion := "2.11.8"

Hello.scala

object Hello {
  def main(args: Array[String]) = println("Hello, World!")
}

実行

$ sbt
> run
[info] Compiling 1 Scala source to target/scala-2.11/classes...
[info] Running Hello
Hello, World!
[success] Total time: 1 s, completed Jun 28, 2016 4:06:08 PM
>
  • Scalaの処理系 (jarファイル) がインストールされる

jarファイル

  • クラスファイルやリソースファイル (画像とか) が入ってるzipアーカイブ
  • META情報も入っていて、どのクラスが実行できるとか書ける

これで君もScalaを書ける!

  • sbtでconsoleコマンドを実行するとScalaのreplに入れる
  • いろいろ試せて便利
  • 電卓にも使えるぞ! (ただしJVMが起動するのが遅い)
    • scalaコマンドもreplとして使えるが、プロジェクトのパッケージは使えない
$ sbt
> console
scala> 1 + 1
res0: Int = 2

scala>

準備完了


Scala 紹介 その1


変数宣言 - var と val と if式

val x = 1 // 再代入できない
var y = 2 // 再代入できる

x = 5 // valだとエラーになる
y = 6 // varだと再代入できる
  • 再代入できない変数を明示的に宣言できる
  • Perlでも、変数を使いまわすと理解しにくいコードができるのでやらない
    • ブログチームのコーディング規約でやめようって書いてある
  • valって書いとくと再代入されてないことが確実なので安心
  • 正当な理由がなければvarは使うべきではない

valはimmutableな変数定義

varはmutableな変数定義


if式

条件に応じて変数の中身を決めたい場合

var result: String = ""

if (i % 3 == 0 && i % 5 == 0) {
  result = "FizzBuzz"
} else if (i % 3 == 0) {
  result = "Fizz"
} else if (i % 5 == 0) {
  result = "Buzz"
} else {
  result = i.toString
}

このように事前に変数を定義しておき、条件によって変数に値を再代入するのがよくあるパターン。 このパターンでも、Scalaだったらvalを使いたい。

Scalaのifは文ではなく式であり、結果を返す。

val result = if (i % 3 == 0 && i % 5 == 0) {
  "FizzBuzz"
} else if (i % 3 == 0) {
  "Fizz"
} else if (i % 5 == 0) {
  "Buzz"
} else {
  i.toString
}

これでresultをvalで定義できる!

このようにScalaには変数の再代入を不要にするいろいろな仕組みがある。


Scala 紹介 その2


パターンマッチ!!

  • めっちゃ便利なswitch文みたいなの
  • 最近の言語だとだいたい入ってる

定数でマッチ

  • match式を使う
  • 式なので値を返す
val msg = x match {
  case 0 => "0だ!!"
  case 1 => "1だ!!"
  case _ => "それ以外だ!!"
}
println(msg)

or にしたりifでガードしたり

val msg = x match {
  case 0 | 1        => "0か1や"
  case n if n < 100 => "100 以下やね"
  case _ => "それ以外だ!!"
}
println(msg)

パターンマッチの力はこの程度ではない

あとででてくる


Scala 紹介 その3


コレクションが便利

  • コレクションはいろいろついてくる
    • List/Array/Map/Set
    • 細かいのを数えるといろいろある
  • コレクションには便利メソッドがめっちゃついてくる
    • map/filter/flatMap/find/findAll/reduce
    • take/drop/exists/sort/sortBy/zip/partition
    • grouped/groupBy
    • やまほどある

List

val list = List(1,2,3,4,5,6,7,8,9) // こういう感じでリスト作れる
list.map { n => // 関数リテラル
  n * n
}.filter { n =>
  n % 2 == 0
}.sum
list.grouped(2).foreach { ns => println(ns) }
list.groupBy { n => if (n % 2 == 0) "even" else "odd" }

関数リテラル

list.reduce { (x, y) => // 引数リスト
  10 * x + y  // 最後の式が返値
}
list.reduce { (x, y) => 10 * x + y }
List("apple", "banana", "grape").map(_.length)

list.reduce { 10 * _ + _ }

Map

  • Mapはキーに対して値を保持するコレクション
  • 便利メソッドはいろいろ使える
  • とりあえず紹介だけ
val urls = Map(
  "www"  -> "http://www.hatena.ne.jp",
  "b"    -> "http://b.hatena.ne.jp",
  "blog" -> "http://hatenablog.com"
)
urls.get("b") // → Some("http://b.hatena.ne.jp")
urls.get("v") // → None

Scala 紹介 その4


Option型とは

  • 値があるかないか表現できる型
  • undefチェックするの忘れてた! というのがなくなるすぐれもの
  • Option[+A]型
    • Some(x)という値
      • x は +A型の値 (Option[Int]型だったらInt型)
    • Noneという値
  • Someの中身を使うには明示的に取り出す操作が必要

Option型をつくる

  • 値があるときはSomeに値をくるむ
  • ないときはNone
Some("hello")
Some(1)
Some({ () => 5 * 3 })
None

Option型に包まれた値を取り出す

val urls = Map(
  "www"  -> "http://www.hatena.ne.jp",
  "b"    -> "http://b.hatena.ne.jp",
  "blog" -> "http://hatenablog.com"
)

val bUrl = urls.get("b") // Some("http://b.hatena.ne.jp")
val vUrl = urls.get("v") // None

// 方法1 (bad)
bUrl.get // SomeかNoneか無視してとりだす/基本的に使わない
vUrl.get // ランタイムエラー!!

// 方法2
bUrl.getOrElse("no url") // Someだったら中身の値
vUrl.getOrElse("no url") // Noneだったらデフォルト値

// 方法3
bUrl match { // パターンマッチでそれぞれ処理する
  case Some(url) =>
    s"bのURLは $url ですぞ"
  case None =>
    "no url"
}
  • getは使ったらアカン
  • 特別な操作なしでは値が使えない
    • 値をちゃんと取り出したどうかは型でチェックされる

パターンマッチ again

  • 値の構造でマッチング
  • Someの場合中身の値がurlにはいる!
  • 対象が unapply メソッドを実装していると、こういうパターンマッチができる
    • case classというのを使うと簡単に作れる
val bUrl = Some("http://b.hatena.ne.jp")
bUrl match {
  case Some(url) =>
    s"bのURLは $url ですぞ"
  case None =>
    "no url"
}

不完全なパターンマッチ

  • 必ずundefチェックできるとかいうけど、Noneのcase忘れるのでは
scala> bUrl match {
     |   case Some(url) =>
     |     s"url is $url"
     | }
<console>:10: warning: match may not be exhaustive.
It would fail on the following input: None
              bUrl match {
  • コンパイラが警告出すぞ!

Option型のいろんなメソッド

val bUrl = Some("http://b.hatena.ne.jp")

bUrl.filter { url => isHatenaUrl(url) } // trueならそのまま, falseならNoneになる
bUrl.exists { url => isHatenaUrl(url) } // Someなら条件式の結果, Noneならfalse
bUrl.map { url => getContent(url) }     // Someなら値を変換, Noneならそのまま
  • Listが持つ多くのメソッドが使える
    • 要素数が0か1しかないListだとみなせる

便利なflatMap

findEntryBy(entryId) // Option[Entry]
findUserBy(userId) // Option[User]
  • entryのauthorを取得したい
  • entryがSomeのときだけauthorを探して、authorが見つかったらSomeを返したい
  • どちらかが見つからなかったらNone
findEntryBy(entryId).flatMap { entry =>
  findUserBy(entry.authorId)
}
  • flatMapを使うとOptionを返すメソッドを次々と繋げられる
    • 全部がSomeだったときの処理が書ける
    • ただしネストしていくと読みづらい...
    • しかし読みやすくする技がある
findEntryBy(entryId).flatMap { entry =>
  findUserBy(entry.authorId).flatMap { user =>
    findUserOptionBy(user.id).flatMap { userOption =>
      findUserStatusBy(user.id).map { userStatus =>
        // 全部見つかった時の処理を書ける
        makeResult(entry, user, userOption, userStatus)
      }
    }
  }
}

Scala 紹介 その5


for式

for文ではなくfor式であり、値を返す

foreach, map, flatMap, filter, withFilterなどの糖衣構文


シンプルなfor

for (i <- (1 to 9)) {
  println(i)
}
  • foreachを用いた以下のコードと等価
(1 to 9).foreach { i =>
  println(i)
}

値を返すfor

val pows = for (i <- (1 to 9)) yield i * i
  • mapを用いた以下のコードと等価
val pows = (1 to 9).map { i => i * i }

ガードつきのfor

for (i <- (1 to 9) if i % 2 == 0) {
  println(i)
}
  • withFilterを用いた以下のコードと等価
(1 to 9).withFilter { i =>
  i % 2 == 0
}.foreach { i =>
  println(i)
}

withFilterが実装されていない時はfilterにfallbackする。


入れ子のfor

for {
  i <- (1 to 9)
  j <- (1 to 9)
} {
  print((i*j).toString + " ")
}
  • foreachを用いた以下のコードと等価
(1 to 9).foreach { i =>
  (1 to 9).foreach { j =>
    print((i*j).toString + " ")
  }
}

値を生成する入れ子のfor

val kuku = for {
  i <- (1 to 9)
  j <- (1 to 9)
} yield i * j
  • flatMapとmapを用いて以下のようにも書ける
val kuku = (1 to 9).flatMap { i =>
  (1 to 9).map { j =>
    i * j
  }
}
  • ならべるとflatMapをネストしてるのと一緒
val kukuku = for {
  i <- (1 to 9)
  j <- (1 to 9)
  k <- (1 to 9)
} yield i * j * k
val kukuku = (1 to 9).flatMap { i =>
  (1 to 9).flatMap { j =>
    (1 to 9).map { k =>
          i * j * k
    }
  }
}
  • flatMapでどんどん処理をつなげていく... どこかで聞いたことがある..

Option型をfor文で使う

  • 以下のようにOptionを返す関数をflatMapでどんどん繋げられるんだった
val result = findEntryBy(entryId).flatMap { entry =>
  findUserBy(entry.authorId).flatMap { user =>
    findUserOptionBy(user.id).flatMap { userOption =>
      findUserStatusBy(user.id).map { userStatus =>
        // 全部見つかった時の処理を書ける
        makeResult(entry, user, userOption, userStatus)
      }
    }
  }
}
  • つまりforを使うとこうかける!!
val result = for {
  entry <- findEntryBy(entryId)
  user <- findUserBy(entry.authorId)
  userOption <- findUserOptionBy(user.id)
  userStatus <- findUserStatusBy(user.id)
} yield makeResult(entry, user, userOption, userStatus)
  • 読みやすい!

  • 値が全部SomeならSome(makeResult(entry, user, userOption, userStatus))

  • いずれかの値がNoneならNone

  • for式を使うとある型の値のつなげて処理していくコードを綺麗に書ける

    • どうつなげられるかはflatMapの実装による
    • OptionならSomeのときは処理がつながるけどNoneならとまる

モナド

  • OptionやListはモナド
  • モナドが要求する関数
    • return
      • 値をOptionやListに包む関数 => Some(10), List(10)
    • bind
      • OptionやListを返す関数を組み合せる関数 => flatMap
    • これらがモナド則を満たす
  • for式はHaskellのdo式に相当する
  • OptionやList以外にも強力な抽象化メカニズムをモナドとして使えるぞ
  • 詳しくは、すごいHaskellたのしく学ぼう! を読もう!
  • Scalazというのを使うとより強力なモナドや記法が使えるようになる
    • Haskellを使ったらよいのではってなるけど、あったら便利

Scala 紹介 その6


class

class Cat(n: String) { // コンストラクタ
  val name = n // フィールド

  def say(msg: String) : String = {
    name + ": " + msg + "ですにゃ"
  }
}
println(new Cat("たま").say("こんにちは"))

class Tiger(n: String) extends Cat(n) { // 継承
  override def say(msg: String) : String = { // オーバーライド
    name + ": " + msg + "だがおー"
  }
}
println(new Tiger("とら").say("こんにちは"))

object

  • クラスの定義に対して1つしか存在しないオブジェクトを簡単に定義できる
  • classで定義したクラスと同名でobjectを定義するとコンパニオンオブジェクトになる
    • コンパニオンオブジェクト
      • お互いの非公開メンバにアクセスできる
      • implicitパラメータの解決時に使われることがある
object CatService {
  val serviceName = "猫製造機"
  def createByName(name :String): Cat = new Cat(name)
}
val mike = CatService.createByName("みけ")
mike.say("ねむい")

object Tama extends Cat("たま") {
  override def say(msg: String) : String = "たまにゃー"
}

object Cat { // すでにあるクラスと同じ名前だと
  // 定義されたメソッドはクラスメソッドのように振る舞う
  def create(name: String) : Cat = new Cat(name)
}
val hachi = Cat.create("はち")

case class

  • クラスに似てる
  • データ構造を定義しやすくカスタマイズされてる
  • いくつかのメソッドがいい感じに生える
    • toString/hashCode
    • apply/unapply (コンパニオンオブジェクトに)
case class Cat(name: String) { // nameは勝手にfieldになる
  def say(msg: String) :String = ???
}
val buchi = Cat("ぶち") // newなしで気楽に作れる
buchi match {
  case Cat(name) => // パターンマッチで使える
    "name of buchi is " = name
}

trait

  • 実装を追加できるインターフェース
  • Scala では設計のベースになるクラスの構造を構築するのによく使われる
  • Rubyのモジュールっぽいやつ
class Cat(n: String) {
  val name = n
}

trait Flyable {
  def fly: String = "I can fly"
}

// withで継承する/多重に継承できる
class FlyingCat(name: String) extends Cat(name) with Flyable
new FlyingCat("ちゃとら").fly

// Scalaで定義されているOrdered traitを実装すると比較できるように
class OrderedCat(name: String) extends Cat(name) with Ordered[Cat] {
   def compare(that: Cat): Int = this.name.compare(that.name)
}
new OrderedCat("たま") > new OrderedCat("みけ")
new OrderedCat("たま") < new OrderedCat("みけ")

sealed trait

traitをmixinしているcase classなどをパターンマッチで判定する場合、すべてのcase classに対してのマッチが考慮されているか、漏れを検出したいときがある。 そのようなときは、sealed を使えばよい

sealed 修飾子は、「同一ファイル内のクラスからは継承できるが、別ファイル内で定義されたクラスでは継承できない」という継承関係のスコープを制御するためのものだが、match式の漏れを検出する用途にも使える。

sealed trait HatenaService
case class HatenaBlog(name: String) extends HatenaService
case class HatenaBookmark(name: String) extends HatenaService
case class JinrikiKensakuHatena(name: String) extends HatenaService
case class Mackerel(name: String) extends HatenaService

val service: HatenaService = HatenaBlog("blog")

service match {
  case HatenaBlog(name) => name
  case HatenaBookmark(name) => name
  case JinrikiKensakuHatena(name) => name
}

<console>:16: warning: match may not be exhaustive.
It would fail on the following input: Mackerel(_)
              service match {
              ^

このように漏れているパターンを警告してくれる


traitの菱型継承について

菱型継承とは

Scalaのtraitのように多重継承が可能な場合に、下図のような継承関係になる場合のこと。

この場合、BとCでそれぞれAのメソッドをoverrideしていた場合、Dからはどのように見えるだろう?

trait A {
  val value = "A"
}

trait B extends A {
  override val value = "B"
}

trait C extends A {
  override val value = "C"
}

class D extends B with C

scala> (new D).value
res0: String = C

Cになる!

これをextendsする順番を入れ替えると...

class D extends C with B

scala> (new D).value
res0: String = B

Bになる!

Scalaでtraitを多重継承する場合、それぞれに親を同じとするoverrideメソッドが実装されていた場合、 withで連結されていく一番後ろの実装が優先される。


Scala 紹介 その7


implicit conversion/parameter

  • 暗黙の型変換
  • 暗黙のパラメータ
  • 暗黙と聞いていいイメージはないが使いドコロをまちがわないことでいろいろできる

普通の型変換

def stringToInt(s:String) : Int = {
  Integer.parseInt(s, 10)
}

"20" / 5 // 型エラーになる
stringToInt("20") / 5 // ok

暗黙の型変換

implicit def stringToInt(s:String) : Int = { // implicit!!
  Integer.parseInt(s, 10)
}

"20" / 5 // 計算できる!!
  • 要求する型が得られない時、スコープ中のimplicit宣言を調べて自動で変換する
  • / の右側ところには数値型しか現れないはずなのに文字列があるのでimplicitで定義した変換関数が呼ばれた
  • とはいえこれは異常なパターンでこんなことはしない...

使いどころ

  • 既存の型を拡張するように見せられる(pimp my libraryパターン)
class GreatString(val s: String)  {
  def bang: String = s + "!!!!"
}
implicit def str2greatStr(s: String): GreatString = {
  new GreatString(s)
}

"hello".bang // まるでStringに新しいメソッドが生えたように見える

implicit classを用いることもできる

implicit class GreatString(s: String)  {
  def bang: String = s + "!!!!"
}

"hello".bang

暗黙のパラメータ

  • 予め暗黙のパラメータを受け取る関数を定義
  • 呼び出し時にスコープ中のimplicit宣言を調べて自動的に引数として受け取る
def say(msg: String)(implicit suffix: String) =
  msg + suffix

say("hello")("!!!!!") // => hello!!!!! 普通に読んだらこう
implicit val mySuffix = "!?!?!!1" // 暗黙のパラメータを供給
say("hello") // => hello!?!?!!1

使いどころ1

  • コンテキストオブジェクトを引き回す
def findById(id: Int, dbContext: DBContext) = ???
def findByName(name: String, dbContext: DBContext) = ???

val dbContext = new DBContext()
findById(1, dbContext)
findByName("hakobe", dbContext) // 毎回DBコンテキストを渡す必要があってだるい
def findById(id: Int)(implicit dbContext: DBContext) = ???
def findByName(name: String)(implicit dbContext: DBContext) = ???

implicit val dbContext = new DBContext()
findById(1)
findByName("hakobe") // dbContextは暗黙的に供給されるのでスッキリ

使いどころ2

// http://nekogata.hatenablog.com/entry/2014/06/30/062342 より引用
trait FlipFlapper[T] {
  def doFlipFlap(x:T):T
}
implicit object IntFlipFlapper extends FlipFlapper[Int] { // ...(1)
  def doFlipFlap(x:Int) = - x
}
implicit object StringFlipFlapper extends FlipFlapper[String] { // ...(2)
  def doFlipFlap(x:String) = x.reverse
}

def flipFlap[T](x:T)(implicit flipFlapper: FlipFlapper[T])
  = flipFlapper.doFlipFlap(x) // ...(3)

flipFlap(1) // => -1
flipFlap("string") // => "gnirts"

Scala 紹介 その8


型パラメータ

ScalaのListなどは、そのListの要素がすべて同じ型であれば、StringとかIntとかいろいろな型を入れることができる。 このような定義を型パラメータという。

List[A] のように定義されていて、Aの部分が型パラメータ。

List[String]と書くとそのListはStringのみ扱えるし、List[Int] と書くとIntのみ扱える。

val l: List[Int] = List("a","b") // コンパイルエラー

自分で定義した関数の引数の型を任意の型としたい場合など、このように定義できる

def example[A](l: List[A]): A = l.head

scala> example(List(1,2))
res: Int = 1

scala> example(List("a","b"))
res: String = a

型パラメータは柔軟に指定することができ、例えばあるクラスの派生クラスのみ受け取りたい、などの指定ができる。 より詳しく知りたい場合は、「型境界」「変位指定アノテーション」などのキーワードで調べてみよう!


Scala 紹介 その9


複数の引数リスト

Scalaでは関数を定義するときに複数の引数リストを作ることができる

def example(x: Int)(y: Int) = x * y // こんな感じ

example(2)(4) // 使うときはこう書く

なにが嬉しいのか??

値を取る引数と関数を取る引数を分けておくと、使う人が楽

def example(i: Int)(f: Int => Int) = f(i)

example(2) { i =>
  i * 2
}

このようにexampleという新しいステートメントを定義したかのように書ける


可変長引数

Scalaで可変長引数を定義するときは

def example(ss: String*): Unit = ss.foreach(println)

のように書く。

こうすると、example は任意個のStringを受け取れるので、

example("AA")
example("AA", "BB")
example("AA", "BB", "CC")

のように文字列を任意個渡せるようになる。

ところで、可変長引数を受け取る関数内部では、引数はSeqで扱われる(なのでss.foreachとかできる)ので、そのままSeqを渡せそうだがそうはならない。

def example(ss: String*): Unit = ss.foreach(println)

val sq = Seq("AA", "BB")

scala> example(sq)
<console>:10: error: type mismatch;
 found   : Seq[String]
 required: String
              example(sq)
                      ^

可変長引数に対してコレクションを渡す場合はこう書く

def example(ss: String*): Unit = ss.foreach(println)

val sq = Seq("AA", "BB")

scala> example(sq:_*)
AA
BB

こうすると、コレクションの要素を可変長引数に1つずつ渡せるようになる


Scala 紹介 その10


文字列補間 (String interpolation)

s"..."のように文字列リテラルの前にプレフィックスをつけることで、リテラル中に$nameの形で変数名を指定してその値を埋め込むことができる。

val name = "foo"
val value = 3
s"$name is $value" // => foo is 3
s"7 * 8 = ${7 * 8}" // このように式も書ける

オブジェクト指向と関数型プログラミング言語の話

Scalaはオブジェクト指向言語であり、関数型プログラミング言語でもある。

オブジェクト指向

Scalaはオブジェクト指向言語。

オブジェクト指向言語の特徴

  • 「オブジェクト」があり、データを保持する場所 (フィールド) と、それらを操作したりデータを用いて行う処理 (メソッド) がある
  • 継承
  • カプセル化
  • ポリモーフィズム

継承

すでに定義済みのオブジェクトの特性を受け継ぐこと

class Parent {
  def helloWorld() = println("hello world")
}

class Child extends Parent {
  def helloChild() = println("hello child")
}

scala> new Child().helloWorld()
hello world

このように、Parentを継承しているChildは親のクラスの特性を受け継ぐので、親クラスのメソッドが使える。


カプセル化

オブジェクト内部のデータを隠蔽したり(データ隠蔽)、オブジェクトの振る舞いを隠蔽したり、オブジェクトの実際の型を隠蔽したりすること。 これにより、オブジェクト内部でのみ呼び出せるメソッドなどを定義することで、無関係なオブジェクトからそれらを扱えなくして、プログラムの影響範囲を局所化できる。

class Capsule {
  private def secretMethod() = println("秘密")

  def publicMethod() = secretMethod()
}

scala> new Capsule().secretMethod()
<console>:9: error: method secretMethod in class Capsule cannot be accessed in Capsule
              new Capsule().secretMethod
                            ^

scala> new Capsule().publicMethod()
秘密

このようにメソッドは定義されているが外部からアクセスできないが、クラス内では使える


ポリモーフィズム

あるオブジェクトへの操作が呼び出し側ではなく、受け手のオブジェクトによって定まる特性

trait HelloWorld {
  def helloWorld: String
}

class En extends HelloWorld {
  def helloWorld: String = "hello world"
}

class Ja extends HelloWorld {
  def helloWorld: String = "こんにちは世界"
}

def printHelloWorld(hw: HelloWorld) = println(hw.helloWorld)

scala> printHelloWorld(new En)
hello world

scala> printHelloWorld(new Ja)
こんにちは世界

printHelloWorld メソッドはtraitを受け取り、その振る舞いを呼び出しているが、その結果はprintHelloWorldに渡された実際のクラスの実装に委ねられる


関数型プログラミング言語

Scalaは関数型プログラミング言語でもある。

  • 関数が第一級オブジェクト
  • 関数型プログラミングスタイルを推奨
    • できるだけ副作用をもたない式や関数を組み合わせる
    • 破壊的な代入は避ける
    • ある変数が、状態を持ったり一部を変更していくようなことはしない

参照透過性が常に成り立つ言語を純粋関数型プログラミング言語、そうでない言語を非純粋関数型プログラミング言語という。Scalaは非純粋関数型プログラミング言語。

Scalaには関数型プログラミングスタイルを支援する様々な仕組みがある。


関数が第一級オブジェクトになっている

関数を引数に取ったり、関数の結果として返したり、変数に束縛したりできる。

val l = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Listのfilterメソッドは、"Listの要素を引数にとり、Booleanを返す関数" を引数にとる
scala> l.filter(i => i % 2 == 0)
res1: List[Int] = List(2, 4, 6, 8, 10)

// isEvenは、"Intを引数に取り、その値を2で割って余りがあるかどうかを返す関数" である
scala> val isEven = (i: Int) => i % 2 == 0
isEven: Int => Boolean = <function1>

// isEven (関数) をfilterの引数として渡すことができる
scala> l.filter(isEven)
res2: List[Int] = List(2, 4, 6, 8, 10)

副作用

副作用をもたないコードが望ましい

  • 変数の値を変更しない (varよりもval)
  • 周りの変数の状態に動作が依存しない
  • ファイルやデータベースとの入出力がない
scala> val fruits = List("apple", "banana", "orange")
fruits: List[String] = List(apple, banana, orange)

scala> val y = fruits.map(_.length)
y: List[Int] = List(5, 6, 6)

scala> val z = scala.collection.mutable.ListBuffer[Int]()
z: scala.collection.mutable.ListBuffer[Int] = ListBuffer()

scala> for (fruit <- fruits) { z += fruit.length }

scala> z
res1: scala.collection.mutable.ListBuffer[Int] = ListBuffer(5, 6, 6)

scala> for (fruit <- fruits) { z += fruit.length }

scala> z
res2: scala.collection.mutable.ListBuffer[Int] = ListBuffer(5, 6, 6, 5, 6, 6)

scala> val w = new Array[Int](3)
w: Array[Int] = Array(0, 0, 0)

scala> var i = 0; for (fruit <- x) { w(i) = fruit.length; i += 1 }; w
i: Int = 3
res3: Array[Int] = Array(5, 6, 6)
  • valなのになぜよくないのでしょうか?

参照透過なコードを書くことで、オブジェクト内部の状態をいちいち気にしなくてよくなる。


なぜ副作用がない方がうれしいのか

  • クラスなどの内部の状態を気にしなくてよくなるので、見通しのよいコードになる
  • 状態や環境に依存しないので、テストしやすい
  • 状態や環境を共有しないので、マルチスレッドにしたときに問題がおきない

Scalaにおける副作用

Scalaは関数型プログラミング言語とオブジェクト指向言語の特徴を兼ね備えたマルチパラダイムな言語なので、手続き的な実装を認めている。 varで変数の再代入が可能だし、mutableなコレクションもある。

ただ、せっかくだから関数プログラミングのメリットを活かしたいので、なるべく副作用の無い実装を心がけよう!


プロジェクトのコードを書くにあたって知っておいてほしいこと


テストを書こう

  • プログラムを変更する二つの方法 [レガシーコード改善ガイドより]
    • 編集して祈る
    • テストを書いて保護してから変更する

なぜテストを書くのか

  • テストがないと、どういう動作をして欲しいプログラムなのかがよく分からない
    • コーナーケースでどうなるのが意図した挙動なのか
  • 大規模プロジェクトでは致命的
    • 昔書いたコードは今もうごいているのか?
    • 一部を書き換えた時に挙動が変わっていないか?
    • 正しい仕様 / 意図が何だったのかわからなくなっていないか?
  • 静的言語はコンパイラに守られているとはいえ、コードの振る舞いはテストを書かないと保証できない

祈らずテストを書こう!


なにをテストするのか

  • 正常系

  • 異常系

  • 境界

  • 100% の カバーは難しい

    • 命令網羅(C0) / 分岐網羅(C1) / 条件網羅(C2)
    • C2 とかはたいへん
  • 必要 / 危険だと思われるところから書き、少しづつ充実する

  • バグ修正で不具合の再現手順が面倒な場合は、不具合が再現するテストを先に書いたりする


テストカバレッジ

C0(命令網羅)

  if (i >= 10) {
    println("true!");
  }

すべてのステートメントを通ればOK。 上の例だと、if文が真の場合しかテストされない (偽の場合のステートメントが無いので)

C1(分岐網羅)

分岐をすべて通るかを確認するテスト。

  if (i >= 10 || j == 0) {
    println("true!");
  } else {
    println("false!");
  }

上の例だと jの値を0に固定して、i == 1 の場合と i == 11の場合がテストされれば良い

C2(条件網羅)

すべての条件の組み合わせがテストされる。

  if (i >= 10 || j == 0) {
    println("true!");
  } else {
    println("false!");
  }

C1ではiの値でのみテストが行われたが、ここではjの値も含めて、それぞれのすべての条件の真偽値の組み合わせがテストされないといけない。


テストを書くコツ

  • まず、こういう振る舞いで有るべきというテストを書く
  sort(List(1,5,7,3,6)) shouldBe List(1,3,5,6,7)
  // Listにsortのメソッドはあるけど、例としてsort関数を自分で作ったと仮定してる
  • 次に境界条件での振る舞いを検証するテストを書く
  sort(Nil) shouldBe Nil // Listが空の場合はそのまま空
  sort(List(99)) shouldBe List(99) // Listの要素がひとつしかなければそのまま
  • 例外条件も確認
  an[IllegalArgumentException] should be thrownBy sort("hello")

リファクタリング

  • リファクタリングとは?
    • プログラムの振る舞いを変えずに実装を変更すること
  • テストがなければ、外部機能の変更がないことを証明できない。
    • テストがなければリファクタリングではない
  • レガシーなコードに対してはどうする?
    • まずは、テストを書ける状態にしよう。

テストを書いてリファクタリングし、常に綺麗で保守しやすいコードを書きましょう


プロジェクトのコードを書く心構え

  • コードが読まれるものであることを意識する
    • あとから誰が読んでもわかりやすく書く
    • 暗黙のルールを知る => コードを読みまくる
    • 変数や関数の名前には充分こだわる (実装者の意図を名前で伝える)
    • コードだけで意図を表現しづらければコメントも併用しよう
  • テストを書いて意図を伝える (テストは自分が書いた関数のリファレンス実装になっていると最高)

データモデリング

構築するソフトウェアにはどのような概念が登場するのか考えて分析してみよう。

以下ではIntern-Bookmarkを例に考えてみる。


登場する概念 (モデル)

  • User ブックマークをするユーザ
  • Entry ブックマークされた記事 (URL)
  • Bookmark ユーザが行ったブックマーク

概念が持つ特性

各クラスがどのような特性を持っているか考えてみよう。

  • User
    • ユーザの名前
  • Entry
    • ブックマークされたURL
    • Webサイトのタイトル
  • Bookmark
    • ブックマークしたUser
    • ブックマークしたEntry
    • コメント

概念間の関係

  • 1つのEntryには複数のBookmarkが属する (一対多)
  • 1つのUserには複数のBookmarkが属する (一対多)

課題1-1

データモデリング

Intern-Bookmarkのモデリングの講義を参考に簡単な"ブログシステム"を考えて、登場する概念 (モデル) とその関係を考えてみましょう。 世の中のブログサービスには様々な機能がありますが、ここでは基本的な機能に絞って考えてもらって構いません。

  • ブログを書く人 (=ユーザ) は存在しそうですね
  • 普通のブログサービスであれば、ユーザごとに個別のブログがありますね
  • ブログには記事がありますね

モデリングに対応するオブジェクトの実装

先の課題で考えたデータモデリングに基づくオブジェクトを実装してください。 どのようなデータモデリングを行ったかによって各モデルのできることは微妙に異なりますが、以下のようなことができるようにしてください。

  • ユーザーはブログに記事を書くことができる
  • ブログは記事の集合を返すことができる

プログラムのインターフェースは自由です。以下に Blog クラスと Entry クラス、 User クラスを用いたサンプルを記しますが、必ずしもこの通りになっている必要はありません。

本日の課題で書いてもらうコードそのものは翌日以降の課程では使いません。

val user = User("daiksy")

val blog = user.addBlog("だいくしーblog")

println(blog.name) // だいくしーblog

// 工夫ポイント: タプルよりかっこいい方法がありそうだ!
val (addedBlog1, entry1) = blog.addEntry("今日の日記", "今日はインターン2日目。Scalaプログラムの基本編の講義を受けた。")
val (addedBlog2, entry2) = addedBlog1.addEntry("一昨日の日記", "今日はインターン初日。最高の夏にするぞ!!!")

val entries = addedBlog2.allEntries
entries.map(_.title).foreach(println) // "今日の日記", "一昨日の日記"

テストに慣れる

「オブジェクトの実装」で実装したクラスの挙動についてのテストを記述してください。

上記の例であれば、 Blog クラスと Entry クラス、 User クラスのそれぞれについてのテストを書く、ということになります。


注意点

  • できるだけテストスクリプトを書く
    • 少くとも動かして試してみることができないと、採点できません
    • 課題の本質的なところさえ実装すれば、外部モジュールで楽をするのはアリ
    • 何が本質なのかを見極めるのも課題のうち
  • 余裕があったら機能追加してみましょう
  • 講義および教科書から学んだことを課題に反映させる
  • きれいな設計・コードを心がけよう
    • 今日のコードは翌日以降の課程では使いませんが、翌日以降は自分の書いたコードに手を入れていくことになります

課題1-2 (オプション)

課題1-1が終わって、時間や気持ちや心に余裕があったり、やる気が漲っている場合はオプション課題に取り組んでみてください。

課題1-1で実装したブログもどきに、なにか機能を追加してそれに対するテストを書いてください。

追加機能の例を下記にあげます (ここにない機能でもOKです)

  • コメント
  • ページング
  • 購読
  • トラックバック
  • リブログ
You can’t perform that action at this time.