Magnolia Tech

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

Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術

  • 作者:渋川 よしき
  • 発売日: 2020/04/21
  • メディア: 単行本(ソフトカバー)

初版も持っていたけど、コレ1冊あればおおよそ俯瞰できるので、2版も購入。

ちょっと分厚いけど、まずは前半だけでも読めばOKだし、読み切るより、リファレンスとして知りたいことが有る時に、入り口として使った方がいいかも。

あと、Go以外で実装してみるのも良いかも。

Dottyではimportするシンボルをコントロールするgivenという記法がサポートされる

dotty.epfl.ch

import A.{given _}と書くと、パッケージAの中でgiven tc as TCと指定した内容がインポートされます。

PerlのExporterモジュールで@EXPORTにエキスポートしたい関数を指定するみたいな感じですね。

pointoht.ti-da.net

単にPerlScalaを並べてみたかっただけです

Scalaにおけるライブラリの衝突

github.com

このPR、単純な依存ライブラリのアップデートのはずがScala 2.11だけテストが失敗している。

エラーメッセージを見ると、サーバ側がstatus code500を返して、テストが失敗していることが分かる。

[info] [info] MyScalatraServletTests:
[info] [info] - GET / on MyScalatraServlet should return status 200 *** FAILED ***
[info] [info]   500 did not equal 200 (MyScalatraServletTests.scala:11)
[info] [info] Run completed in 2 seconds, 18 milliseconds.

しかし、テスト結果からはアプリケーション内で何が失敗して500になっているのか分からないので、同じコードをデバッガで実行しながら確認することにした。そうすると下記の例外が発生していることを突き止めた。

Failure(java.lang.AbstractMethodError: Receiver class org.scalatra.SinatraPathPatternParser does not define or inherit an implementation of the resolved method 'abstract scala.util.DynamicVariable scala$util$parsing$combinator$Parsers$$lastNoSuccessVar()' of interface scala.util.parsing.combinator.Parsers.)

scala-parser-combinatorsのlastNoSuccessVarというメソッドが無くなっているそうだ。確かに、以下のコミットで削除されている。

github.com

このメソッドが削除されたのはscala-parser-combinatorsの1.1.2というバージョンからだ。そしてこの変更によりScala 2.11での非互換が発生していることも既に分かっている。

github.com

そのため、scala-parser-combinatorsを使うときは、ターゲットとするScalaのバージョンに合わせて以下のように、バージョンを切り替える方法が取られる(面倒くさい…)。

  private val parserCombinatorVersion = Def.setting(
    CrossVersion.partialVersion(scalaVersion.value) match {
      case Some((2, 11)) =>
        // https://github.com/scala/scala-parser-combinators/issues/197
        "1.1.1"
      case _ =>
        "1.1.2"
    }
  )

確かにこうやって互換性の有るバージョンが指定されているはずなのに…何故か非互換のある1.1.2が使われているのと同じエラーが出ている。

ほかにscala-parser-combinatorsを使っているライブラリが無いか調べてみると、テンプレートエンジンであるTwirlが使っていることが分かった。

github.com

しかも一律1.1.2を指定している。こっちがクラスパスに存在するためだ。

github.com

しかし、Twirlはscala-parser-combinatorsの中でも非互換が発生するパッケージ(scala.util.parsing.combinator)は使用しておらず(scala.util.parsing.inputのみ利用)、Scala 2.11をサポートしているにも関わらず非互換によるエラーが発生しないのだ。


原因は分かった…あとは解決策

Twirl側に読み込むライブラリを分岐させるロジックを入れる方法も有るが、それほど前向きな話でもない…そもそもScalatra.g8は新規のプロジェクトを作るときのテンプレートライブラリだ。これからScalatraの新規プロジェクトが2.11で始められる可能性は低い(アップデートは有ると思っている)。

というわけで、ここはScalatra.g8からScala 2.11のテストを落とす、というのが妥当である(サポートしない)、という結論に至った。

ScalatraのTwirlサポートは後から入った機能なので、Scala 2.11ベースで動かしている人たちは、今すぐ問題になることは無いと思われるのでScalatra本体のScala 2.11サポートを今すぐ落とす必要は無いけど、やはり古いバージョンはどこかで切る必要が出てくると思う。

このようにアドバイスも頂いた。


ライブラリの非互換性の問題が分かりづらい形で出てくるときも有るね、という話でした。

Spay-JsonがJSON ASTからScalaオブジェクトを生成する仕組み

調べたことを雑に残すシリーズです。

github.com

