はこべブログ ♨

ソフトウェア開発のことやアニメのことを書いてることが多いです

社内技術勉強会でScalaのおすすめポイント解説した

はてなでは週に一回、社内技術勉強会というのをしています。今週は僕の当番だったのでScalaの入門的な話をしました。

普段使いの言語として、Scalaの便利なところをまとめたというつもりです。とはいえ、他の言語にもある特徴もわりと紹介してるので、もうちょっとScala独自の内容にフォーカスしてもよかった... むずかしい。時間の都合で全部話きれなくて、会が終わった後でimplicitまわりの話とか数人にご紹介したら一番おもしろかったと言う話になったので無念。

あの機能を紹介してないとはけしからんみたいなのがあったら教えて下さい。そうはいってもとりあえず
Scalaスケーラブルプログラミング第2版を読むといいです。



↓ 資料はgist形式でembedしてあります ↓


https://gist.github.com/hakobe/e1aa2501a64e7f801b55

こちらもおすすめ

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

AnimeKansaiをHeroku化した

ずいぶん昔に関西のアニメ放送情報を10分前に通知してくれるAnime KansaiというTwitter botを作った。


Twitter/AnimeKantoがうらやましかったのでTwitter/AnimeKansaiを作った - はこべブログ ♨

このTwitter botは自分のVPSで動かしてて、10分ごとにcronで起動されているという仕組みだった。結構便利に使って頂いてるのだけど、TwitterAPIの仕様が変わるとか、うちの怠慢とかがあって、よく止まってしまっていた。

これまでは、スクリプトが自分しかアクセスできないVPSに置いてあって、とくにリポジトリで管理されてなかったのでとりあえずコード変更がしづらかった。cronでちまちまうごかしているというのも、プログラムの様子を調べずらくて厳しかった。VPSをたまに引っ越すときとかにも移管が地味に面倒だった。

ということでAnimeKansaiのコードをHerokuで動くbotとして書きなおした。今どきはHerokuの無料枠でbotをうごかし続けられるので大変便利。開発やDeployも気楽にできるしログの様子も簡単に見れる。もともとはPerlで書かれたすぐに終了するスクリプトだったけど、Node.js + CoffeeScript で常時起動するスクリプトに書き換えてcronいらず。

READMEにも書いてあるけど、任意の地方のアニメ放送情報をツイートできるようになってる。しょぼいカレンダーのアカウントとbot用のTwitterアカウントがあればだれでもbotをDeployできるはず。以下のボタンからご利用ください。

ボタンを押すとHerokuのログインしたら以下の様な画面が出てくるの環境変数を入れたら利用開始できるのでお手軽。bot用のTwitterアカウントでTwitterアプリケーションを作って、管理画面のAPI Keysタブから Create my access tokenすると、楽にaccess tokenが取得できるでしょう。

f:id:hakobe932:20140907122507p:plain

Scala in Perl Company という内容で発表しました #yapcasia【YAPC::Asia Tokyo 2014】

Scala In Perl Company : Hatena - YAPC::Asia Tokyo 2014

Scala in Perl Company という内容で発表しました。はてなではPerlを10年近く利用していますが、最近作っているMackerelというサービスの開発ではScalaを利用しています。この発表では、Perlによるソフトウェア進化の困難さと、Scalaでその課題がどのように解決できるかについて解説しました。

発表時間がたりず、ScalaでのWebアプリケーション開発についてはくわしく紹介できなかったのですが、資料にはその部分も含めています。雰囲気でもつたわればうれしいです。話しきれないところがいろいろあったので、まだ会場にいる方はぜひ声をかけてください!



【予告】YAPC::Asia Tokyo 2014 で話します

Scala In Perl Company : Hatena - YAPC::Asia Tokyo 2014

明日の昼、13:40から20分となっております。

最近はてなScalaのプロジェクトをはじめてるので、なんで別言語をはじめたのかとか、なんでScalaを選んだのかとかについて話します。時間がありそうなら、Scalaで書かれたWebアプリケーションをどういう感じで作っているかという話もします。

プログラミング言語の基礎概念の練習問題を解くプログラムを作った(EvalML3まで)

プログラミング言語の基礎概念を学んでる - はこべブログ ♨ の続きです。

