読者です 読者をやめる 読者になる 読者になる

seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Future についての私見

qiita.com

この記事を読んで、記事の本筋からはそれますが、前から思っていたことをつぶやいたのでまとめておきます。

「それ、今まで通り同期処理で Web ページ返す処理つくるだけで全然いいよね」という要件なのに、非同期縛りをやるのはちょっと不毛な気がするな...という。超高負荷な環境で CPU 使用抑えたい、同時接続が多いんで、とか何かしらの強いモチベーションがあることが大事で、それ以外は適材適所がよいと思うのです。

もちろん新しいアプローチにトライするのは楽しいことだし、そういうコミュニティの盛り上がりは素晴らしいことなので水を差したいわけでは全くないんだけど、仕事でやる開発において「Play とか Scala が盛り上がってる。Rails でやってた案件を今度は Scala でやろう。」みたいなことだとまず幸せにならないなと。小規模な案件は実はただもう少し静的型が欲しいだけなんじゃないかなって思ったりしています。

Java を Scala の中で使うこと

ScalikeJDBC は手軽なはず

ScalaでORマッパーというとSlickやScalikeJDBC等色々あるが、ほんとにちょっとしたツールを作りたいだけならばもっと手軽にやりたいと思うはず。

ScalaでDBを使った小物ツールをサクッと作れるJDBI - 気まぐれラボラトリィ

手軽ですよということを示すためにちょっとしたサンプルを書きました。ScalikeJDBC や Skinny ORM のミニマムな導入方法としても参考になるかなと思います。

https://github.com/seratch/jdbi-scalikejdbc-skinny-orm-example/blob/master/src/main/scala/app.scala

JDBI については私も結構好きなんですが Java であることの限界が見えてしまっている感はあります。SQLアノテーションに書くわけですが、改行もできないですし、アノテーションに渡す文字列はベタ書きするしかないので、こういう簡単なケースではなく、それなりの SQL を書くとなるとかなり苦しいと言わざるをえません。

また Java ライブラリ一般に言えることとして @BeanProperty とか Java っぽいアノテーション駆動な処理が入るコードでは Scala の恩恵を受けづらいことが多いと思います。コンパイルの重さを考慮するとそういうケースでは Groovy の方がよかったりするかもしれません。

JavaScala の中で使うこと

今更という気もしますが、ちょうどよい機会なので Scala の中で Java を使うことについて意見を書いておこうかなと思います。

いざとなれば Java の既存コードをそのまま使えるのは Scala のよいところなのですが、あくまで「いざとなれば」というオプションであり、かなりニッチな分野であればまだしも、RDB アクセスのようなありふれたユースケースではファーストチョイスが Java の利用とは考えない方が幸せになれると思います。

自分自身を振り返っても Scala を始めた頃は「手慣れた Java のライブラリを使ってやった方が楽なのでは?」と思ったりしたものですが、結局 JavaScala の架け橋となるところをケアしながら書かないといけないので簡潔にはなりにくく、落とし穴もあったりします。

例えば null は Scala の世界では「なかったこと」になっていて Option にしてあげないといけないですが、そういうことをやってくれる Java のライブラリはまずないので JavaAPI からの戻り値を自分で Option で包んであげないといけなかったりします。また Scala のコレクション型のオブジェクトは Java の世界からはうまく扱えないので Java の世界に渡す前に必ず scala.collection.JavaConverters なりで変換してあげる必要がありますし、逆に java.util.List などが Java から返ってくれば毎回 #asScala を呼んで変換する必要があります。

どういうときに Java を混ぜるか

ということで Scala であえて Java API を使う場合というのは、既に JavaSDK が提供されている場合だったり、よほど Scala で実装されたライブラリで選択肢がないケース(枯れてない、難解すぎる、機能が足りない、メンテされてないなど)に限るのがよいかと思います。

「どうしてもこの Java ライブラリを流用したい」というケースはあるかと思いますが、それほどのものなのであれば自分で Scala ラッパーを書いてみることも検討してもよいでしょう。OSS として公開すれば一定の需要もあるかもしれません。