Spray-Jsonの基本的な使い方は以下の通りです。

case class Color(name: String, red: Int, green: Int, blue: Int)

object MyJsonProtocol extends DefaultJsonProtocol {
  implicit val colorFormat = jsonFormat4(Color)
}

import MyJsonProtocol._
import spray.json._

val json = Color("CadetBlue", 95, 158, 160).toJson
val color = json.convertTo[Color]
  • case classであれば個別に自分で変換のためのコードを書く必要は無い
  • ただし、case classのフィールドの個数を、jsonFormatNというメソッドで指定する

フィールドの個数だけは指定させているのはなぜでしょう?

jsonFormatNメソッド

jsonFormatNメソッドは以下のソースコードで定義されています。

https://github.com/spray/spray-json/blob/release/1.3.x/src/main/boilerplate/spray/json/ProductFormatsInstances.scala.template

ただし、これはテンプレートであり、実際のコードはsbt-boilerplateというpluginで生成されるようになっています。Scalaでおなじみの引数が1〜22個までの同じ挙動のメソッドを自動で生成するための仕組みになっています。

github.com

一度コンパイルすると、以下の場所にソースが生成されます。

spray-json/target/scala-2.12/src_managed/main/spray/json/ProductFormatsInstances.scala

例えば引数を二つ取るjsonFormat2は以下のようなコードに展開されます。

// Case classes with 2 parameters

  def jsonFormat2[P1 :JF, P2 :JF, T <: Product :ClassTag](construct: (P1, P2) => T): RootJsonFormat[T] = {
    val Array(p1, p2) = extractFieldNames(classTag[T])
    jsonFormat(construct, p1, p2)
  }
  def jsonFormat[P1 :JF, P2 :JF, T <: Product](construct: (P1, P2) => T, fieldName1: String, fieldName2: String): RootJsonFormat[T] = new RootJsonFormat[T]{
    def write(p: T) = {
      val fields = new collection.mutable.ListBuffer[(String, JsValue)]
      fields.sizeHint(2 * 3)
      fields ++= productElement2Field[P1](fieldName1, p, 0)
      fields ++= productElement2Field[P2](fieldName2, p, 1)
      JsObject(fields.toSeq: _*)
    }
    def read(value: JsValue) = {
      val p1V = fromField[P1](value, fieldName1)
      val p2V = fromField[P2](value, fieldName2)
      construct(p1V, p2V)
    }
  }

先ほどの implicit val colorFormat = jsonFormat4(Color)は、オブジェクトを渡していますが、以下の仕組みにより、実際にはColor.applyが渡されているのと同等です。

xuwei-k.hatenablog.com

つまり、そのcase classのファクトリメソッドが渡されているわけです。

writeJSON astを生成するメソッドなので、ここで必要になってくるのはreadの方です。

JSON astからScalaオブジェクトを生成する

先ほどのreadメソッドの中を一つ一つ見ていきましょう。

二つのフィールドをJSON astが格納されているvalueからfromFieldメソッドを使って取り出しているようですね。続いてfromFieldメソッドの中身を見ていきましょう。

fromFieldメソッドは、src/main/scala/spray/json/ProductFormats.scalaで以下のように定義されています。

protected def fromField[T](value: JsValue, fieldName: String)
                                     (implicit reader: JsonReader[T]) = value match {
    case x: JsObject if
      (reader.isInstanceOf[OptionFormat[_]] &
        !x.fields.contains(fieldName)) =>
      None.asInstanceOf[T]
    case x: JsObject =>
      try reader.read(x.fields(fieldName))
      catch {
        case e: NoSuchElementException =>
          deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil)
        case DeserializationException(msg, cause, fieldNames) =>
          deserializationError(msg, cause, fieldName :: fieldNames)
      }
    case _ => deserializationError("Object expected in field '" + fieldName + "'", fieldNames = fieldName :: Nil)
  }

ちょっと複雑で分かりづらいですが…

  • 一つ目は、そのフィールドがオプションでかつ値を持たなければNoneを返しています
  • 二つ目は、JsObjectから指定されたフィールド名で値を取り出しています。
  • そのどちらにも当てはまらない場合は、例外発生です。

元々、case classの引数を取得しているはずなので、JsObjectとして値が取れないと例外なのは妥当ですね。

readは取り出す値の型ごとに用意されていて、例えばStringとして値を取得したい場合は以下のメソッドが呼ばれます。

 implicit object StringJsonFormat extends JsonFormat[String] {
    def write(x: String) = {
      require(x ne null)
      JsString(x)
    }
    def read(value: JsValue) = value match {
      case JsString(x) => x
      case x => deserializationError("Expected String as JsString, but got " + x)
    }
  }

