Magnolia Tech

techっぽいことを書くブログです

RT-AC86Uを買った

特にゲーミングルータが必要なシビアな使い方をしているわけじゃないけど、アクセスポイントを増やす時にメッシュネットワークが組める、ということでこの機種を選定。

www.asus.com

セットアップ時に一時的に利用するアクセスポイントを用意して、セットアップが完了したら(SSIDとパスワード)、そのアクセスポイントが無効になる、という仕組みが良かった。

面倒くさがってデフォルトのSSIDをそのまま使うと、住宅街だとアクセスポイントがたくさん表示されてどれがどれだか分からなくなっちゃうことも有るので、最初から自分で設定する方がいいですね。

反面、ネットワーク名は近所に筒抜けなので、変な名前をつけないようにしないと。

特に色々なセキュリティ機能は試していないけど、電波の届く範囲も割と広めで、通常使う分には全然OKですね。

アクセスポイントを増やす時には、これを買う予定。

ScalaのmapValuesの挙動が2.13.0から変わっているので実装を調べてみた

scalaのmapValuesは、Mapのvalueだけにmapを適用したい時に使用するメソッドです(Mapとmapでちょっと分かりづらいですが)。

Scala 2.12.6で実行すると、以下のような結果になります。

scala> val characters = Map("Gandalf" -> "wizard", "Aragorn" -> "ranger")
characters: scala.collection.immutable.Map[String,String] = Map(Gandalf -> wizard, Aragorn -> ranger)

scala> val upper = characters.mapValues( x => x.toUpperCase )
upper: scala.collection.immutable.Map[String,String] = Map(Gandalf -> WIZARD, Aragorn -> RANGER)

まだ正式リリースされていないScala 2.13.0ですが、先日リリースされた2.13.0-M5という開発途中のバージョンで実行すると、以下のような結果になりました。

scala> val characters = Map("Gandalf" -> "wizard", "Aragorn" -> "ranger")
characters: scala.collection.immutable.Map[String,String] = Map(Gandalf -> wizard, Aragorn -> ranger)

scala> val upper = characters.mapValues( x => x.toUpperCase )
warning: there was one deprecation warning (since 2.13.0); for details, enable `:setting -deprecation' or `:replay -deprecation'
upper: scala.collection.MapView[String,String] = <function1>

何か明らかに挙動が変わっています。

何が変わったのか?

ふとこの挙動の変化についてツイートしたところ、@xuwei_kさんに丁寧に教えていただきました。

Scala 2.12.6における実装は以下の箇所の場所です(これも教えてもらった…ありがとうございます)。

github.com

mapValuesはMapを返すように定義されていますが、実態はMappedValuesという型のインスタンスを返しています。この時点ではインスタンスが作られるだけで実はmapが実行されていません。valuesを取り出すまで(getが実行されるまで)mapValuesの引数に渡された変換関数は実行されていない、つまり遅延評価されるということです。

この挙動が型から分からない、ということが課題になった、ということですね。確かにscaladocにも書かれていないし、型は普通のMapだし、全然分からないですね。

Scala 2.13.0-M5での実装

github.com

MapView.MapValuesという型のインスタンスを返しています。

MapViewの実装は以下の通りになっています。

github.com

先ほどの結果にキーを渡すと、変換されたvalueが得られました。

scala> upper("Gandalf")
res4: String = WIZARD

この変更はmapValuesだけでなく、filterにも適用されています。filterなんかはよく使われていると思うので、けっこう影響が大きいかもしれないですね。Scala 2.13.0へのアップデートの時には気をつけましょう。

Scala 2.13.0へアップグレードするとき

先ほどのmapValuesの件以外にも、網羅的にまとまっているマイグレーションガイドが有りますので、来たるべきScala 2.13.0リリースに向けて、こちらを見ておくと良いでしょう。

https://confadmin.trifork.com/dl/2018/GOTO_Berlin/Migrating_to_Scala_2.13.pdf

小さなPRを書いてOSS開発に貢献する

OSS開発に参加してみたいですよね!(断定)

