Magnolia Tech

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

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) // 生成したオブジェクトが得られる
  }
}

Scalaでリフレクションを使って、与えられた型情報から動的にオブジェクトを生成する

Scalaでも動的にオブジェクトを生成したいですね、そうですね。

早速リフレクションを使って動的にオブジェクトを生成します。

以下、覚え書き…もう少し効率よくできる気がする。

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

case class Person(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 companionSymbol     = typeSymbol.companion            // コンパニオンオブジェクトのsymbolが取れる
      val companionType       = companionSymbol.typeSignature   // symbolからコンパニオンオブジェクトのTypeが取れる
      val applyMethodSymbol   = companionType.decl(TermName("apply")).asMethod  // applyメソッドのmethodSymbolを得る

      val mirror = scala.reflect.runtime.currentMirror            // 現在のミラーを取得する

      val mm = mirror.reflectModule(companionSymbol.asModule)     // オブジェクトのmodule mirrorを取得する
      val im = mirror.reflect(mm.instance)                        // module mirrorのインスタンスからinstance mirrorを取得する
      val methodMirror = im.reflectMethod(applyMethodSymbol)      // instance mirrorからmethod mirrorを取得する

      methodMirror("John").asInstanceOf[T]  // 実行する…applyメソッドなので、戻り値はAnyに入ったオブジェクト…型を指定して返す
    }

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

Cisco Meraki Goを買った

普段は2万円くらいのCisco Meraki Goがタイムセールで16,400円になっていたのと、いろいろとポイントが溜まっていたので買ってみた。

f:id:magnoliak:20200328142820j:plain

Meraki Goは、Ciscoのスモールビジネス向けのWi-Fiアクセスポイント。カフェみたいなところでのWi-Fiサービスとか、SOHOとかでの導入を想定した製品っぽい。

www.cisco.com

リセラーが限定されてて、Amazon経由でしか買えないところが従来のCisco製品と違うところ。また発売当初はサブスクリプションモデルになっていて毎年費用が発生するビジネスモデルだったけど、いつの間にかそれもなくなって、ハードウェアを買えばずっと使えるように変わっていた。

というわけでCisco製品としては珍しく(?)個人ユースでも全然問題なし。CiscoWi-Fiアクセスポイント、デザインがいいので個人でも欲しいけどコントローラが別途必要だったり、そもそもとても高価だったり、ライセンス購入が必要だったりとビジネスユース向けの提供形態なのでとても手が出なかったけど、このMeraki Goはその辺のハードルの高さを全部排除しているのがCisco製品としては新しい試み。

WiFiアクセスポイントとしては、以下のようなスペックで、凄く速いわけでもないけど、手元のMacBook Proがアンテナ2本しかないモデルなので特に不満は無し。Touch Barモデルのようにアンテナ3本のモデルの場合は要注意。

WiFiアクセスポイントにルータ機能いらなくない?ってずっと思ってたので、ちょうど良い機能感。

マニュアルもわかりやすくて、設定用のアプリを見ながらやればOK。

f:id:magnoliak:20200328142852j:plain f:id:magnoliak:20200328142913j:plain f:id:magnoliak:20200328143339p:plain

PoEにも対応しているし、壁掛けにも対応しているので、割と置き場に困らない。しかも壁掛けに必要なパネルもネジも付属しているオールインワン型。

f:id:magnoliak:20200328143028j:plain

f:id:magnoliak:20200328143015j:plain

ネットワークにつないで、スマホの専用アプリからネットワーク名とパスワードを設定すれば、すぐにつながる。転送量などの利用状況もアプリから確認できる。

f:id:magnoliak:20200328143930p:plain

最近の家庭用Wi-Fiアクセスポイント、どれもごっついアンテナがついてて「ちょっとねー」っていうデザインなんだけど、Meraki GoはとにかくデザインがちゃんとしていてPoEも対応しているし、壁掛けもできる自由度が有る点が良い。

値段はもう少し安いといいけど

上記のモデルは屋内利用専用だけど、屋外利用モデルも有る。これはさすがに個人ユースでは使わない気もするけど。

2020/3/30 追記

LAN上に有るはずのNASが見えなくなったので、アレ?と思ってたらMeraki GoがNATモードになってて独自のIPアドレス体系になっていたことが判明。デフォルトはブリッジモードのはずなのに。

以下の記事を参考にリセットしたら直りました。ちょっと分かりづらかったかも。

nwengblog.com

Scalaでapplyメソッドを引数に期待しているところにコンパニオンオブジェクトをそのまま渡しても良い

Spray-JSONのソースを読んでて、まぁそりゃそうかと思ったけど、こう書けるのかって改めて思ったので残しておきます。

scala> case class Person(name: String, age: Int)
defined class Person
scala> def test(constructor: (String, Int) => Person): Person = {
     | constructor("John", 42)
     | }
test: (constructor: (String, Int) => Person)Person

scala> test(Person)
res0: Person = Person(John,42)

scala> test(Person.apply)
res1: Person = Person(John,42)

Json4sのJSON ASTからScalaのオブジェクトを生成する

次は、Json4sのJSON ASTからScalaのオブジェクトを生成する方法について解説。

Play-JSONでのScalaオブジェクト生成

Json4sのオブジェクト生成の前に、Play-JSONにおけるオブジェクト生成方法について見てみます。

import play.api.libs.json._
import play.api.libs.json.Reads._