JsStringであれば、値が取り出され、それ以外は例外になることが分かります。

基本的な型については、src/main/scala/spray/json/BasicFormats.scalaで定義されています。

こうやって値を一つ一つ生成していって、最終的にはScalaのcase classからのオブジェクトがapplyメソッド(相当)で生成されます。

ランタイムリフレクションを使って、メソッドの引数と型を一つ一つ特定する方法もありますが、こちらの方がシンプルですね。

ということで、Spray-JSONが引数の個数を指定させるのは、case classのオブジェクトが型情報を引き回してくれることを利用して型を特定する方法を使うため、ということが分かりました。

Meraki Goを導入したらApple Musicへつながらなくなったが、ルータのIPv6の設定を有効にしたらつながった

タイトルで全部言い切っているんだけど、先日Meraki Goを導入したらApple Musicにつながらなくなったので、その解決までの道のり。

blog.magnolia.tech

普通のウェブサイトへの接続とか、特に動作がおかしいとかもなく普通に使っていたんだけど、よく見るとApple Musicが反応しなくなっている。

別のアクセスポイント経由では普通につながる。

Meraki GoではブリッジモードとNATモードが有り、最初NATモードになっていたのでリブートしてブリッジモードに変更したことが原因かな?と思ったけど、ちょっと違う。

[https://documentation.meraki.com/Go/Meraki_Go-When_Bridge_Mode_is_not_Available_(Auto_NAT)/jp:embed:cite]

macOSのネットワーク設定で変えられるところと言えばIP v6くらい。試しにLocal-Link mode(LANの外にIP v6を使わない)に変更してみると…繋がる!

しかし、iOSには同様の設定は無い…

一瞬Meraki GoがIPv6を上手く扱えないのかと疑ったけど(Meraki GoのアプリにはIPv4のアドレスしか表示されないので)、さすがにドキュメントを読むとフルサポートと書かれている。

普段全然アクセスしないフレッツのルーターにログインしてみると、IPv6のPPoEが切断状態になっている!というか、IPv6のPPoEの設定ができたんだ…

これを有効にしたところ、Apple Musicにつながるようになった。

しかし、以前のWi-Fiルータもブリッジモードでしか使っていなかったのに、この差はいったいなんだったんだろう…

Scalaのコンストラクタのフィールドを取得する

コンストラクタのフィールドを得る方法について

import scala.reflect.runtime.{universe => ru}
import ru._

class Person(val name: String, age: Int, blah: List[Int])

object ReflectionTest {
  def main(args: Array[String]): Unit = {

    def generate[T](implicit tag: TypeTag[T]): Unit = {

      val typeSymbol          = typeOf[T].typeSymbol            // 型のSymbolを得る
      val constructorSymbol = typeSymbol.asClass.typeSignature.decl(termNames.CONSTRUCTOR).asMethod // コンストラクタ

      val list = constructorSymbol.paramLists(0)  // Listが空の時は、def test = {}みたいな定義が行われているとき

      // https://stackoverflow.com/questions/27473440/scala-get-constructor-parameters-at-runtime
      val params = list.reverse.foldRight(Map(): Map[String, ru.Type])((p, a) => {
        a + (p.name.decodedName.toString -> p.typeSignature)
      })

      params.foreach { p => println(p) }
    }

    generate[Person]
  }
}

コンストラクタメソッドからオブジェクトを動的に生成する

一つ前のエントリの続き

次はcase classではなく、普通のクラスを生成する

import scala.reflect.runtime.{universe => ru}
import ru._

class Person(val name: String)

object ReflectionTest {
  def main(args: Array[String]): Unit = {


    def generate[T](implicit tag: TypeTag[T]): T = {

      val typeSymbol          = typeOf[T].typeSymbol            // 型のSymbolを得る
      val constructorSymbol = typeSymbol.asClass.typeSignature.decl(termNames.CONSTRUCTOR).asMethod // コンストラクタ

      val mirror = scala.reflect.runtime.currentMirror            // 現在のミラーを取得する
      val cm = mirror.reflectClass(typeSymbol.asClass)            // Class Mirrorを得る

      val constrcutorMirror = cm.reflectConstructor(constructorSymbol)

      constrcutorMirror("John").asInstanceOf[T]  // 実行する
    }

    println(generate[Person].name) // 生成したオブジェクトが得られる
  }
}