でもどこから手をつけて良いか分からないことも多いですね。

そこでScalaのjson4sというライブラリをベースに、小さなPRを書いて、送るまでの流れを書いてみました。

参考にしてみて下さい。

今回は非推奨になったメソッドの置き換えを3カ所(3行)に対して行いました。とても短いPRですが、これで確実にJson4sをコンパイルする度に出ていた非推奨メソッドの警告メッセージが出なくなり、また今後のメンテナンス性も向上しました(非推奨になったメソッドはいつ削除されるか分からないので)。

GitHubからソースコードをcloneする

まずは(あとでPRを送るため)GitHub上で元のリポジトリをForkします。

GitHubの画面の右上にあるForkボタンを押してみてください。

自分のリポジトリにForkできたら、今度はそこから手元のローカル環境にcloneします。自分のアカウント(magnolia-k)では、以下のようになります。自分のアカウントに合わせて書き換えてください。

$ git clone git@github.com:magnolia-k/json4s.git

compileを実行する

sbtを起動して、compileを実行します。

(詳しいScalaでの開発方法は割愛します。その辺りは、Scalaの入門書などを参照して下さい)

すると、以下のようなメッセージが表示されました(のちほど出てくるPRがマージされると…残念ながらもう出なくなりますが)。

[warn] /home/xxx/json4s/jackson/src/main/scala/org/json4s/jackson/JValueDeserializer.scala:50:28: method mappingException in class DeserializationContext is deprecated: see corresponding Javadoc for more information.
[warn]       case _ => throw ctxt.mappingException(classOf[JValue])
[warn]                            ^
[warn] /home/xxx/json4s/jackson/src/main/scala/org/json4s/jackson/JValueDeserializer.scala:53:61: method mappingException in class DeserializationContext is deprecated: see corresponding Javadoc for more information.
[warn]     if (!klass.isAssignableFrom(value.getClass)) throw ctxt.mappingException(klass)
[warn]                                                             ^
[warn] /home/xxx/json4s/jackson/src/main/scala/org/json4s/jackson/JsonMethods.scala:20:25: method reader in class ObjectMapper is deprecated: see corresponding Javadoc for more information.
[warn]     var reader = mapper.reader(classOf[JValue])
[warn]                         ^
[warn] three warnings found
[info] Done compiling.

警告メッセージの意味を理解する

では実際に出た警告メッセージの理解し、対処方法を探って行きましょう。

順番が前後しますが、まずは下記のメッセージから…

method reader in class ObjectMapper is deprecated: see corresponding Javadoc for more information.

「see corresponding Javadoc for more information.」って書かれていて、具体的な場所を示してくれよ!って一瞬思いますが、そんな気持ちをぐっと堪えて、javadocを探します。

ObjectMapperJson4sが利用しているJackson-databindというJsonライブラリが持つclassです。

不要な箇所を削除しているので、完全に同じソースコードでは有りませんが…mapper.reader(classOf[JValue])readerメソッドの呼び出し箇所で警告が出ていて、そのreaderメソッドがObjectMapperというclassのメソッドであることが分かるでしょう(mapperの中身が_defaultMapperメソッドの中で生成されたObjectMapper)。

import com.fasterxml.jackson.databind._

trait JsonMethods extends org.json4s.JsonMethods[JValue] {

  private[this] lazy val _defaultMapper = {
    val m = new ObjectMapper()
    m.registerModule(new Json4sScalaModule)

    m
  }
  def mapper = _defaultMapper

