Magnolia Tech

いつもコードのことばかり考えている人のために。

「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しか使わない人はちょっと価格が高めなので、微妙かも。

Effective DevOpsを読んだ

Effective DevOps ―4本柱による持続可能な組織文化の育て方

Effective DevOps ―4本柱による持続可能な組織文化の育て方

買ったはいいけど、なかなか読む時間が取れなかった「Effective DevOps」を読み始めた。

一言でまとめれば、以下のツイートで全部かな。

以下、章ごとの感想

1章〜4章

1章から4章までは歴史や概念の説明なので、少しでも最近の開発手法を知っていたり、それなりの経験がある人であれば後から読んでもいい気もするが、2.2.2だけは印象深いトピックが書かれていてぜひ開発組織に属する人であれば読んで欲しい内容(あとで5.2.1でも再度出てくるけど)。

要はミスが起きた時に、個人の責とするか、組織の問題とするか、なのだけど、それを「ヒューマンエラーはトラブルの原因だ」と考える文化と、「ヒューマンエラーをシステムのもっと深いところにある問題の兆候」と考える文化という言い方で説明するところが凄くしっくりくる。

とくに現代的な開発手法やツールの発展を考えると、20年同じ手順を秘伝のタレ的な知見に基づいて続けていく(しかもその根拠は既に失われている)という現場は少なく、個人の責を問うにはあまりに学ぶべきことは多いし、変化も激しい。また、人もどんどん入れ替わっていくし、伝統的な先輩から後輩への伝達とも限らない(ある日突然アウトソーシング先が入札でガラっと変わることだってあり得る)。

そう考えると個人を責めてもしょうがなくて、組織で考えていかないとトータルでは良くなって行かないよねって考え方は実に正しいなって。

5章 devopsに対する誤解とアンチパターン

ある程度の開発経験が有る人であれば、5章の「5.1 devopsに対するよくある誤解」と、「5.2 devopsのアンチパターン」から読み始めると良いかなって思った。特に、5.2は一番最初に読んだ方が良い章。この章に対して納得感が有るか無いかで、この本から得られるものが有るか無いかははっきりすると思う。

6章はⅡ部の導入部なので飛ばして…

7章 コラボレーション

ここは完全に組織で仕事をしていく上での普遍的な話にフォーカスしている。

  • 7.3 個人の違いと経歴、背景
  • 7.4 競争優位を得るためのチャンス
  • 7.5 メンターシップ
  • 7.6 マインドセット入門
  • 7.7 マインドセットと学習する組織
  • 7.8 フィードバックの役割
  • 7.9 評価とランキング
  • 7.10 コミュニケーションと対立の解決スタイル
  • 7.11 共感と信頼
  • 7.12 人材配置と人事管理

これ、完全に組織論、人材論になっているってことがよく分かると思う。個人的には「7.6 マインドセット入門」あたりが興味深かったけど、本当にこの章はdevopsに特化した内容ではなく、どうやってより良い組織を作っていくか?という話が続くので、ひょっとしたら人によってはガッカリしたり、意外な発見が有ったりするのでは、と思う箇所。

だからこそ冒頭のツイートに繋がるんだけど、ひょっとして開発プロセスや、ツールの使い方の本だと思ってこの本を読まないとしたら、本当にそれはもったいないって。開発チームのマネジメントを行う人、特にこれからリーダーになるような人はまず読んだ方が良い。5章と7章だけでも良いので読んで欲しい。

以降はチームでの仕事をどうやってより良くしていくか?という話や、(概念的な意味での)ツール導入、devopsをスケールさせる話とかなので、一気に読むより少しずつ自分の状況に合わせて読み進める方が良いと感じた。

おわりに

全般を通して「これがdevopsだ!」というより、一貫して「良い開発組織であり続けるためには?」という話が続く所が良い本。

ただ、上司にdevopsの導入を進言する時には使いづらいかもね:)

300ページ超は、この手の本としてはそこまで大ボリュームではないけど、それでも全部を一気に読み切ろうとすると消化不良を起こしかねないような「考えさせられる」本であることは間違いないので、あまり慌てずゆっくり(できれば周りの人とディスカッションしたりしながら)読んだ方が良さそう。