前の記事では、プログラミング言語の基礎概念という本を紹介した。この本では学んだことを確認するために、S(S(Z)) + Z evalto S(S(Z))のような式が正しいことを、与えられた推論規則にもとづいて導出するという練習問題が用意されている。しかし、本の中盤くらいの問題から人間ががんばって手で解くのが辛くなってくる。( 例えばこういうのを丁寧に書かないといけない https://gist.github.com/hakobe/860fd1dd9c56c1d33a33 )

前回の記事についたブックマークの中で、id:OKU_s62 さんが以下のようにおっしゃていた。

後半の問題の導出木を書くのは人間業ではないので、導出木を生成するプログラムを書きましょう

http://b.hatena.ne.jp/OKU_s62/20140714#bookmark-205135689

なるほど、たしかに。ということで、問題を解くプログラムを作ってみた。本の中で説明されているEvalML3という言語の式を、したにあるtextareaに与えると導出木を作って出力してくれる。出力を、本の演習システムにコピペすると正解という判定になるはず。

下のtextareaの中身を適当に変更すると導出木も変わるのでおためしあれ。サンプルにいくつかEvalML3の式をリストしてあるので、コピペするとためせる。

  • y = 3 |- let add = fun x -> y + x in add 5
  • |- let twice = fun f -> fun x -> f (f x) in twice twice (fun x -> x * x) 2
  • |- let rec fib = fun n -> if n < 3 then 1 else fib (n - 1) + fib (n - 2) in fib 5

人間がやると辛い問題もすばやく結果が返ってきて、ありがたい。フィボナッチ数を計算してる式とか絶対手でやりたくなかったし助かった。

CoffeeScriptでわりと雑な感じに実装したけどおおよそ動いてそう。ソースコードは、hakobe/copl · GitHubにおいてある。

実装は、入力した式をパースしたあと、ASTをたどりながら計算を実行しつつも同時に導出木を作る、という風になってる。ほぼインタプリタを作ったと言う感じになった。本には、パーサを書くための厳密なBNFは書かれていないので、自分で適当にBNFを考えてパースするのだけど、BNFを見ただけでLL文法なのかLR文法なのかを判定する知識がなかったので困った。id:tarao 先生曰く、LALRのパーサがあれば十分なはずとのことだったので、jisonというJavaScrip用パーサジェネレータを使うことにした。jisonはCoffeeScriptをパースするのにも使われて実績があって、ほぼbisonと同じことができて高機能。文法は、こういう感じにに書ける。

実は、今定義してある文法にはshift/reduce conflictが残ってる。例えば、パーサが1 + f まで読んで、次のトークンが3だったときに、1 + f でreduceするか、1 + f 3 のようにみなすためにshiftするかが曖昧になっているっぽい。これはEvalML3の関数適用の文法が原因なのはわかってる(このへん)。結局解決方法はわかってない。衝突した場合はshiftが優先になるらしく、意図どおりなので放置してるけど、なんとかしたいという気持ちは残ってる。

本題とは関係ないけど、もともと手元のNode.jsで動かしてためしていたコードを、browserifyでブラウザ上で動かすというのもやってみた。gulp, TypeScript, Browserify で Chrome 拡張を書く - 詩と創作・思索のひろば (Poetry, Writing and Contemplation)がともても参考になった。構文定義をコンパイルしたり、coffeeのファイルをコンパイルしたりというのもあるので、gulpでビルドの仕方を記述してみている(このへん)。

本はまだ途中で今後少しづつ言語が拡張されていくのだけれど、それに合わせてこのプログラムも拡張していけば、演習問題も余裕で解けるはず。人間がコンパイルするのはまちがってるので、自動化成功してよかった。

プログラミング言語の基礎概念を学んでる

この本を読んで学んでる。まだ半分くらいで関数の定義とかについて勉強してる。

プログラミング言語の動作を数学的に厳密に記述する方法を順番に教えてくれるという内容で、記述には導出システムが用いられてる。基本的な算術式からはじまって、変数の定義や関数の定義、パターンマッチや型システムなど、様々な言語の機能を推論規則によって定義する方法を教えてくれる。与えられた規則が意味的に意図したものを表しているかの証明だけでなく、証明のやり方もくわしく説明されていて丁寧でたすかる。

おもしろいのはこの本のためのオンラインの演習システムというのがあって、本の中で与えられた導出システムに基づいて式が正しいことを導出する練習問題をすることができる。サイト内に検証器がうごいていて、自分が与えた導出が正しいかを判定してくれる。どこまで問題を解いたか記録してくれたりもするし、ゲームっぽい感じで勉強できておもしろい。

プログラミング言語の基礎概念 演習システム

雰囲気だけお伝えすると、2 + 0 が 2に評価できることを導出するために、以下の様に書くと正解という感じ。S(S(Z))ていうのはペアノ自然数の表現で、チャーチ数みたいなやつ。by E-Plus みたいなのはどのような推論規則を適用したかという雰囲気。詳しくは本読むか演習システムのドキュメント読むと雰囲気わかる。

S(S(Z)) + Z evalto S(S(Z)) by E-Plus {
    S(S(Z)) evalto S(S(Z)) by E-Const {};
    Z evalto Z by E-Const {};
    S(S(Z)) plus Z is S(S(Z)) by P-Succ {
        S(Z) plus Z is S(Z) by P-Succ {
            Z plus Z is Z by P-Zero {}
        }
    }
}

そんなに複雑な問題はないんだけど、たまにきついのがあるっぽくて油断できない。以下の様な式が正しいことを導出するっていう問題があって、たしかに面倒そうな気はするなーと思ってとりくんだら、めっちゃ大層なことになった

let twice = fun f -> fun x -> f (f x) in twice twice (fun x -> x * x) 2 evalto 65536

問題を解くのは難しいわけではなくて、ルールにしたがって丁寧に間違えずに式を変換していけば、いずれ導出がえられる。ただ、少しでもまちがえると全部おかしくなるので、まちがえずに丁寧にやる必要がある。同じようなことを何度も丁寧にまちがえずにやる必要があって、コンパイラの気持ちが体験できる。

ScalaでWebアプリケーションのエラー処理を綺麗に書く

Play Frameworkにおいて、POSTリクエストから得られたbody中のパラメータをもとに何か処理をするというよくあるコードを、ちょっと整理して見やすくする方法を学んだのでメモがてら御シェアさせていただきます。Playのリクエストハンドラを書くときに頻繁に現れたので、例がPlayのコードになっているけど、内容的にはScala全般的な話だと思う。Scalaプロみたいな人にはまったく新しいことはないと思う。

本題と関係ないけど、YAPCScalaの話をするかもしれません。言語自体の話よりかは採用理由とか開発フローの話を、これまでのPerlでのWeb開発を踏まえて話す感じになりそう。Scala In Perl Company : Hatena - YAPC::Asia Tokyo 2014

さて、本題ですが、話題の対象になるのは以下の様なPlayFrameworkのコードです。

def update = Action { implicit req =>
  req.body.asFormUrlEncoded.fold(BadRequest("Wrong POST body")) {
    data =>
      Form(tuple( "id" -> number, "title" -> text, "body" -> text )).
        bindFromRequest(data).value.fold(BadRequest("Wrong parameters")) {
          case (id, title, body) =>
            find(id).fold(NotFound("No entry found")) {
              entry =>
                save(id, title, body)
                Ok("Success!")
            }
      }
  }
}

上の例では、以下の3つの処理が、Option型の値を返却します。返却されたOption型の値がNoneだった場合( = 計算に失敗した場合)、処理を中断してエラーレスポンスを返しています。

(Scalaでは失敗するかもしれない計算結果はOption型に包んで扱います。Option型を使うことで、値が存在することを確認しないかぎり、包まれた値を利用できません。詳しくはゆるよろさんの記事が詳しいのでおすすめです。)

  • req.body.asFormUrlEncoded(PlayのAPI) bodyをapplication/x-www-form-urlencodedとして読み込みパラメータを得る
    • 結果がNoneだったら: "Wrong POST body"という内容のBadRequestレスポンスを返す
  • Form(...).bindFromRequest(data).value(PlayのAPI) によりFormの定義にbodyから得られたパラメータを結びつけて値を得る
    • 結果がNoneだったら: "Wrong parameters"という内容のBadRequestレスポンスを返す
  • find(id)(独自に定義した関数) によりパラメータから得られた値を使ってentryを得る
    • 結果がNoneだったら: "No entry found"という内容のNotFoundレスポンスを返す

Option型を処理するのには比較的コンパクトにかけるfoldメソッドを利用しています。Option型のfoldメソッドはOption型の値がNoneだった場合は一つ目の引数リストの値を、Someだった場合は二つ目の引数リストに与えられた関数に、包まれた値を渡して評価した結果を返します。

Option型を使うことで、漏れ無く計算が失敗した場合の処理を書くことができていますが、ご覧のとおり読みにくいです。どの部分が本来行いたい処理で、どの部分がエラー処理なのかが判別しづらくなっています。

そこで、Option型はflatMapメソッドを実装していますから、forを使って整理してみることにします。

def update2 = Action { implicit req =>
  (for {
    data <- req.body.asFormUrlEncoded
    form <- Some(Form(tuple( "id" -> number, "title" -> text, "body" -> text )))
    (id, title, body) <- form.bindFromRequest(data).value
    entry <- find(id)
  } yield (id, title, body)) match {
    case None =>
      BadRequest("Something wrong")
    case Some((id, title, body)) =>
      save(id, title, body)
      Ok("Success!")
  }
}

ご覧のようになりました。パラメータの準備と検証をforの中で行い、いずれかが失敗した場合にはcase分のNoneの節が、すべて成功した場合にはSomeの節が実行される、という読むことができます。正常系と異常系のコードを分けて読むことができ、比較的わかりやすくなりました。

ただ、この例には問題があります。Option型の値を連ねたfor文からは、計算が失敗した場合に、いずれかの計算が失敗したことはわかりますが、どの計算が失敗したのかがわかりません。そのためエラー処理を分岐できず、とりあえず、"Something wrong"という内容でBadRequestを返すという雑なエラー処理をするはめになっています。

さて、このような計算が失敗したときの理由も取り扱いたいときには、Either型を使うのが便利であると、すごいHaskellたのしく学ぼう!にはありました。独習ScalazのEitherの解説をよめば、Scalaでの使い方もわかります。

ここではOption型の値をtoRightメソッドを使って、Either型に変換し、それぞれの計算で別のエラーを取り扱えるようにしてみます。ScalaではEither型にはflatMapが実装されていないので、Either型のrightメソッドを呼び出して、Either型のサブクラスのRightProjection型に変換して使います。RightProjection型のflatMapメソッドは値がRightである場合に計算を継続します。

Option型の値をRightProjection型の値に変換することで、計算結果がRightであれば計算を継続し、Leftの場合は計算を途中で中止してその値を返すという振る舞いを実現できます。

def update3 = Action { implicit req =>
  (for {
    data <- req.body.asFormUrlEncoded.toRight(
      BadRequest("Wrong POST body")).right
    form <- Right(Form(tuple( "id" -> number, "title" -> text, "body" -> text ))).right
    params <- form.bindFromRequest(data).value.toRight(
      BadRequest("Wrong parameters")).right
    entry <- find(params._1).toRight(
      NotFound("No entry found")).right
  } yield params) match {
    case Left(error) =>
      error
    case Right((id, title, body)) =>
      save(id, title, body)
      Ok("Success!")
  }
}

ご覧のようになりました。計算の見通しが良いまま、それぞれの計算ごとに失敗した場合の計算結果を返すことができるようになりました。RightProjection形のfilterメソッドの制限か何かで、forの中でパターンマッチが使えないのが惜しい。

追記: id:xuwei さんに教えていただいたところによると、Eitherについて自然なfilterを定義できない(Optionを返すようなMonadPlusに適合しないシグニチャになってる)ために、forの中で利用できないとのことでした。ありがとうございます。参考: HaskellのdoとScalaのfor式とEitherとMonadPlus - scalaとか・・・

追記2: ScalazのEither型相当の\/型はfoldMapはRight優先で実装されているので、それを使うとちょっとスッキリする。

import scalaz._
import Scalaz._

def update5 = Action { implicit req =>
  (for {
    data <- req.body.asFormUrlEncoded \/>
      BadRequest("Wrong POST body")
    form <- Form(tuple( "id" -> number, "title" -> text, "body" -> text )).right
    params <- form.bindFromRequest(data).value \/>
      BadRequest("Wrong parameters")
    entry <- find(params._1) \/>
      NotFound("No entry found")
  } yield params) match {
    case -\/(error) =>
      error
    case \/-((id,title,body)) =>
      save(id, title, body)
      Ok("Success!")
  }
}

インデントが深くなるのが嫌であれば、エラーが発生した時点でreturnすれば分かりやすいしええやん?という風にも思えます。例えば以下の様になります。

def update4 = Action { implicit req =>
  def handle: Result = {
    val dataOption = req.body.asFormUrlEncoded
    if (dataOption.isEmpty) {
      return BadRequest("Wrong POST body")
    }
    val form = Form(tuple( "id" -> number, "title" -> text, "body" -> text ))
    val valueOption = form.bindFromRequest(dataOption.get).value
    if (valueOption.isEmpty) {
      return BadRequest("Wrong parameters")
    }
    val (id, title, body) = valueOption.get
    val entryOption = find(id)
    if (entryOption.isEmpty) {
      return NotFound("No entry found")
    }

    save(id, title, body)
    Ok("Success!")
  }
  handle
}

たしかに、シンプルな手続きの並びでわかりやすくはあります。ただし、Option型の中の値が入っているかをチェックする部分と使う部分が別になってしまい、コンパイル時に値の取り出しが安全かどうかを判定できなくなってしまいました(getを使っているためランタイムエラーの可能性が残る)。わざわざコンパイラによるチェックがされない方法をとるメリットはなさそうです。Actionが引数にクロージャをとる都合上、returnを使うために内部に関数を定義しないと行けないのもつらいところですね。

まとめ

  • Either便利
  • for文便利
  • 追記: Scala本体に付いているEither型はちょっと微妙。Scalazについているやつは便利

とりあえずfor文をつかっておしゃれに書きたい期に突入してると思う。

"Scala RightProjection"とかでググってたら、エラー処理をいい感じに書く議論がScalaハッカーの方々の間で執り行われているのを見つけて、めちゃくちゃ参考になった。RightProjection以外のいろんな方法について言及されている。

Scalaハッカーの方々による議論

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!