  def parse(in: JsonInput, useBigDecimalForDouble: Boolean = false, useBigIntForLong: Boolean = true): JValue = {
    var reader = mapper.reader(classOf[JValue])

readerメソッドのjavadocは下記のURLで参照できます。

ObjectMapper (jackson-databind 2.9.0 API)

javadocには以下のように書かれています。

ObjectReader reader(Class<?> type)
Deprecated. 
Since 2.5, use readerFor(Class) instead

つまり、readerForというメソッドに置き換えれば良いことが分かります。

ちなみに今回は、結果的にreaderForで単純に置き換えれば良かったのですが、そもそも「なぜdeprecatedになったのか?」という理由を探すことができませんでした。急にdeprecatedになったので書き換えろ、と言われても…という気持ちになったので、READMEやChangesに書いておいて欲しいな、と思いました。

そもそも設計思想が変わった場合は当然単純な置き換えではいけないので…

次は同じメッセージが2つ出ている警告の方に取りかかります。

method mappingException in class DeserializationContext is deprecated: see corresponding Javadoc for more information.

こちらも

DeserializationContext (jackson-databind 2.9.0 API)

以下のように書かれているので、handleUnexpectedTokenで置き換えれば良いことが分かります。

@Deprecated
public JsonMappingException mappingException(Class<?> targetClass)
Deprecated. Since 2.8 use handleUnexpectedToken(Class, JsonParser) instead
Helper method for constructing generic mapping exception for specified type

しかし、よく見ると元のメソッドと戻りの型が違います。handleUnexpectedTokenObject型を返すのに対して、元のmappingExceptionJsonMappingException型を返しています。当然そのまま置き換えるとコンパイルエラーになります。

handleUnexpectedTokenメソッドのソースを追いかけてみましょう。

public Object handleUnexpectedToken(Class<?> instClass, JsonParser p)
        throws IOException
{
    return handleUnexpectedToken(instClass, p.currentToken(), p, null);
}

public Object handleUnexpectedToken(Class<?> instClass, JsonToken t,
            JsonParser p, String msg, Object... msgArgs)
        throws IOException
{
...
reportInputMismatch(instClass, msg);
return null; // never gets here
}

public <T> T reportInputMismatch(Class<?> targetType,
        String msg, Object... msgArgs) throws JsonMappingException
{
...
    msg = _format(msg, msgArgs);
    throw MismatchedInputException.from(getParser(), targetType, msg);
}

どうやら最終的に例外を送出するコードにしか到達せず、Object型の返り値は便宜的に付けられていることが分かりました。つまり元のコードの呼び出し方法を変えれば良さそうなことが分かります。

元々は例外オブジェクトを返り値で受けて、それをthrowしていました。

case _ => throw ctxt.mappingException(classOf[JValue])

handleUnexpectedTokenメソッドの中でthrowしているので、呼び出し時のthrowを削除します。

case _ => ctxt.handleUnexpectedToken(classOf[JValue], jp)

コミットする

コミットメッセージは、gitのお作法に従って、変更理由を書きましょう。

この記事なんか参考になると思います。

qiita.com

でもtypoの修正とかなら、「fixed typo」だけでも充分です。

PRを送る

修正ができたらPRを送ります。

github.com

終わりに

如何でしたでしょうか?1行のコード修正と言っても、ドキュメントや関連するソースを追いかける必要が有り、サクっと終わるわけではないですが、解決方法が分かると成長した感を実感できますね。

ほかにもドキュメントのtypoを直す、チュートリアルが最新バージョンと合っていない所を直す、サンプルコードを直す・追加する等、貢献できるところは色々と有るので、ぜひチャレンジして見て下さい。

その途中で調べることや試行錯誤することがたくさん出てきて、周辺知識がどんどん身についていくと、単純な修正だけでも得られることがたくさん有ることを実感できるのではないでしょうか。

「Scala関数型デザイン&プログラミング」のexerciseを解き進めるための環境準備

吉祥寺.pmのブログに掲載していた「Scala関数型デザイン&プログラミング」のexerciseを解く時の環境構築について、少し修正してこちらに載せ直しました。イベントブログにだけ載っているのも、もったいないな、と思って再掲。


環境構築

JDK(Java Development Kit)のインストール

Scalaはご存じの通り、JVM(Java Virtual Machine)上で実行される言語です。そのため、Scalaを使うためにはJDK(Java Development Kit)をインストールする必要が有ります。

Oracleのサイトからダウンロード

JDKのインストール方法はいろいろな所で解説されていますので、ここでは詳細は割愛しますが、自分の環境に合わせてOracleのサイトからインストーラをダウンロードしてインストールしてください。

Java SE - Downloads

Homebrewによるインストール

macOSではbrew caskコマンドでインストールできます。以下の1行で最新版がインストールされます。

$ brew cask install java

OpenJDKのインストール

Linuxの場合は、パッケージマネージャーから簡単にインストールできるOpenJDKを使った方が良いでしょう。

OpenJDK

yumや、aptといったパッケージマネージャーからインストールしてください。

Gitのインストール

Scala関数型デザイン&プログラミング」に記載されているコード例や、例題(exercise)のヒント、解答などはすべてGitHub上のリポジトリに置かれています。そのため、サンプルコードをダウンロードするためには、Gitをインストールする必要があります。

Gitのインストール方法もいろいろな所で解説されているので割愛しますが、公式サイトのドキュメントに詳しく書かれていますので、そちらを参考にしてみてください。

Gitのインストール

macOSではbrewコマンドでインストールします。

$ brew install git

サンプルコードのダウンロード

任意のディレクトリに、サンプルコードのリポジトリをクローンします。

$ git clone https://github.com/fpinscala/fpinscala.git

これで準備完了です。

特にScalaコンパイラをインストールしていませんが、そのあたりの仕組みは次の「sbtの実行」で解説します。

コードの実行準備

sbtの実行

ダウンロードしたサンプルコードのディレクトリに、sbt(Windows用はsbt.cmd)というコマンドが含まれていますので、まずはこれを実行します。

macOSや、Linuxの場合、sbtを起動します。

$ cd fpinscala
$ ./sbt

Windowsの場合は、sbt.cmdを起動します。

$ cd fpinscala
$ .\sbt.cmd

sbtは、Simple Build Toolという身も蓋もないくらい普通の名前が付けられたScala用のビルドツールです。

sbt Reference Manual — 始める sbt

sbt自体がScalaコンパイラ一式をダウンロードしてくれるので、Scala自体を手動でインストールする必要はありません。

sbtを起動して特にエラーが表示されなければ、準備はOKです。

sbtはひじょうに高機能なビルドツールですが、のちほど説明するprojectcompileconsoleruntestの5つのコマンドを覚えれば、以降のexerciseのコードを実装する上では十分です。

  • project

sbtでは一つのリポジトリで複数のプロジェクトを管理できます。そのプロジェクトを切り替えるためのコマンドです。どのようなプロジェクトが含まれているかは、projectsコマンドで確認します。

  • compile

ターゲットのプロジェクトに含まれるすべてのソースコードコンパイルします。

  • console

    sbtからScalaのREPL(Read - Eval - Print - Loop)を起動します。

  • run

ScalaJavaと同様にプログラムの実行は、main関数から始まります。runは、main関数を実行します。

もし、プロジェクト内に複数のmain関数が含まれる場合は、どのパッケージに属するmain関数を実行するか、選択するためのリストが表示されます。

  • test

Javaや、Scalaでは、テストコードはsrc/test/*ディレクトリに格納されますが、testは、testディレクトリに格納されたテストコードを実行します。

PermSizeの削除

macOSや、Linux環境で実行する際には、sbtコマンドを使いますが、Java8以降では不要なパラメータ(PermSize)が書かれており、実行のたびに警告メッセージが出てしまうので、Java8以降のJDKをインストールしている場合は、下記の通り書き換えることをお勧めします。

sbtは単なるシェルスクリプトで、実態はsbt-launch.jarにパラメータを与えて起動しているだけです(Windows用のsbt.cmdには最初から記述は有りません)。

変更前

SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"

変更後

SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled"

brachの作成

以降、コードの実装を始めますが、サンプルコードのリポジトリは今でも日々コミットが続いています。自分が書いたコードを区別するためにも、gitのbranchを作成しておきましょう。

$ git checkout -b myexecirse

コードの実装

サンプルコードのディレクトリ構成は大きく分けて、以下の3つに分かれています。

exerciseと、answerが対になっているので、テキストエディタを上下分けて、それぞれ表示しながら進めるのがおすすめです。

exercise

書籍に掲載されているサンプルコードと、exerciseで書かれている関数のひな形(関数名と、引数、返値だけが書かれている)が掲載されています。例えば最初のリストの実装であれば、以下のファイルを書き換えていく形になります。

https://github.com/fpinscala/fpinscala/blob/master/exercises/src/main/scala/fpinscala/datastructures/List.scala

関数のひな形が掲載されているものはコードの本体が「sys.error("todo")」となっていて、実行するとエラーになるようになっています。まずはこの「sys.error("todo")」という部分を削除して実装を始めることになります。

def tail[A](l: List[A]): List[A] = sys.error("todo")

def tail[A](l: List[A]): List[A] = l match { ... }

なぜか関数のひな形が書かれていないものや、本誌に書かれたひな形と引数名が違ったりするものも有りますが、後述のanswerに書かれている回答の関数名と、引数、返値を見ながら進めると良いでしょう。

answer

exerciseの回答と、その解説が書かれています。

先ほどのListの回答は、以下のファイルに書かれています。

https://github.com/fpinscala/fpinscala/blob/master/answers/src/main/scala/fpinscala/datastructures/List.scala

answerkey

exerciseの回答がファイル別に置かれています。また、回答ごとに、ヒントも置かれているので、まずはこのヒントを見ながら実装していくと良いでしょう。

コンパイルと、実行

exerciseのディレクトリ配下に置かれているファイルを書き換えながら進めていくので、exerciseのディレクトリだけがコンパイルされるようにsbt上のプロジェクトを切り替えておきます。

また、compileコマンドでエラーが無いことを確認したら、consoleコマンドScalaのREPL(Read-eval-print loop)が起動するので、そのまま実装したパッケージ(Listでいえば、fpinscala.datastructures)をロードすることで、実装の確認ができます。

$ ./sbt
> project exercise
> compile
> console
scala> import fpinscala.datastructures._
scala> val x = List.Cons(1, Cons(2, Nil))
...

エラーが出たり、挙動が正しく無いときは、実装の正しさを、answerか、answerkeyを参照して確認します。

あとはひたすら繰り返しです。頑張りましょう。

runコマンド

なお、例題によってはmain関数を起動するものも有ります(第2章gettingstartedなど)。それらのmain関数を起動するときはsbtからrunコマンドを実行します。

プロジェクトの中には、複数のmain関数が有るので、どれを起動するか選択するリストが表示されます。パッケージ名を確認して、該当する番号を入力して、エンターキーを押下してください。

> run
...
Multiple main classes detected, select one to run:

 [1] fpinscala.streamingio.ProcessTest
 [2] fpinscala.gettingstarted.MyModule
 [3] fpinscala.gettingstarted.FormatAbsAndFactorial
 [4] fpinscala.gettingstarted.TestFib
 [5] fpinscala.gettingstarted.AnonymousFunctions
 [6] fpinscala.iomonad.IO2aTests
 [7] fpinscala.iomonad.IO2bTests

Enter number:

実装の記録(tips)

exerciseのファイルには、関数のひな形は書かれていても、対象のexerciseの番号が書かれていません。あとで振り返ったときに分かりづらいので、コメントでexerciseの番号を書いておくと便利です(「// exercise 3.3」のような形式で書いておきます)。

また、いろいろと自分で気がついたところも都度コメントで残しておくと、あとで振り返ったときに便利です。

章ごとに実装が終わったら、gitでコミットしておくとよいでしょう。

また、急激に難易度が上がるところや、最初から「難問」と書かれているような例題も有りますが、そうゆうときはさっさとanswerのコードを写経して、挙動や実装の背景を理解するようにシフトした方が良いでしょう。そのときに気がついたことをひたすらコメントで残しておく方が理解が早いでしょう。

テストコードの追加

sbtconsoleを使って実行結果を確認しても良いですが、いまどき実行結果はテストコードを書いて、テスト結果で確認すべきです。

以下に、exerciseで書いたコードをテストコードで確認する方法を紹介します。

Scalaのテスティングフレームワークとしては、ScalaTestか、Specs2がよく使われていますが、ここではScalaTestを使うことにします。

ScalaTest

依存ライブラリの追加

Build.scalaのoptsに、ScalaTestへの依存を追加します。

resolversの最後にカンマを追加するのを忘れないように。

val opts = Project.defaultSettings ++ Seq(
  scalaVersion := "2.11.7",
  resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/",
  libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0" % "test"
)

テストコードの追加

exercises/src/test/scala/fpinscala/datastructures/ListTest.scalaというテストコードのファイルを用意します。パスの3番目がtestになっていることに注意して下さい。Javaではおなじみですが、テストコードはtestディレクトリに保存します。

例えば、3章で出てくるtailメソッドのテストコードは以下のように書けます。

ScalaTestではいくつかのテストスタイルが使えますが、ここでは一番シンプルに書けるFunSuiteを使っています。

最後のshould equalで期待する値との比較をしています。

import org.scalatest._

import fpinscala.datastructures._

class ListSuite extends FunSuite with Matchers {
  test("tailメソッドは先頭の要素を削除する") {
    val listInt = List(1, 2, 3)
    val listDouble = List(1.0, 2.0, 3.0)
    val listString = List("one", "two", "three")

    List.tail(listInt) should equal (List(2, 3))
    List.tail(listDouble) should equal (List(2.0, 3.0))
    List.tail(listString) should equal (List("two", "three"))
  }
}

テストはsbtから実行します。

$ ./sbt
> test
...
[info] ListSuite:
[info] - tailメソッドは先頭の要素を削除する
...
[success]...

最後に[success]が出力されればテスト全体が成功したことになります。テストが一つでも失敗すると[error]が表示されます。

どうように3章に出てくるsetHeadメソッドのテストを追加します。

import org.scalatest._

import fpinscala.datastructures._

class ListSuite extends FunSuite with Matchers {
  val listInt = List(1, 2, 3)
  val listDouble = List(1.0, 2.0, 3.0)
  val listString = List("one", "two", "three")

  test("tailメソッドは先頭の要素を削除する") {
    List.tail(listInt) should equal (List(2, 3))
    List.tail(listDouble) should equal (List(2.0, 3.0))
    List.tail(listString) should equal (List("two", "three"))
  }

  test("setHeadメソッドは先頭の要素を置き換える") {
    List.setHead(listInt, 4) should equal (List(4, 2, 3))
    List.setHead(listDouble, 4) should equal (List(4.0, 2.0, 3.0))
    List.setHead(listString, "four") should equal (List("four", "two", "three"))
  }
}

Chapter Note

すべて英語で書かれていますが、本書に載っていないChapter NotesがGitHubWikiに有りますので、時間に余裕があればこちらも読んでおくと参考になります。

Home · fpinscala/fpinscala Wiki · GitHub

おわりに

Scala関数型デザイン&プログラミング」は非常に噛み応えが有るというか、じっくり取り組む必要が有るし、exerciseのコードを書いてみても、書いたことの意味をきちんと解説してくれる人が近くにいないと、挫折し易いというか、本当にストロングスタイルな本ですが、最後まで進めると確実に実力がつく良本ですね。

Scala関数型デザイン&プログラミング」はScalaの入門書ではないので、Scalaの入門書としては元祖Scalaの解説本である「Scalaスケーラブルプログラミング」の方がおすすめです。つい先日最新の第3版が邦訳されました。

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

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

なお、技術書と言えば定番のO'Reillyからも何冊かScala本がリリースされていますが、邦訳がなかなかリリースされないですね。

Programming Scala: Scalability = Functional Programming + Objects

Programming Scala: Scalability = Functional Programming + Objects

Learning Scala: Practical Functional Programming for the JVM

Learning Scala: Practical Functional Programming for the JVM

Scala Cookbook: Recipes for Object-Oriented and Functional Programming

Scala Cookbook: Recipes for Object-Oriented and Functional Programming

Scalatra-Jsonがレスポンスを返す仕組み

前回のエントリで、Scalatraがレスポンスを返す仕組みについて紹介しました。Any型で返すのは型安全的にどうなの?という気持ちも有り、いつの日か変えたいという気持ちですが(Scalatra 3.0?)、現状はそんな仕組みになっています。

blog.magnolia.tech

Scalatra-JSON

前回説明した通り、ScalatraではAny型で受けたコードブロックの返り値を、パターンマッチで振り分けます。

NativeJsonSupport traitか、JacksonJsonSupport traitをmix-inすると、下記のrenderPipelineメソッドが有効になります。

github.com

github.com

Json4sがXMLをサポートしている関係で大量のXML関係のコードが含まれていますが、飛ばしながら読み進めていきましょう。

JValueResult traitが持つrenderPipelineメソッドがもっとも優先的に起動されます。対応するcaseが無ければ、orElse super.renderPipelineにより次のrenderPipelineへ処理が委譲されます。ここでのsuperが指す先はJsonOutput traitrenderPipelineメソッドになり、更にそのsuperが指す先はScalatraBase traitrenderPipelineメソッドになります。

JValueResult traitが持つrenderPipelineメソッドの一部を以下に引用します。Json4sのASTであるJValue型であれば、すぐにJsonOutput traitrenderPipelineへ移譲されていることが分かるでしょう。また、詳しくは後述しますが、case a: Any if isJValueResponse && customSerializer.isDefinedAt(a)によりJson ASTへ変換可能なcase classがJValue型として処理されていることが分かるでしょう。

  override protected def renderPipeline: RenderPipeline = renderToJson orElse super.renderPipeline

  private[this] def renderToJson: RenderPipeline = {
    case JNothing =>
    case JNull => response.writer.write("null")
    case a: JValue => super.renderPipeline(a)
    case a: Any if isJValueResponse && customSerializer.isDefinedAt(a) =>
      customSerializer.lift(a) match {
        case Some(jv: JValue) => jv
        case None => super.renderPipeline(a)
      }
    case status: Int => super.renderPipeline(status)

また、status: Intのように一旦JValueResult traitrenderPipelineで処理されるけど、すぐにsuperを呼び出しているパターンが有ることも分かるでしょう。

JsonOutput traitrenderPipelineは以下のようなコードになっていて、JSONPへ対応するコードが混じっていて分かりづらいですが、通常のJSONであればwriteJson(transformResponseBody(jv), writer)というコードでwriterに渡されていることが分かるでしょう。

override protected def renderPipeline = ({

    case JsonResult(jv) => jv

    case jv: JValue if format == "xml" =>
      contentType = formats("xml")
      writeJsonAsXml(transformResponseBody(jv), response.writer)

    case jv: JValue =>
      // JSON is always UTF-8
      response.characterEncoding = Some(Codec.UTF8.name)
      val writer = response.writer

      val jsonpCallback = for {
        paramName <- jsonpCallbackParameterNames
        callback <- params.get(paramName)
      } yield callback

      jsonpCallback match {
        case some :: _ =>
          // JSONP is not JSON, but JavaScript.
          contentType = formats("js")
          // Status must always be 200 on JSONP, since it's loaded in a <script> tag.
          status = 200
          if (rosettaFlashGuard) writer.write("/**/")
          writer.write("%s(%s);".format(some, compact(render(transformResponseBody(jv)))))
        case _ =>
          contentType = formats("json")
          if (jsonVulnerabilityGuard) writer.write(VulnerabilityPrelude)
          writeJson(transformResponseBody(jv), writer)
          ()
      }
  }: RenderPipeline) orElse super.renderPipeline

