Skip to content

Instantly share code, notes, and snippets.

@hakobe
Last active August 16, 2017 00:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hakobe/e1aa2501a64e7f801b55 to your computer and use it in GitHub Desktop.
Save hakobe/e1aa2501a64e7f801b55 to your computer and use it in GitHub Desktop.

Scala おすすめポイントご紹介


おすすめポイント

普段Scalaを使っていて、この機能は便利、よくできていると感じているところをご紹介します。


準備


開発環境

  • plenvみたいなやつあるの?
  • ライブラリはどうやっていれるの?
    • cpanfile/Gemfileみたいなのあるの?
  • 自分で書いたコードのビルドはどうやるの?

全部sbtでできる(まじか)


インストール

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

Hello, World

build.sbt

name := "hello"

scalaVersion := "2.10.3"

Hello.scala

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

実行

$ sbt
> run
[info] Compiling 1 Scala source to ...
[info] Running Hello
Hi!
[success] Total time: 27 s, completed 2014/09/13 11:37:35
>
  • Scalaの処理系(jarファイル)がインストールされる

jarファイル!! Javaだ!!


jarファイル

  • クラスファイルやリソースファイル(画像とか)が入ってるzipアーカイブ
  • META情報も入っていて、どのクラスが実行できるとか書ける
  • Scalaの処理系のjarファイルあれば原理上は実行できる

これで君もScalaが書ける!!

  • sbt でconsoleを実行するとScalaのreplに入れる
  • いろいろ試せて便利
  • 電卓にも使えるぞ! (ただしJVMが起動するのが遅い)
$ sbt
> console
scala> 1 + 1
res0: Int = 3

scala>

準備完了


Scala おすすめポイント1


変数宣言 - var と val

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

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

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
    • 細かいのを数えるといろいろある
  • コレクションには便利メソッドがめっちゃついてくる
    • List::Util + List::MoreUtils よりいろいろできる
    • 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) => // 引数リスト
  n * n  // 最後の式が返値
}

Map

  • mapもだいたい普通な感じ
  • 便利メソッドはいろいろ使える
  • とりあえず紹介だけ
val urls = Map(
  "www"  -> "http://www.hatena.ne.jp",
  "b"    -> "http://b.hatena.ne.jp",
  "blog" -> "http://hatenablog.com"
)
m.get("b") // → Some("http://b.hatena.ne.jp")
m.get("v") // → None

突然の Some(x)! None!!


Scala おすすめポイント4


Option型が便利!

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ならそのまま, flseならNoneになる
bUrl.exists { url => isHatenaUrl(url) } // Someなら条件式の結果, Noneならfalse
bUrl.map { url => getContent(url) }     // Someなら値を変換, Noneならそのまま
  • 実はListに使えたメソッドは割りと使える
    • 要素が1つしか無いListだとみなせなくもない

便利なflatMap

findEntryBy(entryId) // Option[Entry]
findUserBy(userId) // Option[User]
  • entryのauthorを取得したい
  • entryがSomeのときだけauthorを探して、authorが見つかったらSomeを返したい
  • どちらかが見つからなかったらNone
fincEntryBy(entryId).flatMap { entry =>
  findUserBy(entry.authorId)
}
  • flatMapを使うとOptionを返すメソッドを次々と繋げられる
    • 全部がSomeだったときの処理が書ける
    • ただしネストしていくと読みづらい...
    • しかし読みやすくする技がある