以上、あるべき論で Scala らしいコードを書くべきということではなく「(ニッチなケースでなければ)普通は Scala ネイティブなやり方をした方が結果的に楽ですよ」という話でした。

Anorm についてのふりかえり

Twitter 上でのやりとりを読んでいて、Anorm は 2.4 から Play Framework 本体からは分離されたとはいえ Play チームがメンテし続けるであろうライブラリであることには変わりないので、機能の比較以前にそういったバックアップ体制を重視するなら Anorm を選択するという判断もあるのかなと思いました。

https://github.com/playframework/anorm

Anorm といえば Play 2.0 が出た直後の勉強会で Anorm のコードをざっと読んで短い発表をしたことがありました。当時の Scala の DB ライブラリ事情は ScalQuery、Squeryl、Lift Mapper、Querulous(MySQL のみ対応)といったあたりがメジャーどころで ScalikeJDBC は Querulous にインスパイアされた初期バージョンの段階でした。

http://www.slideshare.net/seratch/reading-anorm-20-12238243

当時の私の印象は「何かすごくいい点があるというわけではないが ScalaQuery とかに比べると使い方は単純でわかりやすそう、SQL を書いて次にどう取り出すか書くという API も直感に合っているし、いろんな場面でツールとしては使えるのかもしれないな」という感じでした。ScalikeJDBC にもその後 SQL オブジェクトの API を追加しましたが、これは見て明らかな通り Anorm に強く影響されています。

Anorm と ScalikeJDBC の比較

Anorm に影響を受けている ScalikeJDBC の機能面での優位性を挙げるなら Anorm でできることができるだけでなく interpolation に SQLSyntax として bind 変数以外の SQL の部品を安全に埋め込める機能があることが最も大きいかと思います。

http://scalikejdbc.org/documentation/sql-interpolation.html