このように、Scalatraではコードブロックが返す値の型に応じて色々な処理へ分岐していること、新しい処理をカスケードして追加できることが分かってもらえたでしょうか。

case classへの対応

既にコードは出てきましたが、ScalatraではAnyで受けた値を、パターンマッチにより処理を振り分けています。また、Json4sにはcase classへのマッピング機能があります。そのため、Json4sの機能を使って、Json ASTへ変換可能なcase classを受け取った場合は、自動的にJson ASTへ変換して処理したいわけです(わざわざ呼び出し側でJson ASTに変換してから呼び出すのもアレなので)。

Json4sにはCustomSerializerという機能が用意されていて、これによりターゲットとしているオブジェクトがJson ASTに変換可能であるか判定することができます。

これは他のJsonモジュールにはない、Json4s特有の機能です。この機能があるため、Anyで返したオブジェクトがJson ASTに変換可能か、判断することができるようなっています。

Scalatra in Action

Scalatra in Action

Scalatraがレスポンスを返す仕組み

Scalatra-JSONがレスポンスを返す仕組みについて書こうとしたら、その前にそもそもScalatraがレスポンスを返す仕組みを解説しないと分かりづらいな、と思ったので、まとめます。

Scalatraがレスポンスを返す仕組み

Scalatraでは、下記のようにgetpostといったメソッド名に、対応するパスとコードブロックを渡すことでWebアプリケーションとしての振る舞いを定義していきます。