GraphQLナイトへ参加してきた

たまたま「GraphQL」について調べていたタイミングで開催が決定したのと、最近すっかりpmイベント以外に参加できていないことも有って参加してきた。

connpass.com

GraphQL自体の入門、ScalaRubyでのライブラリなどの紹介など、丁度知りたいレベル感と合っていたので、ぴったりのイベントでした。

あと、今回開催のきっかけが主催の@htomineさんが所属する会社の中でみんながGraphQLについて楽しそうに語っているんだけど、意外と外に出て発表していないからって所も良かった。背中を押す人、環境を作る人ってマジで大事ですね。

確認できた分のスライドへのリンクを張っておきます。そのうち、イベントサイトにもまとめられると思います。

speakerdeck.com

speakerdeck.com

speakerdeck.com

会場提供はfreeeさんでした。

あと、GraphQLのイベントとしてGraphQL Tokyoというミートアップが有るそうです。次回はすぐの7月4日だそうです。

www.meetup.com

GraphQL、複雑化した要求や制約の中で効率よくデータを取得する必要性から生まれたものだそうですが、こうやってベストプラクティスがライブラリや規約の形で世の中に広まっていく感じがいいですね。

イベント最高でした。@htomine++!

Learning GraphQL: Declarative Data Fetching for Modern Web Apps

Learning GraphQL: Declarative Data Fetching for Modern Web Apps

WEB+DB PRESS Vol.104

WEB+DB PRESS Vol.104

  • 作者: 末田卓巳,林田千瑛,陶山嶺,八谷賢,辰己佳祐,竹澤俊季,服部智,藤岡裕吾,牧大輔,西郡卓矢,松木雅幸,穴井宏幸,新日出海,桑原仁雄,小田知央,ひげぽん,池田拓司,はまちや2,竹原,大場光一郎,大場寧子,松館大輝,日高尚美,Vu Xuan Dung,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/04/24
  • メディア: 単行本
  • この商品を含むブログを見る

GraphQL API Design (API-University Series Book 5) (English Edition)

GraphQL API Design (API-University Series Book 5) (English Edition)

java.net.URLConnectionのguessContentTypeFromNameが使うMIME Type設定は実行時に変更することはできない

java.net.URLConnectionのguessContentTypeFromNameは、content.types.user.tableというシステムプロパティで定義された内容で任意のMIME typeを推測できるようになります。

公式ドキュメントにも書かれています。

URLConnection (Java Platform SE 8)

では実際にそれを確かめてみようと、テストコードの中でSystem.setProperty("content.types.user.table", "/path/to/content-types.properties")と指定してもさっぱり有効になりませんでした。

例えば、拡張子.csstext/cssと判定させるためには、以下のような設定ファイルを用意します。

text/css: \
        description=Cascading Style Sheets;\
        file_extensions=.css

以下のようなコードを用意して、設定ファイルを読み込んでみましが、mimeTypeにはnullが入りました。

System.setProperty("content.types.user.table", "/path/to/content-types.properties")
val mimeType = URLConnection.guessContentTypeFromName("test.css")

実行時ではなく、予めsbt -Dcontent.types.user.table=/path/to/content-types.propertiesというふうに実行前に設定されるようにしておくと意図した通りにmimeTypeにはtext/cssが入ります。

java.net.URLConnectionのguessContentTypeFromNameのコードを追いかける

これは何が起きているのでしょうか?

実は最初、Stack OverflowにSystem.setPropertyでセットすれば反映されると書かれていたので、それを鵜呑みにして「正しく動かない!自分の書いたコードがおかしいのか?」と思いましたが、実際にはその回答が誤りでした。

順番に、java.net.URLConnectionguessContentTypeFromNameメソッドの挙動を、openJDK10のコードを例に追いかけていきましょう。

openJDKの該当するソースコードは以下の場所に有ります。

jdk10/master: be620a591379 src/java.base/share/classes/java/net/URLConnection.java

public static String guessContentTypeFromName(String fname) {
        return getFileNameMap().getContentTypeFor(fname);
}

