Magnolia Tech

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

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がランタイムリフレクションを使いまくっている様子をご紹介します。

Json4sについて、その機能と内部構造

Json4sの使い方と、内部構造についてのいくつかのこと

年明けからJson4sのメンテナーをやっている。

Json4sは、2012年に作られた歴史あるScala用のJSONライブラリで、Apache Sparkで使われていることもあり、割と広く使われている。しかし、ランタイムリフレクションをフルに使った(使いすぎた?)その非常に複雑な構造が足かせとなって、エッジケースの改修が全然追いついていない。そのため、今ではこんな評価となっている(このツイート自体ですら、4年も前…)。

とはいえ、まだまだ使われているし、古いissueが解決されないままになっているのも良くない。

元々、ScalatraのSwaggerライブラリのコードがJson4sベースだったということもあり、それなりにコードを読み込んでいたので、年末年始のまとまった時間が取れたタイミングで放置されてたissueにコメントしたり、PR作ったりしていたら、メンテナに入れてもらった。

あらためて、JSONについて

一度JSONについてあらためて復習する。


JSON(JavaScript Object Notation)の仕様は、RFC8259で定義されている。

JavaScript Object Notation (JSON) is a text format for the serialization of structured data. It is derived from the object literals of JavaScript, as defined in the ECMAScript Programming Language Standard, Third Edition [ECMA-262].

冒頭にこのように書かれている通り、JavaScriptに由来するデータ構造をシリアライズし、テキストで表現するための規格である。

また、同じ仕様がECMA-404としても規格化されている。

改めて最新のJSONの仕様を見てみると、エンコーディングは必ずUTF-8と決められ、トップレベルのデータ型がobjectやarrayに限定されない(現在では、nullや、"foo"だけでも有効なJSON)など、初期の頃から意外と変わっていることが分かる。

JSONのデータ構造

簡単におさらいすると、JSONには、object(辞書/連想配列), array(配列), string(文字列), number(数値), boolean(true or false), nullの6種類のデータ型が用意されている。

また、objectやarrayは複数のデータ型を混在して保持でき、かつobejctやarrayは任意の深さでネストできる。

例えば下記のようなデータ構造が作れる(objectの中に、arrayと真偽値の"true"と”null”が混在している)。

{
  "foo" : [ "fizz", "buzz", 42, {
    "wizard" : "Gandalf"
  } ],
  "bar" : true,
  "baz" : null
}

この”複数のデータ型が混在できる”、”任意の深さでobjectとarrayをネストできる"という特徴がScalaJSONを扱うときのポイントとなってくる。

各言語の組み込みのデータ構造や、コレクションクラスがサポートするデータ構造は当然JavaScriptと完全に互換性が有るとは限らない。つまり、JavaScriptの言語仕様をベースに作られたJSONを読み込むためにはJSONが表現するデータ構造を再現できる機能を作り込む必要がある。

PerlにおけるJSON

Json4sの解説に入る前に、JavaScriptと同様の動的型付言語であるPerlにおけるJSONの扱いを見てみる。

Perlのコアライブラリに含まれているJSONライブラリJSON::PPのコードを紹介する。このコードはJSON文字列をパースするコードの一部(余計なところは削除している)だが、文字列が'{'だったらobjectのはじまり、'['だったら配列の始まりと認識して、それぞれobjectをパースする関数、arrayをパースする関数がコールされていることが分かる。

sub value {
    return          if(!defined $ch);
    return object() if($ch eq '{');
    return array()  if($ch eq '[');
    return string() if($ch eq '"');
    return number() if($ch =~ /[0-9]/ or $ch eq '-');
}