package com.example.app
import org.scalatra._

class HelloWorldApp extends ScalatraFilter {
  get("/") {
    <h1>Hello, {params("name")}</h1>
  }
}

このメソッド名形式での定義は、CoreDslというtraitの中で定義されています。例えばgetメソッドは以下のように定義されていて、transformersがパス名を、Any型を返すコードブロックが実際のWebアプリケーションの挙動を定義するコードブロックを示します。

def get(transformers: RouteTransformer*)(block: => Any): Route

Scalaでは、コードブロックの最後の値が、コードブロック全体の返り値となるので、上記のHelloWorldAppの例で言えば、最後の値である<h1>Hello, {params("name")}</h1>がコードブロック全体の帰り値となります。ScalaではXMLリテラルが有るので、このコードブロックの値はXML型(scala.xml.Elem)になります(型推論される)。

メソッドの引数定義が=> Anyになっているので、getメソッドはAny型として受け取ります。

そして、最終的にScalatraBaseというtraitのrenderPipelineというメソッドで処理されます。詳細は省きますが、renderPipelineはpartial functionとして定義されていて、後から色々なパターン(case)への対応を追加できるようになっています(例えば、JSONのASTを表す型への対応はScalatraBaseには含まれていません)。

renderPipelineメソッドの一部を引用します。