case class Person(name: String, age: Int)

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

    implicit val PersonFormat = Json.format[Person]

    val source = """{ "name": "John", "age": 42 }"""
    val jsonAst = Json.parse(source)

    val result = jsonAst.validate[Person]

    result match {
      case s: JsSuccess[Person] => {
        val person: Person = s.get
        println(person)
      }
      case e: JsError => println(e) # prints 'Person(John,42)'
    }
  }
}

ポイントは、implicit val PersonFormat = Json.format[Person]という指定です。Play-JSONでは、case classのオブジェクトを生成するために、このような指定が必要になります。

Json4sでのScalaオブジェクト生成

では同じようにJson4sでオブジェクトを生成してみる。

import org.json4s._
import org.json4s.jackson.JsonMethods._

case class Person(name: String, age: Int)

object Main {
  def main(args: Array[String]): Unit = {
    implicit val formats = DefaultFormats

    val source = """{ "name": "John", "age": 42 }"""
    val jsonAst = parse(source)

    val result = jsonAst.extract[Person]

    println(result) # prints 'Person(John,42)'
  }
}

少し形式を変えて、case classではなく、通常のクラスとして定義してみます。

import org.json4s._
import org.json4s.jackson.JsonMethods._

class Person(val name: String, val age: Int)

object Main {
  def main(args: Array[String]): Unit = {
    implicit val formats = DefaultFormats

    val source = """{ "name": "John", "age": 42 }"""
    val jsonAst = parse(source)

    val result = jsonAst.extract[Person]

    println(result.name) # prints "John"
    println(result.age) # prints "42"
  }
}

unapplyメソッドが無いので、個別に出力していますが、case classでなくてもオブジェクトが得られていることが分かると思います。

一方で、Play-JSONでは通常のクラスに変更するとコンパイルエラーになります。No apply function found for example.Personと、No Json deserializer found for type example.Person. Try to implement an implicit Reads or Format for this type.というエラーが出ます。

これは他のScala用のJSONライブラリでも同様で、case classの場合は、コンパニオンオブジェクトを渡したり、予め利用するcase classの型を登録しておいたり、通常のクラスであれば、自前で変換方法を詳細に定義する必要があります。

しかし、Json4sではどれも必要ありません。

なぜでしょうか?

というところで次回に続きます。

Json4sの使い方

前回のエントリからの続き。今回はざっとJson4sの使い方をおさらいする。

JSON文字列のパースと、生成

Json4sはバックエンドとしてJavaJSONライブラリであるJacksonを使うjacksonパッケージと、lift-json由来のパーサーを提供するnativeパッケージの2種類が用意されていて、どちらを使うかによってimportするパッケージが変わってくる。

例えばjacksonパッケージを使ってJSONをパースし、指定したデータ型にマッピングするときは、以下のようなコードとなる。

import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.read

implicit val formats = Serialization.formats(NoTypeHints)

case class Person(name: String, age: Int)

val p = read[Person]("""{"name": "John", "age": 42 }""") # -> Person(John,42)

逆に、ScalaのデータからJSON文字列を生成する場合は、以下のようなコードとなる。

import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write

implicit val formats = Serialization.formats(NoTypeHints)

case class Person(name: String, age: Int)

val p = Person("John", 42)
val j = write(p) # -> {"name":"John","age":42}

上記の例ではjacksonパッケージを使ったが、nativeパッケージでも同様に動作する。

ただし、Jacksonのパーサーは頻繁にメンテナンスが行われているが、Json4sのパッケージ内でメンテナンスされているnative版パーサーは現在では決してきちんとメンテナンスされているとは言いがたく、不正なJSON文字列でもパースできてしまったり、RFC8259で定義されているobjectやarray以外のトップレベルのデータを正しくパースできないといった問題が有り、今ではJackson版を使っておく方が安全である。

JSON ASTを経由する

先ほどの例では直接JSON文字列をScalaのcase classにマッピングしたが、内部では一回JSON ASTを経由している。

次はJSON ASTを明示的に生成する方法を紹介する。

JSON文字列からJSON ASTを生成するためにはparseメソッドを使用する。

import org.json4s._
import org.json4s.jackson.JsonMethods._

val pa = parse("""{"name":"John","age":42}""") # -> JObject(List((name,JString(John)), (age,JInt(42))))

反対に、JSON ASTからJSON文字列を生成するためには、prettyメソッドか、compactメソッドを利用する。

val p = pretty(pa)

# {
#   "name" : "John",
#   "age" : 42
# }

val c = compact(pa)
# {"name":"John","age":42}

JSON ASTを経由する理由は、Json4sにはASTを直接操作するための豊富なメソッド(filterやmerge、query構文)が用意されていて、JSONをそのままScalaのデータ型にマッピングする前に一定の前処理をしておきたい、といった場合に有効である。詳しくはREADMEを参照のこと。

元々Json4sはScalaJSONライブラリに共通的に利用可能はASTを提供するために作られた経緯があり、豊富なASTレベルでの操作メソッドが用意されている。

おわりに

Json4sが用意するJSON ASTを操作するためのメソッド群は非常にたくさんの種類が用意されていますが、冒頭の例のように直接Scalaのデータ型にマッピングした方が扱いやすいと思いますので、まずはJSONScalaのデータ型を変換するorg.json4s.jackson.Serializationパッケージの使い方を覚えれば十分です。

次回は、JSON ASTからScalaのデータ型へ変換する方法について解説します。Json4sがランタイムリフレクションを使いまくっている様子をご紹介します。