実際に配列をパースするコードは以下のようになっていて、最初のmy $a = $_[0] || [];がポイント。引数無しで呼び出された時は、空の配列へのリファレンスを生成していることが分かる。

    sub array {
        my $a  = $_[0] || []; # you can use this code to use another array ref object.

        if(defined $ch and $ch eq ']'){
            --$depth;
            next_chr();
            return $a;
        }
        else {
            while(defined($ch)){
                push @$a, value();

                if($ch eq ']'){
                    --$depth;
                    next_chr();
                    return $a;
                }

その後、すぐに配列が']'で閉じられていれば、そのリファレンスを返却しているし、実際のデータが有ればそれを配列に格納(push)して、その配列(リファレンス)を返却している(next_chrは処理対象の文字を1文字ずつ進める関数、valueは続くデータをパースする関数)。

Perlは動的型付言語であり、組み込みのデータ構造としてハッシュ(JavaScriptのobjectに相当)と、配列(JavaScriptのarrayに相当)を持ちハッシュや、配列には複数のデータ型の混在が許される。また、リファレンスというデータ構造により、任意の深さでハッシュと配列とネストできる。

JSON::PPは、JSONのデータを以下のようにPerlのデータ型へ変換する。

JSONのデータ型 対応するPerlのデータ型
object ハッシュのリファレンス
array 配列のリファレンス
string 文字列
number 数値
boolean JSON::PP::Boolean
null undef

Perlには組み込みの真偽値型が存在しないので、if文の中で真偽として使えるJSON::PP::Booleanというオブジェクトで代替しているが、その他は比較的素直な対応となっている。

特にハッシュリファレンスと、配列リファレンスによりJSONのobjectとarrayがそのまま表現できているところがポイントとなる。

冒頭のJSONデータをJSON::PPで変換すると、以下のようなPerlのデータに変換される。JSONと同じように、複数のデータ型が混在していること、配列の中にハッシュが含まれていることが分かる。

$ref = {
          'baz' => undef,
          'foo' => [
                     'fizz',
                     'buzz',
                     42,
                     {
                       'wizard' => 'Gandalf'
                     }
                   ],
          'bar' => bless( do{\(my $o = 1)}, 'JSON::PP::Boolean' )
        };

通常のPerlのリファレンスなので、そのままPerlのリファレンスアクセスの構文で値を取り出すことができる。

print $ref->{'foo'}[2]; # prints 42

上記のことから、JSONPerlのデータ構造に移し替えるのは比較的容易であることが分かってもらえたと思う。

ScalaにおけるJSON

JavaScriptと同様に、組み込み型としてobjectやarrayに相当するデータ型を持ち、かつ動的にネストした構造が作れるPerlの場合、割と素直にJSONを言語組み込みのデータ形式に変換できた(真偽値だけは、固有のオブジェクトを利用したが)。

一方でScalaは静的型付言語であるため、事前に型は一つに決まっている必要がある。JSONを読み取りながら、そのデータの内容に応じて動的にネストしたobjectやarrayに相当するデータ構造を汎用的に作り出すことができない。

例えば、arrayを標準コレクションクラスに用意されているList[Int]マッピングすると、Int型のデータしか保持できない。おなじようにobjectをMap[String]マッピングすると、String型のデータしか保持できない。

Scalaに予め用意されているデータ型だけを使って混在させることは、できない。

もちろんあらかじめデータ構造を決め打ちにすればできるが、それでは汎用性が失われる。また、全部Any型にしてしまえばできなくはないが、それでは型情報が失われ、Scalaのメリットが失われてしまう。

つまり、Scalaの標準のコレクション型ではJSONが前提とするデータ構造を素直にマッピングすることはできない。

では、どうするか?

上記のListMapはあくまでScalaの標準コレクションライブラリが提供するクラスであり、そのクラスが表現できないデータ構造が必要であれば、作れば良い。

Json4sは、ScalaのクラスとしてJSONの持つ情報量を保持したままScalaであつかえるようなデータ構造、AST(Abstract Syntax Tree)を提供している。

Json4sが提供するASTと、JSONとの対応は以下の通りとなる。

JSONのデータ型 対応するJson4sのデータ型
null JNull
string JString
number JNumber
boolean JBool
array JArray
object JObject
- JNothing

最後に追加されているJNothingは、OptionのNone相当であり、JSON文字列のパースに失敗した時などにJson4sが返却するデータ型。

また、実際の定義を抜粋すると、以下のようになる。特に、JObjectやJArrayがどうやって元のJSON情報を保持しているか、よく分かると思う。

object JsonAST {
  sealed abstract class JValue extends Diff.Diffable with Product with Serializable

  case object JNothing extends JValue
  case object JNull extends JValue
  case class JString(s: String) extends JValue
  
  trait JNumber
  case class JDouble(num: Double) extends JValue with JNumber
  case class JDecimal(num: BigDecimal) extends JValue with JNumber
  case class JLong(num: Long) extends JValue with JNumber
  case class JInt(num: BigInt) extends JValue with JNumber {
  case class JBool(value: Boolean) extends JValue {

  type JField = (String, JValue)
  case class JObject(obj: List[JField]) extends JValue
  case class JArray(arr: List[JValue]) extends JValue
}

Scalatra 2.7がリリースされた

自分がメンテナンスに参加しているScala用のSinatra-likeなWeb Application Framework、Scalatraの最新バージョンがリリースされた。

scalatra.org

今回のリリースの最大の特徴は、Scala 2.13対応だけど、それ以外にも古くて使われていない機能をバシバシ削除してメンテナンス性を向上させています。

詳しくはリリースノートを見てください。

自分でも覚えられないくらいPRを出したので、もし問題が有ったら自分が書いたコードの部分かもしれないので、問題を見つけたらGitHubにissueを上げてください。

ちゃんとフォローします!

Scalatra in Action

Scalatra in Action

「教養としてのコンピューターサイエンス講義 」を読んだ

日常的にコンピュータを手足のように扱い、コードを書いて、やりたいことを実現している人たちからすれば当たり前のことが、一般の人たちからすれば全然当たり前のことではなかったりすることが、まれによくある。

ブライアン・カーニハンの「教養としてのコンピューターサイエンス講義 」は、一般人向け「コンピューターサイエンス」の講義の内容が書籍化されたもので、コンピュータを取り巻くさまざまなことが「なぜそうなったか?」「どうしてそうあるべきなのか?」という視点と、豊富な歴史的経緯や周辺情報と共に、圧倒的な情報量で語られる。

ハードウェア、ソフトウェア、そしてプログラミング…ネットワークや、インターネット、検索エンジン、セキュリティなどなど、とにかくなにげなく使っているコンピュータの、それを支える仕組みがざっとわかるようになっていて、一般の人だけでなく、コードを書くような人たちも、自分の知識のベンチマークとしてもちょうど良い構成になっている。

自分の知識がどれだけちゃんと背景をふまえたものか、どれだけの網羅性を持っているかを確認するためにも、あるコンピュータの概念を一般の人に説明するときにどこからどのくらいの粒度で説明すればいいのかを理解するためにもちょうど良い一冊です。

ちなみに、60ページの猫の写真が最高なので、それだけでもぜひ見てみてください。


2022/4に第2版が出版されたので、リンク先を更新...ちょうど読み返そうと思っていたところだったので、書い直そうかな