fincEntryBy(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 ( i <- List(1,2,3,4,5,6,7,8,9) ) {
  println(i.toString)
}

for ( i <- (0 until 10) ) {
  println(i.toString)
}
  • foreach メソッドが実装されてるとforのイテレーション対象にできる
    • 以下とほぼ一緒
List(1,2,3,4,5,6,7,8,9).foreach { i =>
  println(i.toString)
}

値を返すfor

val pows = for ( i <- List(1,2,3,4,5,6,7,8,9) ) yield i * i
  • map メソッドが実装されてるとyieldを使って値を生成できる
    • 以下とほぼ一緒
val pows = List(1,2,3,4,5,6,7,8,9).map { i =>
  i * i
}

ガードつきのfor

for ( i <- List(1,2,3,4,5,6,7,8,9) if i % 2 == 0 ) {
  println(i.toString)
}
  • filter メソッドが実装されてるとifでfilterできる
    • 以下とほぼ一緒
List(1,2,3,4,5,6,7,8,9).filter { i =>
  i % 2 == 0
}.foreach { i =>
  println(i.toString)
}

入れ子のfor

for (
  i <- List(1,2,3,4,5,6,7,8,9);
  j <- List(1,2,3,4,5,6,7,8,9)
) {
  print((i*j).toString + " ")
}
  • filterメソッドが実装されているとこうかける
List(1,2,3,4,5,6,7,8,9).foreach { i =>
  List(1,2,3,4,5,6,7,8,9).foreach { j =>
    print((i*j).toString + " ")
  }
}

値を生成する入れ子のfor

val kuku = for (
  i <- List(1,2,3,4,5,6,7,8,9);
  j <- List(1,2,3,4,5,6,7,8,9)
) yield i * j
  • flatMapとmapが実装されているとこうかける
val kuku = List(1,2,3,4,5,6,7,8,9).flatMap { i =>
  List(1,2,3,4,5,6,7,8,9).map { j =>
    i * j
  }
}
  • ならべるとflatMapをネストしてるのと一緒
val kukuku = for (
  i <- List(1,2,3,4,5,6,7,8,9);
  j <- List(1,2,3,4,5,6,7,8,9);
  k <- List(1,2,3,4,5,6,7,8,9);
) yield i * j * k
val kukuku = List(1,2,3,4,5,6,7,8,9).flatMap { i =>
  List(1,2,3,4,5,6,7,8,9).flatMap { j =>
    List(1,2,3,4,5,6,7,8,9).map { k =>
          i * j * k
    }
  }
}
  • flatMapでどんどん処理をつなげていく... どこかで聞いたことがある..

Option型をfor文で使う

  • 以下のようにOptionを返す関数をflatMapでどんどん繋げられるんだった
val result = fincEntryBy(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      <- fincEntryBy(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("hoge"), List(1,2,3)
    • bind
      • OptionやListを返す関数を組み合せる関数 => flatMap
    • これらがモナド則を満たすことが必要
  • for式はHaskellのdo式に相当する
  • OptionやList以外にもEitherやStateなどの強力な抽象化メカニズムをモナドとして使えるぞ
  • 詳しくは、すごい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を定義するとクラスメソッドを定義できる特別なobjectになる
    • コンパニオンオブジェクト
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("みけ")

Scala おすすめポイント7


implicit conversion/parameter

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

普通の型変換

def stringToInt(s:String) : Int = {
  s.toInt
}

"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宣言を調べて自動的に引数として受け取る
def say(msg: String)(implicit suffix: String) = 
  msg + suffix
  
say("hello")("!!!!!") // => hello!!!!! 普通に読んだらこう
implicit val mySuffix = "!?!?!!1" // 暗黙のパラメータを供給
say("hello") // => hello!?!?!!1

使いどころ1

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

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"

おすすめポイントまとめ

  • 紹介した機能
    • varとval
    • パターンマッチ
    • コレクション
    • Option型
    • for式
    • クラス定義
    • implicit
  • 対象を正確にモデリングできる
    • プログラマががんばってコードを読まなくても意図がわかる
    • コンパイラがエラーをチェックできる範囲が増える
  • 正直いろいろできすぎて、複雑な感じはある
    • とはいえライブラリ作者くらいしか気にしなくて良い機能とかも結構あるので意外と普通に書いてたらいい

他の話題

  • まだまだおすすめポイントはあるぞ! 無限に勉強できる!!
    • クロージャの便利な作り方
    • 型の合成
    • 型のパラメータ化
    • 関数の部分適用
    • 部分関数
    • 便利なモナド
  • 型の便利さ実感
    • renameリファクタリングしてみる
  • コンパイル時間体験
  • mackerelのよくあるコードのご紹介
  • playframeworkの世界をみる
  • slick(ORM)の世界をみる
  • scalazの闇の世界をみる
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment