パッケージの依存を単方向にするための拡張メソッド
DDDの話をするとしよう*1。
IDDD 第9章 モジュールにおいて、モジュール(Scalaだとパッケージ)の依存は極力減らせ。現実的には難しいが、単方向にしたり、親子関係にあるならば双方向を許可する、ということが述べられている。
しかしこれめっちゃ難しい。集約が別集約のファクトリになる場合、単方向という制限はだいたい破綻する。
// userモジュールの集約 class User(id: UserId) { def createTask(): Task = // Taskを返すため、taskモジュールに依存 new Task(TaskId.generate(), id) } // taskモジュールの集約 class Task(id: TaskId, ownerId: UserId) // ownerIdとしてUserIdを持つため、userモジュールに依存
そもそもこういった関係のものにファクトリを持たせるな、という話な気もしてくるが、だからといって避けては通れないときもあると思う。ならば親子関係にすれば良いのか、というとそうでもない。そうしちゃうとほとんどがuserモジュールの子になってしまう。たぶん。
そういうときに拡張メソッドを使うことも出来るな、という話をしたいわけです。
さきほどの例だと user.User
をこうする。
class User(id: UserId)
要はファクトリを削る。これでtaskモジュールへの依存はなくなる。
で、taskモジュールに以下のようなのを書く。
import user._ package object task { implicit class TaskCreator(user: User) { def createTask(): Task = new Task(TaskId.generate(), user.id) } }
user.User
から task.TaskCreator
への変換を定義し、そこに拡張メソッドとしてファクトリを持たせてあげる。こうするとファクトリのある場所がtaskモジュールに移動するので、依存が単方向に収まる。
ちなみに使うときは:
import user._ import task._ val user = createUser() val task = user.createTask() // import task._ をしていないとcreateTaskメソッドが使えない
のように、両方のモジュールをimportしてあげる必要がある。が、むしろこれはtaskモジュール中のオブジェクトを使いますよ、という意味になるので、むしろ好都合だと思ってる。
実践で用いてはいないのだけど、「あれ、こうすればいいんじゃ?」と思った話でした。デメリットは実践してないから知らない。
Y8 2017 spring in Shibuyaでお話した #y8spring
久し振りに登壇しました。
内容はメディロムで開発しているウェブAPI*1を、どんな風にScalaで書いているか、どんな判断を下しているか、などといったものです。今の仕組みもそろそろこなれてきたし、次(イベントソーシング、CQRSとかな!)に行く前にいっちょまとめとくか、的なノリです。
登壇自体は午後からでしたが、午前の開会式から居たので、すべてのトークを聞いていたのですが、ベストトーカー賞でもあったid:motchangさんのお話は過去似たようなことをやった身としては気が気じゃない感じで、おもしろつらくて楽しかったですね。他の方のトークも学びや再確認できることがあって、楽しかったなーーー。
こんな楽しい機会を企画/運営してくれたスタッフのみなさん、会場を提供してくれた方々にありがとうのお気持ちをですね、お伝えします。また参加したいですねー。
ScalaのJSONライブラリcirceの使い方
ScalaのJSONライブラリと言うと、json4sあたりが有名なのかと思います(私感)。
が、json4sはリフレクションを使うので、何かと避けたい方も多いかと思う。
ということで、circeを使ってみましょう。
circe概要
読み方
公式サイトに書かれています。
たしかにサーシーと読めるが、circeで調べるとWikipediaにヒットするので、キルケー呼びを推したい所存。通じれば何でもいいと思う。
特徴
元々はArgonautをforkして作られたものなので、それに似た特徴を持つ。
- 純粋関数型のライブラリ(リフレクションを使っていない)
- catsを内部的に使用
- shepelessによるJSONオブジェクト ⇔ データ型の相互変換
- Scala.js対応
- Argonautにある複雑なオペレーター(
--\
とか)はすべて削除されている
各モジュールの用途
circeはモジュールに分かれている。よく使うモジュールはcirce-core, circe-parser, circe-genericの3つ。
circe-core
コアなクラス(Json, Encoder, Decoder, etc…)が含まれている。よく使うパッケージはio.circeとio.circe.syntaxのふたつ。
circe-parser
文字列をパースしてJSONオブジェクトに変換してくれるやつ。パーサーはjawnを使用していて、実態はcirce-jawnモジュールにある。
io.circe.parser
パッケージのみがあり、parse
関数やdecode
関数を提供している。
circe-generic
よく使うパッケージはio.circe.generic.autoパッケージ。これを取り込んでおくと、JSONオブジェクト ⇔ case classおよび主要な型(String, Int, etc…)の相互変換を可能にしてくれる。
このモジュールによる変換は、変換が可能な場合コンパイルを通してくれる優れもの。反対に言うと、コンパイルが通らない場合は変換できない型が含まれている。その場合はその型のEncoder/Decoderを定義しておけばちゃんとcirce-genericモジュールで変換が可能になる。
よってコンパイルエラーとなった場合、ちゃんとすべての型が変換できるか確認すると良さ気。
その他モジュール
circe-opticsモジュールはJSONオブジェクトの操作(深い階層にある値を書き換えたりできる)を行えたりと、その他いろいろある。が、今回は割愛。
エンコード
JSONライブラリの主要な使い道はJSON文字列からデータ型への変換と、データ型からJSON文字列の変換を行うという2つだと思う。よってその2つの変換の方法を紹介。
データ型 → JSONオブジェクトの変換はエンコードと呼び、asJson
メソッドで行う。
asJson
メソッドの戻り値はio.circe.Json
のオブジェクトとなり、このオブジェクトのnoSpaces
メソッドやspaces4
メソッドなどで文字列に変換できるので、HTTPのレスポンスなどとして返す場合はそのメソッドを用いる。
import io.circe.syntax._ // これがないと`asJson`メソッドが使えない import io.circe.generic.auto._ // これがないと`io.circe.Encoder[Person]`を自動定義してくれなくて、`asJson`メソッドに渡すEncoderがないと怒られる case class Person(name: String) val person = Person("takkkun") val json = person.asJson println(json.noSpaces) // => {"name":"takkkun"}
asJson
メソッドはio.circe.Encoder[A]
をimplicit parameterで受け取るようになっている。circe-genericモジュールのとこでも書いたように、case classと主要な型であればio.circe.generic.autoパッケージを取り込むだけでそれらの型のEncoderが使えるようになるので、上記のコードはちゃんと動作する。
もし定義されていない型のEncoderが必要になる場合は、自前で定義する。
import io.circe._ import io.circe.syntax._ import io.circe.generic.auto._ import org.joda.time.LocalDate case class Person(name: String, birthday: LocalDate) val person = Person("takkkun", new LocalDate(1987, 1, 15)) // `io.circe.Encoder[org.joda.time.LocalDate]`が定義されていないのでコンパイルエラー person.asJson implicit val localDateEncoder = new Encoder[LocalDate] { final def apply(a: LocalDate): Json = Json.fromString(a.toString("yyyy-MM-dd")) } val json = person.asJson println(json.noSpaces) // => {"name":"takkkun","birthday":"1987-01-15"}
Encoderの定義方法いろいろ。以下はすべてio.circe.Encoder[org.joda.time.LocalDate]
を定義している。
// `io.circe.Encoder[A]`トレイトを継承してその場で定義 implicit val localDateEncoder = new Encoder[LocalDate] { final def apply(a: LocalDate): Json = Json.fromString(a.toString("yyyy-MM-dd")) } // `io.circe.Encoder[A].contramap`を使用(Aの箇所は変換先のJSON型を指定) implicit val localDateEncoder: Encoder[LocalDate] = Encoder[String].contramap(_.toString("yyyy-MM-dd")) // `io.circe.Encoder.encodeString.contramap`を使用(Stringの箇所は変換先のJSON型を指定、encodeIntとか) implicit val localDateEncoder: Encoder[LocalDate] = Encoder.encodeString.contramap(_.toString("yyyy-MM-dd"))
デコード
JSONオブジェクト → データ型の変換はデコードと呼び、parse
関数およびio.circe.Json
またはio.circe.HCursor
(io.circe.ACursor
)のas
メソッドで行う。JSONオブジェクトにせず、JSON文字列から直接データ型へ変換する場合はdecode
関数を使う。
import io.circe.parser._ // これがないと`parse`関数および`decode`関数が使えない import io.circe.generic.auto._ // これがないと`io.circe.Decoder[Person]`を自動定義してくれなくて、`as`メソッドに渡すDecoderがないと怒られる case class Person(name: String) val jsonString = """{"name": "takkkun"}""" parse(jsonString).right.flatMap(_.as[Person]) match { case Right(person) => println(person) // => Person(takkkun) case Left(error) => println(error) } decode[Person](jsonString) match { case Right(person) => println(person) // => Person(takkkun) case Left(error) => println(error) }
as
メソッドおよびdecode
関数はio.circe.Decoder[A]
をimplicit parameterで受け取るようになっている。エンコードのときと同様、case classと主要な型であればio.circe.generic.autoパッケージを取り込むだけでそれらの型のDecoderが使えるようになるので、上記のコードはちゃんと動作する。
もし定義されていない型のDecoderが必要になる場合は、自前で定義する。
import io.circe._ import io.circe.parser._ import io.circe.generic.auto._ import org.joda.time.LocalDate case class Person(name: String, birthday: LocalDate) val jsonString = """{"name": "takkkun", "birthday": "1987-01-15"}""" // `io.circe.Decoder[org.joda.time.LocalDate]`が定義されていないのでコンパイルエラー decode[Person](jsonString) implicit val localDateDecoder = new Decoder[LocalDate] { final def apply(c: HCursor): Decoder.Result[LocalDate] = c.as[String].right.map(LocalDate.parse) } decode[Person](jsonString) match { case Right(person) => println(person) // => Person(takkkun,1987-01-15) case Left(error) => println(error) }
Decoderの定義方法いろいろ。以下はすべてio.circe.Decoder[org.joda.time.LocalDate]
を定義している。
// `io.circe.Decoder[A]`トレイトを継承してその場で定義 implicit val localDateDecoder = new Decoder[LocalDate] { final def apply(c: HCursor): Decoder.Result[LocalDate] = c.as[String].right.map(LocalDate.parse) } // `io.circe.Decoder[A].map`を使用(Aの箇所は変換元のJSON型を指定) implicit val localDateDecoder: Decoder[LocalDate] = Decoder[String].map(LocalDate.parse) // `io.circe.Decoder.decodeString.map`を使用(Stringの箇所は変換元のJSON型を指定、decodeIntとか) implicit val localDateDecoder: Decoder[LocalDate] = Decoder.decodeString.map(LocalDate.parse)
まとめ
circeによる相互変換はだいたい書いたやつで事足りる。と思う。少なくとも私が実際にjson4sからcirceに乗り換えたときには。
Argonautも使ってみたが、毎回すべての型に対してEncoder/Decoderを書かなければいけないので、なかなか面倒だった。その点circeは必要な分だけ書けばいいので、これが思いの外嬉しい。実際に書いたらよく分かった。
その上でリフレクション使っていないので、なんというか安心感があって助かる。今後はcirceを使っていこうかな。
Akka HTTPを使ってみる。Akka Streamsにもちょっと触れるよ
Akka HTTPとはAkkaの上でHTTPサーバーを実現したものです。HTTPクライアントとしての機能を持っていたり、WebSocketのサポートもしています。
sprayという似たものもありますが、spray/sprayには「長らくメンテしてないよ。Akka HTTPに置き換えらているよ」とアナウンスされています。ですので、素直にAkka HTTPを使います。
ふたつのAPI
Akka HTTPのサーバーサイドにはレベルの異なるふたつのAPIがあります。
低レベルAPI(Low-Level Server-Side API)
Akka HTTPはAkka、もっと言うとAkka Streamsを利用して書かれているのですが、低レベルAPIはそのAkka Streamsが剥き出しになっており、よりHTTPに近い方(コネクションとか)にも触れられます。こちらを利用することによってWebSocketによるアクセスを捌くことも出来ます。
Akka Streamsが剥き出しにはなっていますが、それらのあれこれをやってくれて、ちょっと短く書けるように、bindAndHandleSync
というメソッドがあります。そのメソッドを使うと以下のように書けます。
import akka.actors.ActorSystem import akka.streams.ActorMaterializer import akka.http.scaladsl.Http import akka.http.scaladsl.model.{ HttpRequest, HttpResponse, Uri } import akka.http.scaladsl.model.HttpMethods._ implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() val handle: HttpRequest => HttpResponse = { case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => HttpResponse(entity = "pong") } Http().bindAndHandleSync(handle, "localhost", 8080)
bindAndHandleAsync
メソッドもありますが、こちらはHttpRequest => Future[HttpResponse]
と、Futureを使うようになります。
高レベルAPI(High-Level Server-Side API)
高レベルAPIは低レベルAPIをラップし、ルーティングの機能を備えたものです(よく見るとこのページのURLも"routing-dsl"になっていますね)。その分低レベルAPIに比べて細やかなことは出来ません。
高レベルAPIでは、akka.http.scaladsl.server.Directives
のメソッド群と、bindAndHandle
メソッドを使います。
import akka.actors.ActorSystem import akka.streams.ActorMaterializer import akka.http.scaladsl.Http import akka.http.scaladsl.server.Directives._ implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() val route = path("ping") { get { complete("pong") } } Http().bindAndHandle(route, "localhost", 8080)
bindAndHandle
メソッドの第1引数に渡すのはFlow[HttpRequest, HttpResponse, Any]
という型の値で、このFlowというのはAkka Streamsのものです。ですので、高レベルAPIにおいてもAkka Streamsが隠蔽されるわけではないのですが、akka.http.scaladsl.server.Directives
(path
, get
, complete
はすべてこのオブジェクトのメソッド)を用いるので、低レベルAPIに比べるとほとんど意識しなくなります。
反対に、低レベルAPIにおいてFlowを用いる箇所ではakka.http.scaladsl.server.Directives
で組み立てたものを渡せたりもします。
低レベルAPIを使ってみる
高レベルAPIのakka.http.scaladsl.server.Directives
が提供するメソッド群を使ったルーティングの記述はややクセがありますが(sprayに触れてた人はほぼ同じだから馴染みやすいらしい)、見て分からないほどのものではないと思います。が、これは他のウェブフレームワークでもやってくれそうなもの。というわけで、低レベルAPIの方を詳しく見ていきます。
その前に軽くAkka Streamsを説明。
- Source: 何かしらの入力を発生させる。始端。ひとつの出力チャンネルを持つ
- Sink: Sourceから発生した入力が最終的に行き着く先。終端。ひとつの入力チャンネルを持つ
- Flow: 入力を受け取って、別の形に変換し出力する。入力チャンネルと出力チャンネルをひとつずつ持つ
より突っ込んだ話であれば:
あたりが詳しかったです。
Akka HTTPの低レベルAPIはこのAkka Streamsを利用しているので、それをわざとらしく使用したコードが以下です。
import akka.http.scaladsl.Http import akka.stream.scaladsl.Sink val source = Http().bind("localhost", 8080) source.runWith(Sink.foreach { connection => println(s"New connection is accepted: ${connection.remoteAddress}") // handle connection })
bindAndHandleSync
メソッドやbindAndHandle
メソッドではなく、bind
メソッドを使用すると、戻り値はSource[Http.IncomingConnection, Future[Http.ServerBinding]]
となります。ポイントはSourceを返している点です。
このSourceはHttp.IncomingConnection
を発生させるSourceです。ですので、このあとにFlowやSinkを繋げばHttp.IncomingConnection
、つまりHTTPによってアクセスされた場合のコネクションを得ることが出来るわけです。
source.runWith
メソッドにSinkを渡すと実行されるので、HTTPコネクションを受け取るSinkを渡してあげればOKです。
ただ、Sinkは入力の行き着く先ですので、これだけではHTTPコネクションを受け取って終わりになってしまいます。何の処理もしていません。8080ポートで接続を受け付けるようになっただけです。
ですので、connection.handleWith
メソッドを使います。
import akka.http.scaladsl.Http import akka.http.scaladsl.model.{ HttpRequest, HttpResponse, Uri } import akka.http.scaladsl.model.HttpMethods._ import akka.stream.scaladsl.{ Sink, Flow } val source = Http().bind("localhost", 8080) source.runWith(Sink.foreach { connection => println(s"New connection is accepted: ${connection.remoteAddress}") connection.handleWith(Flow.fromFunction { case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => HttpResponse(entity = "pong") }) // Http().bindAndHandleSync(...) の第1引数に渡すものと一緒 // connection.handleWithSyncHandler(handler) // Http().bindAndHandleAsync(...) の第1引数に渡すものと一緒 // connection.handleWithAsyncHandler(handler) })
handleWith
メソッドは第1引数にFlow[HttpRequest, HttpResponse, Mat]
(Mat
の説明は割愛)を受け取ります。想像に難くないかもしれませんが、HttpRequest
を入力として受け取り、HttpResponse
を出力として返すFlowのことであり、そのFlowでHTTPリクエストを処理してHTTPレスポンスを返してやってね、ということです。
これでHttp.IncomingConnection
を受け取った後、あらたにHttpRequest
を受け取りHttpResponse
を返すというふたつのストリームによって、HTTPサーバーを実現します。
ちなみにコメントでも書きましたが、Http.IncomingConnection
にはhandleWithSyncHandler
メソッドとhandleWithAsyncHandler
メソッドも定義されていて、これはHttp()
で使えるbindAndHandleSync
メソッドおよびbindAndHandleAsync
メソッドにて実現できることをHttp.IncomingConnection
にて実現するものです。
とまあこんな風にHTTPリクエストなどなどを扱えます。高レベルにHTTPを隠蔽するなら他の選択肢もありますが、HTTPコネクションのレベルで何かしてやる場合、Akka HTTPはひとつの選択肢だなーと思いました。
型推論ふりかえり
型推論、よく聞きますね。よく聞くのでWikipediaにも型推論のページがあります。
この仕組みの恩恵は案外自然と享受しているものですが、機能を提供することライブラリの開発においては知らないと始まらない前提のようなものであると最近思います。が、自然と享受しているので「あーはいはい」となんとなくの理解だったりします。主に私が。
なのでどう考えればいいかを順に辿っていきます。
型推論をしないケース
型推論の話をする前に、まったく型推論しないケースを考えてみます。以下のようなケースはそうです。
val name: String = "takkkun"
変数name
の型はString
で、String
型の値である"takkkun"
を代入しているので、まあ普通のコードです。ただやたら丁寧なだけですね。
型推論とは「型に関する情報が欠けている箇所を補うもの」である
私は特別型推論に詳しいわけではないのですが、型推論が何をしてくれるものかと言うと、型に関する情報が欠けている箇所を補ってくれるものだと思っています。例えば以下のケース。
val name = "takkkun"
型推論をしないケースと比べると、変数name
の型が指定されていません。つまり型に関する情報が欠けています。ですので型推論が働き、右辺の"takkkun"
がString
型の値であるので、変数name
はString
型だな、となります。難しい話じゃないですよね。
型の情報が欠けるケースは代入先だけかと言われるとそんなことはありません。代入元(右辺)に型の情報がないこともありえます。型パラメータを使っている場合です。以下はまたまた型推論しないケース。
val lover: Option[Person] = Option.empty[Person]
型推論を使うと:
val lover = Option.empty[Person]
とも書けるし:
val lover: Option[Person] = Option.empty
とも書けます*1。
前者は変数name
に"takkkun"
を代入するのと同じケースです。後者について詳しく見ていきます。まずOption
objectのempty
メソッドの定義を見てみます。
def empty[A] : Option[A] = None
型パラメータA
を取っていますね。なのでOption.empty
と呼び出すと、やはり型の情報が欠けることになるわけです。というわけで型推論の出番。以下のように考えると分かりやすいかなーと思います。
- 代入先の変数
lover
の型はOption[String]
である empty[A]
メソッドの戻り値はOption[A]
であるempty[A]
メソッドの戻り値をOption[String]
にすると型が一致するので、A
はString
とする
おお、A
はString
と解決できましたね。こんな風に求める側の型(変数の型, 関数の仮引数の型, 関数の戻り値の型, etc…)をJavaではターゲット型と呼ぶそうですが、そのターゲット型から推論するケースですね。
具体例
以下は型推論が働いていて「おお、ナイス」と個人的に思ったケースです。他にもきっとあります。
関数を代入するケース
val intToString: Int => String = { i => i.toString }
変数側にInt => String
型を指定しない場合、仮引数i
の型が不明となるため、val intToString = { i: Int => ... }
と書かなければならくなります。「まあ別にそれでもいいじゃん」と言ってしまえばそうです。が、このケースでは仮引数であるi
に対してtoString
メソッドを呼んでいるだけですので:
val intToString: Int => String = _.toString
と書けます。この書き方にすると、そもそもi
という仮引数が現れなくなるため、代入先の方に型を指定して推論させる必要が出て来ます。
当然ですが:
val lover: Option[Person] = Option.empty lover.foreach { person => // do something }
というケースでも、仮引数person
に型を指定しなくても大丈夫なのは、Option[Person]
型のforeach
メソッドがPerson => Unit
(正確にはUnit
じゃありませんが割愛)となり、そこから型推論できるためです。「変数への代入」も「実引数として与える」のも同じ推論が働くと思っていいと思います。
ScalikeJDBC
RDBとあれこれする場合はよくScalikeJDBCを使っているんですが、このときもよくこんな書き方(というかscalikejdbc-mapper-generatorによって生成される)をします。
object Person { def apply(p: ResultName[Person])(rs: WrappedResultSet): Person = new Person( id = rs.get(p.id), name = rs.get(p.name), lover = rs.get(p.lover), ... ) }
rs.get
の箇所ですが、このget
メソッドは型パラメータA
を受け取り、それが戻り値となるようになっています。ですので、キーワード引数によって与えられる先の仮引数の型からA
が何か推論されています。受け取る側の型を気にせずget
メソッドを並べれば良いので、すっきりしていますね。
で、さらにTypeBinder[A]
が求まり、という話になってくるのですが、それは型クラスの話なので今回は割愛します。
私がナイスと思った具体例、代入先から代入元を推論するケースばかりですね。
まとめ
実際のところ型推論は便利です。記述がすっきりします。変数の型指定や無名関数の仮引数の型指定、関数が求める型パラメータの指定などを省略できます。
しかしどのケースにおいても重要なのは「型に関する情報が欠けている箇所を補う」ために型推論は働き、その推論の元となる型は必ずどこかに指定があります。推論は既にある事実がないことには始まりませんからね。
型推論は案外付きまとうものですが、人の頭でなぞれば分かるよ、怖くないよという気付きと振り返りでした。
YAP(achimon)C::Asia Hachioji 2016 mid in Shinagawaに行ってきた
7月2日(土)、3日(日)の2日間開催されたYAP(achimon)C::Asia Hachioji 2016 mid in Shinagawa(以下ヤパチー)に行ってきました。
yapcasia8oji-2016mid.hachiojipm.org
トークした
実はこういうイベントに参加するの初めてで、まートークしたことも当然ないです。が、同僚のしんぺいさん(id:nkgt_chkonk)に誘われ、「応募する〜する〜」とか雑な返事をし、社内リポジトリに:
こういうIssueが立てられ、「いっちょやってやるか」と雑な意識を奮い立たせてですね、応募しました。で、採択されました。
発表の内容はこんな感じ。
普段会社でやっていることを発表しただけです。が、まとめるとなるとそれなりにパワーがいるもので、結構尻込みしたりするものです。そういう意味でしんぺいさんからの後押し(業務命令じゃないぞ!いやほんとうに)だったり、「楽しいことやるんだぞ!」という雰囲気があったのはとても良かったと思います。
なんというか、ややエモ話になるんですが、やったことないと大丈夫かなアハ〜ンとか思ったり、怯えたりとかで、見送る、見送り続けるってよくある話だと思うんです。ですが、同時に「このままでいいのかな」みたいな焦りもあるわけです。人と話すたびに「なんだこれ知らない……。知らなさすぎることがありすぎる……。大丈夫なのかこのままで私……」みたいなね。
ただそのまま何もしないと、そう思い続けるだけで。最悪なのは1, 2日経つとそんな感情まで忘れてしまうところですよ。適度に褒められずにやっていけるほどマッチョでもないし、かといって何もしないと褒められないしで、要は人に見える形で行動を起こせみたいな自己啓発本みたいなことを言うわけですが。
とまあそんな中途半端なところに火を点けられたので、よかったです。やってよかった!!!!!!ばんざい!!!!!!!これからも熱量は上げていきたい所存。
ちなみに発表の内容に対して、発表後や懇親会で聞かれたことのひとつに「これ後からフィールド(プロパティ)の内容書き換えたら意味なくね?(要約)」があるんですが、これ後でエントリーにします。このブログに書くかもしれませんし、社のブログに書くかもしれません。ここに書こうかと思ったが、ちょっと量が多くなりそうなんだ!
もうひとつトークした
当日はほとんど飛び入りトラックが行われる部屋に居ました。飛び入りトラックは当日までタイムテーブルが空っぽで、当日「これを喋るぞ!喋らせろ!」とかいう要請の元タイムテーブルを埋め、その時間が来たら発表してもらう、ってやつなんですが、そこでもトークしました。まあただの漫談なんですけど。なのでスライドはありません。
内容はしんぺいさんと「Scalaとか設計とか」について話すというもので、とか言いつつ大体は「immutableとmutableの差による設計への影響」みたいなところが焦点だった気がします。なんというか、いつも会社で話していることの延長線ですね。
スタッフした
ほとんどを飛び入りトラックの部屋で過ごしたのは、そこのスタッフだったからです。もちろんスタッフ経験も初めて。初めて尽くしですね。
他の部屋の雰囲気はそんな掴めていないのですが、飛び入りトラックの部屋はそれらに比べて大分規模が小さく、その分トークする人とそれを聞く人の距離が近くて良い雰囲気でした。特に id:kksg さんのErgoDoxの話は楽しかった。実際に触らせてもらえたし、触ったら全然今までの感覚が通用しないし、それが面白すぎるし、マウスのくだり(マウスからの入力に対してErgoDoxで「マウスのカーソル動かせますよ」「動かせんの!?」というやつ)はコントかよって感じで笑ったし、本人は訪問販売とか言ってましたが、まんまと欲しくなりました。いや、本当に買おうかな……。
まとめ
まとめというか、だいたい「圧倒的感謝」みたいな話になって、「おいおいあいつ多幸感に包まれてそのまま……」みたいに思っちゃうんですが、本当に感謝は尽きなくてですね。
主催のuzullaさん、会場を提供してくれたMicrosoftさん、スタッフさん、トークを聞きに来てくれた方々。そういう方々に感謝をお伝えしたいです。
それとお前好きすぎるだろと言う話なんですが、しんぺいさんにも感謝を伝えたいと思います。もしやこれはヤパチー行ってきたエントリーに偽装したラブレターかな?
とにかく!楽しかった!最高!また次(「の機会」という安全側に倒した表現はせず、是非やってほしい、手伝っていくぞ、というスタンス)も参加するぞ!
PlayStation 3が欲しかったのでその欲望を満たしてみたら想像以上に満たされた
「PlayStation 3とか買ってもやりたいタイトルあるの?」と言っていたのは何年前か。多分PlayStation 3が発売された直後とかだと思います。今ではタイトルの本数もかなりの数に上るんじゃないでしょうか。だってPlayStation 4の時代ですもんね。
しかもBlu-ray Discも再生できるとか何だこれ最強だろって思います。
というわけでPlayStation 3を買いました。ついでにいろいろ揃えました。
PlayStation 3 クラシック・ホワイト 250GB (CECH-4200BLW)
- 出版社/メーカー: ソニー・コンピュータエンタテインメント
- 発売日: 2013/09/02
- メディア: Video Game
- この商品を含むブログ (2件) を見る
大神 絶景版 PlayStation 3 the Best (『大神 絶景版』オリジナルダイナミックカスタムテーマ プロダクトコード 同梱)
- 出版社/メーカー: カプコン
- 発売日: 2013/11/07
- メディア: Video Game
- この商品を含むブログ (2件) を見る
iiyama WLEDバックライト搭載 27型ワイド液晶ディスプレイ ProLite E2773HS-GB2
- 出版社/メーカー: マウスコンピューター
- 発売日: 2013/05/24
- メディア: Personal Computers
- この商品を含むブログを見る
ONKYO WAVIO アンプ内蔵スピーカー 15W+15W ブラック GX-D90(B)
- 出版社/メーカー: オンキヨー
- 発売日: 2002/07/15
- メディア: エレクトロニクス
- 購入: 7人 クリック: 262回
- この商品を含むブログ (18件) を見る
【Amazon.co.jp限定】PLANEX ハイスピードHDMI Ver1.4ケーブル 1m (PS3/Xbox360) PL-HDMI01-EZ (FFP)
- 出版社/メーカー: プラネックス
- 発売日: 2010/07/09
- メディア: エレクトロニクス
- 購入: 77人 クリック: 248回
- この商品を含むブログ (10件) を見る
PLANEX 光デジタル角型オーディオケーブル 1m PL-DA01
- 出版社/メーカー: プラネックス
- 発売日: 2008/02/10
- メディア: エレクトロニクス
- 購入: 29人 クリック: 45回
- この商品を含むブログ (4件) を見る
- 出版社/メーカー: MacLab.
- メディア: エレクトロニクス
- この商品を含むブログを見る
Thunderbolt/HDMIケーブルは所有のMacBook Airと液晶ディスプレイを繋ぎたかったので、ついでに購入しました*1。
で、届いたその日に各機器を繋いで、こんな感じに。
スピーカーを置く専用の台が欲しいです。ぴったりなの探すより自作した方が早い気がする。まあそれは追い追い。
とりあえず早速念願であった大神 絶景版をプレイしてみました。いや、めっちゃ綺麗ですね。FF8で「うわー、すげー綺麗!」と感じた記憶が蘇えり、「これがHDMIの力か……」とかバカみたいなこと思いました(機器をHDMIで接続するのが初めてだった)。HDMIもあるでしょうが、昨今の技術やPlayStation 3の能力でしょうね。そして27インチにして良かったって思いました。今度はおおかみこどもの雨と雪や、ラブライブ!のBlu-rayでも買いたいところです。
ひとしきり遊んだら今度はMacBook Airとディスプレイを繋いで、デュアルディスプレイを試してみました。とりあえずまだ観てなかったキルラキル13話を観て、正直PlayStation 3による体験より、こっちの体験の方が強烈なんじゃないかと思いました。ついでに買った割には予想外の充足を得られたThunderbolt/HDMIケーブル。
ちなみにオーディオは、PlayStation 3とスピーカーは光デジタルオーディオケーブルで繋いで直接出力し、ついでに液晶ディスプレイとスピーカーをアナログケーブルで繋いでMacBook Airからの音声は液晶ディスプレイ経由のスピーカー出力にしてみました。もう少し賢い方法があれば良いのだけど。
ひと通り堪能したので、あと何がいるかなと考えたところ、「あー、入力機器を切り替えるたびにHDMIのケーブルを抜き差しするの面倒」と思い至ったので:
iBUFFALO HDMI切替器HEAC対応2ポートBSAK202
- 出版社/メーカー: バッファロー
- 発売日: 2011/01/18
- メディア: エレクトロニクス
- 購入: 11人 クリック: 50回
- この商品を含むブログを見る
も購入したのですが、これだけが充足を得られませんでした。
これのオスを液晶ディスプレイに差し、メス2口にPlayStation 3のとMacBook Airのを繋いで、切り替えを目論んだのですが、PlayStation 3の方は入力に問題ないものの、MacBook Airの方はノイズが乗り、見られたもんじゃない写り方になりました。このスイッチがThunderboltからの出力と相性が悪いのか、それともThunderbolt/HDMIケーブルの方がよろしくないのか、とりあえず購入した商品の組み合わせだと非常に都合が悪い感じです。「これなら写る!」って構成をご存じの方が居たら教えてほしいです。
とりあえず直に差す方向で乗り切りますが、基本的には非常に良い買い物をしたなあって思います。やっぱ新家電導入の体験はいつでもシビれるもんですね。
Capistrano 3への手引き
Capistrano、便利ですよね。
最近メジャーバージョンアップがあったのですが、使い方、というかスクリプトの書き方やお作法が変わり、「Capistrano 3にアップデートしたはいいけど全然動かなくてどうなってんだ」という流れはもはやお約束みたいです。
試しに僕も個人で作ってるウェブサイトのCapistranoをアップデートしてみたので、その上でこんなところに気を付けたいな、と思うポイントでも書いておきます。
capifyは使わない
Capistranoを使うときは$ bundle installをし、次に$ bundle exec capify .とするのがお約束の流れですが、これからはcapifyを使ってもcap installを使ってねと言われます。
ですので:
$ bundle exec cap install
としましょう。
マルチステージが最初から有効になっている
ひとつのスクリプトで複数の環境(ステージ)にデプロイするときは今までcapistrano/capistrano-extのcapistrano/ext/multistageを使うのが常套手段でしたが、これはデフォルトで有効になっています*1。
デフォルトで有効であるため、cap installを行うと、Capfileとconfig/deploy.rbという今まで通りのものに加え、config/deploy/staging.rbとconfig/deploy/production.rbが作られます。もしconfig/deploy/*に書き出されるステージ名を変えたい場合は:
$ bundle exec cap install STAGES=development,staging,production
とSTAGES環境変数でcap install渡してあげましょう。
Capistrano拡張の対応
Ruby on Rails然りですが、これだけバージョンが変われば、今まで使ってきた拡張も使えなくなるというものです。
ただいくつかの拡張に関してはcapistrano自身が対応させた拡張を公開していますので、そちらを使いましょう。
# Gemfile source 'https://rubygems.org' group :development do gem 'capistrano' gem 'capistrano-rbenv', github: 'capistrano/rbenv' gem 'capistrano-bundler', github: 'capistrano/bundler' gem 'capistrano-rails' end
capistrano-railsは「Capistranoは別にRuby on Railsだけのものじゃないんだよ」を体現するために別扱いになりました。こちらはrubygems.orgにあるため、githubオプションなどでgitリポジトリを指定する必要はありません。
また、これらの拡張は今まで人によってconfig/deploy.rbなどにrequireを記述していたかもしれませんが、Capfileに書くのが通例のようです。実際、cap installによって生成されたCapfileには上記の拡張がコメントアウトされています。必要に応じてコメントアウトを外しておきましょう。
変数の参照
今まで:
set :application, 'example-app'
として設定した変数は直接applicationとローカル変数のように参照できていましたが、これからは出来ません。fetchメソッドを使いましょう。
set :deploy_to, "/u/apps/#{fetch(:application)}"
なお、current_pathなどは変数とはまた別の扱いらしく、今まで通りローカル変数のように参照できます。
変数名の変更
「なんで変えたんだ!」と言いたくなる気がしますが、repository変数はrepo_url変数と名前が変わりました。拡張ではありますが、capistrano/rbenvの場合、rbenv_pathではなくrbenv_type(:systemで/usr/local/rbenv、:userで~/.rbenv)で、rbenv_ruby_versionはrbenv_rubyになりました。
大きな変更というより、Capistrano開発者の好みの範囲での変更に思えてしまいますが、ドキュメント参照の上、適切に設定する必要がありそうです。
デプロイ先のサーバの設定
今までは下記のように書いていました。
role :app, 'app1.example.com', 'app2.example.com' role :web, 'web1.example.com' role :db, 'db1.example.com', primary: true
Capistrano 3でも上記記述でエラーが出るわけではないのですが、公開鍵での認証が出来なくなってしまいました。以下のように書くのがベターな気がします。
server 'app1.example.com', roles: %w(app), user: 'deploy', ssh_options: { keys: [File.expand_path('~/.ssh/...')] }
正直ここら辺はよく調べていないのですが、サーバごとに認証を設定できたりする方が理に適っていて、個人的にはこちらの方が好きです。
共有ディレクトリの設定
Capistranoはdeploy_toの配下にcurrent, releases, sharedの3つのディレクトリを作成します*2。
その中のsharedにはデプロイの度に上書きを避けたいファイルなどをディレクトリにまとめて置き、デプロイ時にそのディレクトリへシンボリックリンクを張るという処理が行なわれていました。そのシンボリックリンクを張るディレクトリはshared_childrenという変数で設定できていたのですが、それがlinked_dirsに変わりました。ちなみにlinked_filesという変数も追加されています。
ただ名前が変わっただけなら良いのですが、ちょっとディレクトリ構造が変わることがあるので、注意が必要です。具体的には:
set :shared_children, %w(public/system)
とすると、deploy:setup時にshared/systemが作成され、そのディレクトリがreleases/LATEST_RELEASE/public/systemとしてシンボリックリンクを作成するような動作になっていましたが、Capistrano 3では:
set :linked_dirs, %w(public/system)
とすると、shared/systemではなく、shared/public/systemが作成されるようになります。ですので、Capistrano 2から乗り換えた場合、サーバ側でちょっと手を加えてあげないと今まで参照できていたファイルが参照できない、などといったことが発生するため注意が要ります。
各タスクの記述
ここが一番大きな変更点だと思います。今までtaskメソッドを呼び出してタスクを定義し、そのメソッドへのオプションでどのロールで実行するのか、などを定義していました。タスクの中では愚直にrunを並べて、そのタスクが何を行うのかを記述していました。何度もrunを呼びたくないからと、コマンドを組み立て、runメソッド1回で全コマンドを実装するように心掛けていた人もいるんじゃないかと思います。
Capistrano 3では基本taskメソッドはタスク名しか渡さず、その中のonメソッドでいろいろ記述します。
task :example do on roles(:app), in: :sequence, wait: 5 do # ~ end end
そしてonメソッドに渡したブロックの中で実行するコマンドなどを記述していきます。
コマンドの記述
先述のように、onメソッドに渡したブロックの中にコマンドを記述するのですが、ここで様々なメソッドが使えるようになっています。
namespace :log do task :clear do on roles(:app), in: :parallel do # 渡したパスが存在するかを確かめ、存在しなければブロック内のコマンドを実行しない。 # また、コマンド実行時に先頭に"cd 渡したパス && "を付与する within current_path.join('log') do # コマンド実行時に先頭に環境変数の設定を付与する。 # 下記の例では"RAILS_ENV=rails_env変数の値"となる with rails_env: fetch(:rails_env) do # コマンドの実行成否を真偽値で取得する(元々あった?) if test '[ -f development.log ]' execute :rm, 'development.log' end end end end end end
他にもasなどがあるのですが、これらはleehambley/sshkitというSSHによるコマンドの実行を構造的に記述するためgemの仕業です。タスクの記述方法然り、コマンドの記述方法然りなのですが、ここらへんの記述方法が変わったのはSSHKitを導入したからなのかなあ、と思います。
SSHKitを使えばexecuteをもっとスマートに書けたりするので、ぜひ活用していきたいところです。ちなみにonメソッドもSSHKitのものですね。
deploy:setup
なくなりました。いきなりdeployで大丈夫です。他にもいくつかタスクの名前が変わっていたりします。
他にもまだまだたくさんありそうですが、というかcap installで作成されたスケルトンから読み取れることしか書いてないのですが、とりあえずこんなところで。
*1:ていうか2.xのいつからかcapistrano-ext使わなくても有効でしたよね?
*2:Capistrano 3ではrepoも加わりました。単にcloneやpullをするための(サーバから見て)ローカルリポジトリです
EmacsからVimに乗り換えた
ちょっと前にEmacs + markdown-modeでドキュメントを書いていたのですが、Markdownって改行を入れると、ウェブブラウザ上では改行されていなくとも、HTML上で改行(単にCRが入ってる、という意味)されるじゃないですか? そういうときってウェブブラウザ上で見ると、どうしてもCRによるホワイトスペースが入っちゃうんですよね。それがもう嫌で嫌でしょうがないって人、結構いると思います。僕です。
て言うかそもそも改行を入れていたのは、1行あたり80文字程度に収めるためです。つまり、次行への送りを手動でやっていたわけです。そこからしてナンセンスなわけですが、じゃあなぜ行の折り返しを使わなかったのかと言うと、あれ、行の折り返しを使うとCtrl+nとかで移動したとき、折り返された部分を飛び越して、その次の行に飛んじゃいますよね。「あ〜〜〜〜〜〜!」って感じです。
それもEmacs 23からはvisual-line-modeというマイナーモードのおかげで解決できるのですが、恥ずかしながら僕が使っているのはMac OS X LeopardでCarbon Emacsです。バージョンは22…… あまりに酷いので一昨日ぐらいに一番新しいMacBook Air注文しました。これで解決。
ですが、届くまで待てないので、なんか良い機会だし、Vimに乗り換えるか〜っていうのが経緯です。経緯長くてごめんね。
以下何をしたか、です。
続きを読むMeCabのメモリ管理はどうなっているのか
以前mecab-rubyを用いた下記のコードがコケる場合がありました。
node = MeCab::Tagger.new.parse(text)
これはparseメソッドを呼び出している最中にMeCab::TaggerのインスタンスがGCによって解放されてしまい、メモリ違反を起こすためです(昔のことなのであやふやですが)。なので、parseメソッドに与える文字列の長さが短い場合はエラーが発生せず、長ければ長いほど発生し易いといういやらしいものです。
まあ今手元のmecab-ruby(MeCabのバージョンは0.996)で試したら'a' * 100_000な文字列を与えてもそんな問題は発生しませんでしたけどね…… おかしいな……
そもそもなんでこんな話を思い出したかと言うと、MeCabのメモリがどういう風に管理されているかが気になったからです。
MeCabは0.99になってからModelとLatticeという概念が導入され、マルチスレッド時における使用法が変わりました。Modelはこの問題によって存在を知りましたが、実を言うとLatticeは知りませんでした。で、さきほどLatticeが気になりだしたのですが、なぜLatticeはスレッド毎に生成しなければならないのか、あれ、メモリはどうなってるんだろう、と次々と疑問が湧いたのでMeCabのソースコードで確認した次第です。
それのレポートがこちら。
なおMeCabのバージョンは最新の0.996です(ソースコード)。調べた対象のファイルはsrc/tagger.cppです。
続きを読む