Anorm の interpolation には同等の機能は現在も存在していません(Slick の StaticQuery は #$ で外部パラメータを何でも埋めることができるようです)。他のライブラリがこういうアプローチを真似しない理由はよくわかりませんが、ある程度こういうサポートがないと join クエリをたくさん書いたりする場合にかなりしんどいのではないかなと思います。少なくとも Scala 2.9 時代の ScalikeJDBC はそこがつらいなと自分でも感じていました。

逆に Anorm にあって ScalikeJDBC にないものを挙げるとすれば、あの Parser API かなと思います。

ドキュメントの写経

そもそもちゃんと比較したことがなかったので、思い立って Anorm のドキュメントを ScalikeJDBC で書いてみることにしました。久しぶりに Anorm のドキュメントを眺めてみましたが、2.3.x のものは同じページの中にコピペらしき重複があったりしますね。2.4.x ではそこは直っていましたが。

https://www.playframework.com/documentation/2.3.x/ScalaAnorm

https://www.playframework.com/documentation/2.4.x/ScalaAnorm

書いてみましたが... Anorm 固有の事情や制限のための例が多く、差を出しづらかったので... 途中でやめてしまいました。興味があればどなたかやってみてください。しかし、今となっては interpolation がないコードを書くのは結構つらいですね。

project/build.properties

sbt.version=0.13.7

project/plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0")
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")

build.sbt

scalaVersion := "2.11.5"
libraryDependencies ++= Seq(
  "org.scalikejdbc" %% "scalikejdbc"       % "2.2.4",
  "com.h2database"  %  "h2"                % "1.4.185",
  "ch.qos.logback"  %  "logback-classic"   % "1.1.2"
)
scalariformSettings

src/main/scala/Example.scala

import scalikejdbc._

object Example extends App {

  // initialize JDBC driver & connection pool
  Class.forName("org.h2.Driver")
  ConnectionPool.singleton("jdbc:h2:mem:hello;MODE=PostgreSQL", "user", "pass")
  implicit val session = AutoSession

  sql"create table City (id serial not null primary key, name varchar(100), country varchar(100))".execute.apply()
  sql"create table Country (Code varchar(3) not null primary key, Name varchar(100), Population bigint)".execute.apply()
  sql"create table CountryLanguage (id serial not null primary key, Language varchar(100), CountryCode varchar(3) references Country(Code))".execute.apply()
  sql"create table prod (id varchar(10) not null primary key, name varchar(100), price float)".execute.apply()
  sql"create table tbl (str_arr array)".execute.apply()
  sql"create table item (id varchar(10) not null primary key, last_modified timestamp)".execute.apply()
  sql"create table books (title varchar(100), author varchar(100))".execute.apply()
  sql"create table test (id varchar(10) not null primary key, cat varchar(10), a varchar(10), b varchar(10), c varchar(10), colA varchar(10), colB varchar(10))".execute.apply()

  // ----------------------------------------------
  // Anorm Documentation Examples with ScalikeJDBC
  // https://www.playframework.com/documentation/2.3.x/ScalaAnorm
  // ----------------------------------------------

  {
    /*
import anorm._
import play.api.db.DB
DB.withConnection { implicit c =>
  val result: Boolean = SQL("Select 1").execute()
}
     */
    val result: Boolean = DB.autoCommit { implicit s =>
      SQL("select 1").execute.apply()
    }
  }

  {
    /*
val result: Int = SQL("delete from City where id = 99").executeUpdate()
     */
    val result: Int = SQL("delete from City where id = 99").update.apply()
  }

  {
    /*
val id: Option[Long] =
  SQL("insert into City(name, country) values ({name}, {country})")
  .on('name -> "Cambridge", 'country -> "New Zealand").executeInsert()
     */
    val id: Long =
      SQL("insert into City(name, country) values ({name}, {country})")
        .bindByName('name -> "Cambridge", 'country -> "New Zealand")
        .updateAndReturnGeneratedKey
        .apply()
  }

  {
    /*
import anorm.SqlParser.str
val id: List[String] =
  SQL("insert into City(name, country) values ({name}, {country})")
  .on('name -> "Cambridge", 'country -> "New Zealand")
  .executeInsert(str.+) // insertion returns a list of at least one string keys
     */
    // Although ScalikeJDBC 2.2 doesn't support multiple generated keys, it's possible to specify generated key to be returned
    val id: Long = SQL("insert into City(name, country) values ({name}, {country})")
      .bindByName('name -> "Cambridge", 'country -> "New Zealand")
      .updateAndReturnGeneratedKey("id")
      .apply()
  }

  {
    /*
import anorm.{ SQL, SqlParser }
val code: String = SQL(
  """
    select * from Country c
    join CountryLanguage l on l.CountryCode = c.Code
    where c.code = {countryCode}
  """)
  .on("countryCode" -> "FRA").as(SqlParser.str("code").single)
     */
    val code: Option[String] = SQL("""
      select * from Country c
      join CountryLanguage l on l.CountryCode = c.Code
      where c.code = {countryCode}
      """).bindByName('countryCode -> "FRA").map(rs => rs.get[String]("code")).single.apply()
  }

  {
    /*
// Parsing column by name or position
val parser =
  SqlParser(str("name") ~ float(3) map {
    case name ~ f => (name -> f)
  }
val product: (String, Float) = SQL("SELECT * FROM prod WHERE id = {id}").
  on('id -> "p").as(parser.single)
     */
    val parser = (rs: WrappedResultSet) => rs.get[String]("name") -> rs.get[Float](3)
    val product: Option[(String, Float)] = SQL("SELECT * FROM prod WHERE id = {id}")
      .bindByName('id -> "p").map(parser).single.apply()
  }

  {
    /*
val name = "Cambridge"
val country = "New Zealand"
SQL"insert into City(name, country) values ($name, $country)"
     */
    val (name, country) = ("Cambridge", "New Zealand")
    sql"insert into City(name, country) values ($name, $country)"
    /*
val lang = "French"
val population = 10000000
val margin = 500000
val code: String = SQL"""
  select * from Country c
    join CountryLanguage l on l.CountryCode = c.Code
    where l.Language = $lang and c.Population >= ${population - margin}
    order by c.Population desc limit 1"""
  .as(SqlParser.str("Country.code").single)
     */
    val lang = "French"
    val population = 10000000
    val margin = 500000
    val code: Option[String] = sql"""
      select * from Country c
        join CountryLanguage l on l.CountryCode = c.Code
        where l.Language = $lang and c.Population >= ${population - margin}
        order by c.Population desc limit 1"""
      .map(_.get[String]("Country.code")).single.apply()
  }

  {
    /*
// Create an SQL query
val selectCountries = SQL("Select * from Country")
// Transform the resulting Stream[Row] to a List[(String,String)]
val countries = selectCountries().map(row =>
  row[String]("code") -> row[String]("name")
).toList
     */
    val selectCountries = sql"Select * from Country"
    val countries = selectCountries.map(rs => rs.get[String]("code") -> rs.get[String]("name")).toList.apply()
    /*
// First retrieve the first row
val firstRow = SQL("Select count(*) as c from Country").apply().head
// Next get the content of the 'c' column as Long
val countryCount = firstRow[Long]("c")
     */
    val countryCount: Long = sql"Select count(*) as c from Country".map(_.long("c")).single.apply().get
  }

  {
    /*
// With default formatting (", " as separator)
SQL("SELECT * FROM Test WHERE cat IN ({categories})").
  on('categories -> Seq("a", "b", "c")
    */
    val categories = Seq("a", "b", "c")
    sql"SELECT * FROM Test WHERE cat IN (${categories})"
    /*
// With custom formatting
import anorm.SeqParameter
SQL("SELECT * FROM Test t WHERE {categories}").
  on('categories -> SeqParameter(
    values = Seq("a", "b", "c"), separator = " OR ",
    pre = "EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name=",
    post = ")"))
    */
    val pre = sqls"EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name="
    val condition = sqls.joinWithOr(categories.map(c => sqls"$c"): _*)
    val post = sqls")"
    sql"SELECT * FROM Test t WHERE ${pre}${condition}${post}"
  }

  {
    /*
import anorm.SQL
import anorm.SqlParser.{ scalar, * }
// array and element parser
import anorm.Column.{ columnToArray, stringToArray }
val res: List[Array[String]] =
  SQL("SELECT str_arr FROM tbl").as(scalar[Array[String]].*)
     */
    val res = sql"SELECT str_arr FROM tbl".map(_.get[java.sql.Array]("str_arr")).list.apply()
  }

  // Batch update
  {
    /*
import anorm.BatchSql
val batch = BatchSql(
  "INSERT INTO books(title, author) VALUES({title}, {author}",
  Seq(Seq[NamedParameter](
    "title" -> "Play 2 for Scala", "author" -> Peter Hilton"),
    Seq[NamedParameter]("title" -> "Learning Play! Framework 2",
      "author" -> "Andy Petrella")))
val res: Array[Int] = batch.execute() // array of update count
     */
    val paramsList = Seq(
      Seq('title -> "Play 2 for Scala", 'author -> "Peter Hilton"),
      Seq('title -> "Learning Play! Framework 2", 'author -> "Andy Petrella")
    )
    sql"INSERT INTO books(title, author) VALUES({title}, {author})"
      .batchByName(paramsList: _*).apply()
  }

  // Edge cases
  {
    /*
// Wrong #1
val p: Any = "strAsAny"
SQL("SELECT * FROM test WHERE id={id}").on('id -> p) // Erroneous - No conversion Any => ParameterValue
// Right #1
val p = "strAsString"
SQL("SELECT * FROM test WHERE id={id}").on('id -> p)
*/

    val p: Any = "strAsAny"
    // ScalikeJDBC binds params with their actual types
    SQL("SELECT * FROM test WHERE id = {id}").bindByName('id -> p)
      .toMap.list.apply()

    /*
// Wrong #2
val ps = Seq("a", "b", 3) // inferred as Seq[Any]
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on('a -> ps(0), // ps(0) - No conversion Any => ParameterValue
    'b -> ps(1), 'c -> ps(2))
// Right #2
val ps = Seq[anorm.ParameterValue]("a", "b", 3) // Seq[ParameterValue]
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on('a -> ps(0), 'b -> ps(1), 'c -> ps(2))
*/
    val ps = Seq("a", "b", 3) // inferred as Seq[Any]
    SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}")
      .bindByName('a -> ps(0), 'b -> ps(1), 'c -> ps(2))
      .toMap.list.apply()

    /*
// Wrong #3
val ts = Seq( // Seq[(String -> Any)] due to _2
  "a" -> "1", "b" -> "2", "c" -> 3)
val nps: Seq[NamedParameter] = ts map { t =>
  val p: NamedParameter = t; p
  // Erroneous - no conversion (String,Any) => NamedParameter
}
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").on(nps :_*)
// Right #3
val nps = Seq[NamedParameter]( // Tuples as NamedParameter before Any
  "a" -> "1", "b" -> "2", "c" -> 3)
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on(nps: _*) // Fail - no conversion (String,Any) => NamedParameter
*/
    val ts = Seq( // Seq[(String -> Any)] due to _2
      "a" -> "1", "b" -> "2", "c" -> 3)
    SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").bindByName(ts.map { case (k, v) => Symbol(k) -> v }: _*)
      .toMap.list.apply()

    /*
import anorm.features.anyToStatement
val d = new java.util.Date()
val params: Seq[NamedParameter] = Seq("mod" -> d, "id" -> "idv")
// Values as Any as heterogenous
SQL("UPDATE item SET last_modified = {mod} WHERE id = {id}").on(params:_*)
 */
    val (mod, id) = (new java.util.Date(), "idv")
    sql"UPDATE item SET last_modified = ${mod} WHERE id = ${id}".update.apply()

  }

  {
    /*
case class SmallCountry(name:String)
case class BigCountry(name:String)
case class France
val countries = SQL("Select name,population from Country")().collect {
  case Row("France", _) => France()
  case Row(name:String, pop:Int) if(pop > 1000000) => BigCountry(name)
  case Row(name:String, _) => SmallCountry(name)
}
*/
    sealed trait Country
    case class SmallCountry(name: String) extends Country
    case class BigCountry(name: String) extends Country
    case object France extends Country

    val queryResults = sql"Select name,population from Country"
      .map { rs => rs.get[String]("name") -> rs.get[Int]("population") }
      .list.apply()
    val countries: Seq[Country] = queryResults.map {
      case ("France", _) => France
      case (name, pop) if pop > 1000000 => BigCountry(name)
      case (name, pop) => SmallCountry(name)
    }

  }

  // Using for-comprehension
  {
/*
import anorm.SqlParser.{ str, int }
val parser = for {
  a <- str("colA")
  b <- int("colB")
} yield (a -> b)
val parsed: (String, Int) = SELECT("SELECT * FROM Test").as(parser.single)
*/
     // for-comprehension is not suitable for ScalikeJDBC
     sql"SELECT * FROM Test".map(rs => rs.get[String]("colA") -> rs.get[Int]("colB")).list.apply()
  }

}

specs2 で unresolved dependency: org.scalaz.stream#scalaz-stream_2.11;0.5a: not found

こんな感じのエラーになってググってここにたどり着いたでしょうか?

[info] Resolving org.scalaz.stream#scalaz-stream_2.11;0.5a ...
[warn]  module not found: org.scalaz.stream#scalaz-stream_2.11;0.5a
[warn] ==== local: tried
[warn]   /Users/k-sera/.ivy2/local/org.scalaz.stream/scalaz-stream_2.11/0.5a/ivys/ivy.xml
[warn] ==== public: tried
[warn]   https://repo1.maven.org/maven2/org/scalaz/stream/scalaz-stream_2.11/0.5a/scalaz-stream_2.11-0.5a.pom
[info] Resolving jline#jline;2.12 ...
[info] downloading https://repo1.maven.org/maven2/org/specs2/specs2_2.11/2.4.4/specs2_2.11-2.4.4.jar ...
[info]  [SUCCESSFUL ] org.specs2#specs2_2.11;2.4.4!specs2_2.11.jar (56915ms)
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  ::          UNRESOLVED DEPENDENCIES         ::
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  :: org.scalaz.stream#scalaz-stream_2.11;0.5a: not found
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]
[warn]  Note: Unresolved dependencies path:
[warn]      org.scalaz.stream:scalaz-stream_2.11:0.5a
[warn]        +- org.specs2:specs2_2.11:2.4.4 (/Users/k-sera/tmp/zzz/build.sbt#L1-2)
[warn]        +- default:zzz_2.11:0.1-SNAPSHOT
sbt.ResolveException: unresolved dependency: org.scalaz.stream#scalaz-stream_2.11;0.5a: not found

ということで specs2 は2.4.2 -> 2.4.3 のマイナーアップデートから突然 Maven Central に存在しない scalaz-stream に依存するようになりました・・というか、これまでの specs2 と同等のものは specs2-core となり、specs2 という artifact はより多くのモジュールを含むものになりました。

specs2 のバージョンを上げて上記のようなエラーになったら "org.specs2" %% "specs2""org.specs2" %% "specs2-core" に変えましょう。この記事を読んでいる人は、それで問題ないはずです。

2015/03/12 追記

specs2 3.0 からは specs2-core であっても scalaz-stream に依存するようになったようです(specs2-common が依存しているので)。ということで全ての specs2 ユーザの方は 3.0 に上げるタイミングから Scalaz の bintry repository を resolvers に追加する必要があります。

resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases"

というか Maven Central にあげてほしいですよね。+1 しましょう、日本からも。

福岡に行ってきた

9 月末に予定していた広島への帰省のチケットをそろそろとろうかと思っていたところ 10/10 にヌーラボさん 10 周年の NUCON というイベントがあると知ったので、良い機会だということで少し後ろにずらして、一日だけ福岡に行ってみました。

http://nucon-10th.nulab-inc.com/

ヌーラボさんのサービスといえば Backlog、Cacoo、Typetalk の三つですね。

中でも Typetalk はバックエンドでは Scala、Play Framework などを使っているということで

https://twitter.com/search?f=realtime&q=typetalk%20scala&src=typd

プロダクトオーナー兼メイン開発者の @ussy00 さんとも仲良くさせていただいているし、個人的にも応援しています。ScalikeJDBC も使ってくださっているそうで、ありがとうございます。

GitHub で Play Framework を使った OAuth2 Provider 実装を公開されているので、Scala に興味がある方は見てみるとよいのではないでしょうか。 https://github.com/nulab/scala-oauth2-provider

NUCON 本編では @tksmd さんのプレゼンにあった「何をやるかももちろん大事だけど、誰とやるのか、どんなチームにしたいのかがもっと大事」というのはとても共感するところがありました。

また「新しい何か」としてこの日にヌーラボさんが提供する各サービスの稼働状況を確認できるサービスがお披露目になりました。

https://status.nulab-inc.com/

こちらの開発合宿でつくられたものが正式にサービスとして公開とのことでした。golang で実装されているそうです。だから、この名前か。 http://nulab-inc.com/ja/blog/nulab/developers-camp-in-fukuoka/

アフターパーティでは、社員の方々の家族も来られていて、小さい子供のためのキッズスペースも考慮されているのがよかったですね。また、皆さん音楽好きのようで、楽器を演奏したり、爆音で音楽を楽しんでいたのが印象的でした。


さて、ここから先は技術と全く関係ないですが。。本当は別のサービスを使おうかと思ったのですが、Tweet の埋め込みなどはてなブログの方がやりやすかったので、、結局ここに書くことにしました。

まず、お昼くらいに博多駅に着いたのですが、昼食は @kaorunix さんのオススメで天神駅前の大砲ラーメンへ。とても美味しかったです。本場のラーメンは美味いし、東京に比べて安いですね。

そして、夜はアフターパーティに少しおじゃました後で、天神駅前の角屋で一人飲みしました。角屋がどんな店かはこちらを見てもらえるとわかると思います。

http://www.ensen24.jp/fukuoka/2010/08/post-134.php

ビールもつまみも全部食券を買って頼むスタイルなんですが、店員さんを呼んだり、料理を持ってきてもらうのを待たなくていいし、とてもよかったです。立ち飲み屋さんだと店員さんがなかなかつかまらなかったり、料理が出るのに時間かかったりとかたまにありますが、このスタイルにすればそういう問題は起きなさそうです。東京でもこういう店が増えてほしいですね。

備え付けのテレビで日本代表サッカー中継が流れていて、一人で来ている人は何となく観てたりしましたが、私はちょうど東京でやっていた Scala 初心者向け勉強会 のタイムラインが面白かったので iPhoneTwitter を見ていました。この店ではそういう客も別に浮かないというか、客はそれぞれ自分の好きなようにしているし、さくっと飲んで帰る人が多くて隣の人もどんどん入れ替わるので居心地よかったです。

もう一カ所くらいどこか、福岡といえばもつ鍋かラーメンかなと思いましたが「一人だし、もつ鍋ということもなかろう」ということで昼に続きもう一軒ラーメンに行くことにしました*1

食べログのランキングを参考に、そこそこ駅から離れているものの泊まっているホテルと同じ方面だったので、元祖長浜屋*2。ここは一杯 500 円。安い。もうおなかいっぱいだったので替え玉できなかったけど、普通に空腹で来ていたら替え玉していたと思います。美味かった。

この長浜屋へ徒歩で往復して一旦ホテルに戻ったところ、アフターパーティと角屋でそこそこ飲んでいたこともあり、そのまま寝てしまいました*3

翌日(今日ですが)は 6 時に目が覚めたので、ホテルの朝食券で少し軽食を食べた後、もう一カ所くらいどこかに行こうと調べていたら、一蘭本社総本店が歩いて 15 分くらいの場所にあり、なんと 24 時間営業で早朝でも開いていたので博多駅に行く前に立ち寄りました。

あのカウンターはちょっと苦手ではあるんですが、味はとても美味しかったです。

ということで、博多・天神はとても楽しかったです。今度来るときは屋台など行ってみたいところです。

最後にヌーラボさん 10 周年おめでとうございました!

*1:屋台でもつ煮込みとかもありえたのかもしれないけど、どこ行けばいいかよくわからなかったので、、

*2:そういえば、一文字変えただけの名前の店がすぐそばにあったりして笑ってしまいました

*3:まだ 22 時前くらいでしたが

Scalate の公式サイトが GH pages に移行

Scala の老舗テンプレートエンジンで私自身 Skinny Framework のコアなコンポーネントとして利用している Scalate のウェブサイト http://scalate.fusesource.org これが閉じることになったらしく GitHub pages に移行しました。今日になってアクセスしてみると http://scalate.fusesource.org はもはやアクセスできなくなっていてました。一定期間リダイレクトするとか・・そういうのないですか。そうですか。

https://github.com/scalate/scalate/pull/60

The box hosting http://scalate.fusesource.org is going to be shutdown very soon (ideally today :-) ). Could someone merge this PR and also grab the gh-pages branch at https://github.com/janstey/scalate/tree/gh-pages so the website will be hosted at http://scalate.github.io/scalate ?

こんな記事もありましたが

Scalate 1.7を試してみる - Starlight

Scala 2.11 対応の Scalate 1.7.0 が Scalatra 2.3.0 のタイミングに合わせて @rossabaker さんのリードによりリリースされたものの、公式サイトは放置されていて最新バージョンは 1.6.1 のままでした。organization(Maven でいう groupId)も変わっているというのに...

今回の公式サイト移行をみて、これはいい機会とばかりにウェブサイトを 1.7.0 対応にする Pull Request をして

Update GH page for Scalate 1.7.0 by seratch · Pull Request #61 · scalate/scalate · GitHub

日本のみなさんに :+1: をお願いしたところ

無事マージされました。ありがとうございます!

ということで Scalate の公式サイトはこちらになりました。

Scalate

Skinny Framework からのリンクも更新しておきました。

https://github.com/skinny-framework/skinny-framework.github.io/commit/2c0c7fcfc0c739c761b72433a12db0b5d937cce4

最近は Scala といえば Play で Play といえば Twirl ですが、Scalate もなかなか便利ですよ。ぜひ皆さん使ってみてください。

ScalaMatsuri ふりかえり(今更)

何となく ScalaMatsuri のふりかえりでもしてみようかという気分になりました。自分の中での整理という感じなので、あまり面白みはないかもしれません。

Scala と私

というところで見直してみたけど、私の Scala への関わり方は以下のスライドに全てあらわれているので、その点はもう補足の必要がないですね。私の活動に興味を持っていただいていた方にとっては、やってきたことそのままであることがおわかりいただけるかと思いますが、この機会に明文化できて自分としては一区切りついたというか、すっきりした感はあります。

スライド

もう少し踏み込んだ話

もう少し踏み込んで言うと、もしも Scala 使うようになって前より仕事が楽になったり、楽しくなっていないとしたらそれは明らかに間違っているというか、少なくとも今いるメンバー、やろうとしてることとのミスマッチがあるわけなので「頑張って FP の勉強しなきゃ」「コンパイル早くならないかな」の前に考えるべきことがあるのでは?と思ったりしています。

もっと直球で言えば、何となく他の選択肢を検討することを嫌ってはいけないということです。それを考えてもなお Scala ということだったのかという。そして既に Scala でやっているのならば、その中でどんどんベストプラクティスを見つけていって、それをコミュニティで共有したいですよねという。まだ若いコミュニティですし。

Scala を使って本番ロンチしました!」という事例が多く出てくることはすばらしいのですが「ぶっちゃけ困ってること結構あるでしょ」「これから持続していく必要があるわけだけど、大丈夫そうですか」というのが私の最近の関心事です。もちろん「うちは万全!」という人たちもいると思うので、そういう人にぜひコミュニティでもっと目立ってもらえるとよいかなと。怖いとか怖くないとかはそろそろいいのでもっと現実の話をしたいですね。

そして、私も微力ながら可能な限りそれを解決することに貢献していきたいと思っていますが、OSS の開発という面では特定の納期に合わせるというものでもないですし、個別の案件としては色々あるというのが実際のところかなと思います。

英語プレゼンの件

英語発表の件でスタッフの方々にお手間をおかけしたことは申し訳なかったですが、長期的には「そんなこともあったね」とふりかえることができるだろうと思っているので、後悔はしていません。ただ、私のトークに満足いただけなかった方に対しては率直に私の力不足を詫びたいと思いますし、発表内容だけでなく運営としても改善点はあっただろうとも思います。

一方で、これはあくまで私個人の意見として言わせていただきますが、コミュニティは誰かが与えるものではなくみんなで育てていきたいと思っていますし、私個人としてはそういう場所こそを大事にしたいということを感じた出来事ではありました。

これからの Scala コミュニティ

Scala が好きな人だけで集まるのも楽しいですが、そもそも今でもオブジェクト指向と関数型の交差点として混沌としている面白さがある界隈です。もし「正直 Scala 自体はそんなに好きじゃないんだけど、Scala に関連したアレが便利すぎるから Scala 界隈に絡んでいる」みたいな人たちが出てきて、もっと多様性のあるコミュニティになったりすると、より一層面白くなるのではないかと思います。Spark とかはその兆しかもしれません。

日本のコミュニティが世界に閉じていないというのも一つの目標ですが、ふとそういう未来も見てみたいと妄想しました。