protected def renderPipeline: RenderPipeline = {
    case 404 =>
      doNotFound()
    case ActionResult(status, x: Int, resultHeaders) =>
      response.status = status
      resultHeaders foreach {
        case (name, value) => response.addHeader(name, value)
      }
      response.writer.print(x.toString)
    case status: Int =>
      response.status = status
    case bytes: Array[Byte] =>
      if (contentType != null && contentType.startsWith("text")) response.setCharacterEncoding(FileCharset(bytes).name)
      response.outputStream.write(bytes)

    case x =>
      response.writer.print(x.toString)
  }

パターンマッチは上から順に適合性をチェックしていくので、コードブロックの返り値が404だったらdoNotFoundメソッドが実行され、単なるInt型であればそのままステータスコードとしてのみ使われます。特に適合するものが無ければ、最後に文字列化されてresponsewriterに渡されます。先ほどの例でいけば特にXML型特有のコードはここには無いので、case x =>のパターンにマッチして<h1>Hello, {params("name")}</h1>が文字列として返されます。

ソースコードの全体は以下のリンクから参照してみてください。

github.com

Actions - Scalatra

Surface Precision Mouseを買った

Surface Precision Mouse

Surface Precision Mouse

長年使っていたMagic Mouseが使っている最中に頻繁に接続が切れてしまうようになったので、ずっと気になっていた「Surface Precision Mouse」を購入。

Surface用なのでmacOSで使ってもフル機能が使えるわけでもないけど、大きさといい、重さといい、さすが伝統のMicrosoftマウス、過去に使ったマウスの中では最高の出来映え。デザインもシンプルで、他社の上位機種のようなゲーミング感が無くて良い。

注意事項としては、サイドのボタンが使えないのと、Magic Mouseと比べるとスクロールの方向が逆になることだけど、まぁ元々macOS用ではないので。

Windowsと共用する人にはお勧めだけど、macOSしか使わない人はちょっと価格が高めなので、微妙かも。