public static FileNameMap getFileNameMap() {
    FileNameMap map = fileNameMap;

    if (map == null) {
        fileNameMap = map = new FileNameMap() {
            private FileNameMap internalMap =
                sun.net.www.MimeTable.loadTable();

            public String getContentTypeFor(String fileName) {
                return internalMap.getContentTypeFor(fileName);
            }
        };
    }

    return map;
}

MIME Typeに関連するデータは、FileNameMapというクラスに格納されていること、値が無ければ(nullならば)されていなければsun.net.www.MimeTable.loadTableを呼び出して、初期化していることが分かります。

なお、getContentTypeForFileNameMapのメソッドで、ほぼ単純なmap構造のデータから該当するMIME typeを取得する機能を提供します。

sun.net.www.MimeTable.loadTableの中身を追いかける

では、実際にMIME typeのテーブルを保持するsun.net.www.MimeTable.loadTableの中身を追いかけてみましょう。

sun.net.www.MimeTable.loadTableは以下のファイルに収録されています。

jdk10/master: be620a591379 src/java.base/share/classes/sun/net/www/MimeTable.java

public static FileNameMap loadTable() {
    MimeTable mt = getDefaultTable();
    return (FileNameMap)mt;
}

public static MimeTable getDefaultTable() {
    return DefaultInstanceHolder.defaultInstance;
}

private static class DefaultInstanceHolder {
    static final MimeTable defaultInstance = getDefaultInstance();

    static MimeTable getDefaultInstance() {
        return java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<MimeTable>() {
            public MimeTable run() {
                MimeTable instance = new MimeTable();
                URLConnection.setFileNameMap(instance);
                return instance;
            }
        });
    }
}

loadTableから始まり、DefaultInstanceHolderクラスにdefaultInstanceというstaticなメンバが有ることが分かります。staticなクラスのstaticなメンバなので、クラス自体がロードされた時点で、getDefaultInstanceメソッドが実行され、defaultInstanceにはMIME typeのデータがロードされている、ということなのです。

更に、この時点でなぜかURLConnection.setFileNameMapを呼び出し、ロードしたMIME Typeをセットしています。getFileNameMapメソッドの中でやっているFileNameMap mapがnullか否かを判定するロジックっていらなくない?って思いますね。

実際にロードする部分のコードをもう少し追いかけてみましょう。

MimeTable() {
    load();
}

public synchronized void load() {
    Properties entries = new Properties();
    File file = null;
    InputStream in;

    // First try to load the user-specific table, if it exists
    String userTablePath = System.getProperty("content.types.user.table");
    if (userTablePath != null && (file = new File(userTablePath)).exists()) {
        try {
            in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            System.err.println("Warning: " + file.getPath()
                                + " mime table not found.");
            return;
        }
    } else {
        in = MimeTable.class.getResourceAsStream("content-types.properties");
        if (in == null)
            throw new InternalError("default mime table not found");
    }

    try (BufferedInputStream bin = new BufferedInputStream(in)) {
        entries.load(bin);
    } catch (IOException e) {
        System.err.println("Warning: " + e.getMessage());
    }
    parse(entries);
}

確かにcontent.types.user.tableというシステムプロパティが設定されていれば、そこからパスを取得するようになっていますね。存在しなければデフォルトのcontent-types.propertiesを取得しています。

ちなみに、このcontent.types.user.tableというシステムプロパティを元に任意のMIME Typeを設定する機能について、テストコードが存在しません。確かにこれではグローバルにシステムプロパティを汚染しないとテストが書けないですね…

おわりに

以上、java.net.URLConnectionguessContentTypeFromNameにおけるcontent.types.user.tableの取扱について、実際のJavaのライブラリのコードを追いかけてみました。

依存関係(sun.net.www.MimeTable.loadTablejava.net.URLConnectionに依存している)がおかしいとか、初回利用時ではなく、クラスロード時に強制的に初期化が行われ、以降は再設定もできないとか、そもそもテストが無いとか、よく考えるとguessContentTypeFromNameというメソッド自体java.net.URLConnectionではなく、独立したMIMEに関するクラスに所属しているべきでは?と、標準のJavaのライブラリでも色々と設計が気になるところが有るんだな、というのが今回の感想でした。