PredictionIO Meetup #2 に参加してきました

f:id:takezoe:20170708134650j:plain

先日弊社のオフィスでPredictionIO Meetup #2が開催されるとのことだったので見物に行ってきました。

d-cube.connpass.com

コミッタの菅谷さんの発表資料が公開されていたので貼っておきます。

PythonはPySparkとPy4Jがとにかくつらいという話でした。今検討しているのは学習処理だけなので、予測サーバでもPythonを使えるようにするには別途検討が必要とのこと。将来的にはPredictionIOでSpark MLlibだけじゃなくscikit-learnなども使えるようになるかもしれません。

この他にも若者によるDeeplearning4JをPredictionIOで使う話だったり(結局まだ動かせていないらしいw)、レコメンドシステムを題材にしたPredictionIOのEvaluation機能の紹介などがありました。PredictionIOのイントロダクション的なところからPython対応といったマニアックな話、機械学習初心者でも勉強になる話など幅広い内容で面白かったです。

8月に第三回を開催するそうです。LTも募集するそうなので次回は自分も何か話をしてみようかなと思っています。

Play 2.6の新機能

Play 2.6のドキュメントにある「What’s new in Play 2.6」からScalaに関する部分をざっと日本語にしてみました。

細かい部分はマイグレーションガイドも見ないとダメそうですね。こちらはかなり分量があるのでさくっと日本語にするのは厳しそう…。

グローバルステートが非推奨に

アプリケーションではplay.api.Play.current / play.Play.application()でグローバルアプリケーションにアクセスできるけど非推奨。以下のようにして禁止することもできる。

play.allowGlobalApplication=false

こうしておくとPlay.currentが例外をスローするようになる。

Akka HTTPベースのバックエンド

Akka HTTPベースのバックエンドがデフォルトになった。Nettyのバックエンドも利用可能。Nettyを使用する場合は以下を参照:

HTTP/2サポート (Experimental)

PlayAkkaHttp2Supportモジュールを使うことでHTTP/2サポートを利用可能。

lazy val root = (project in file("."))
  .enablePlugins(PlayJava, PlayAkkaHttp2Support)

デフォルトではrunコマンドでは動作しない。詳細は以下を参照:

リクエストの属性

リクエストが属性を含むようになった。属性にリクエストに関する追加の情報を格納することができる。たとえばフィルタで値をセットしておき、後続のアクションでその値を参照するといったことができる。

属性はTypedMapというタイプセーフかつイミュータブルなマップに格納される。

// Create a TypedKey to store a User object
object Attrs {
  val User: TypedKey[User] = TypedKey[User].apply("user")
}

// Get the User object from the request
val user: User = req.attrs(Attrs.User)
// Put a User object into the request
val newReq = req.addAttr(Attrs.User, newUser)

これに伴ってリクエストタグは非推奨になった。リクエストタグはリクエスト属性に置き換えることが望ましい。

Route modifierタグ

routeファイルに、routeごとのカスタム動作を指定するための"modifiers"を記述できるようになった。たとえば"nocsrf"というタグを記述しておくとCSRFフィルタが適用されなくなる。

+ nocsrf # Don't CSRF protect this route 
POST /api/foo/bar ApiController.foobar

独自のmodifierを作成することもでき、+のあとに空白で区切っていくつでもタグを記述することができる。

modifierタグはHandlerDefリクエスト属性で参照可能(これはroutesファイルで定義された他のメタデータも含んでいる)。

import play.api.routing.{ HandlerDef, Router }
import play.api.mvc.RequestHeader

val handler = request.attrs(Router.Attrs.HandlerDef)
val modifiers = handler.modifiers

TwirlテンプレートでのDI

Twirlテンプレートで@thisと記述するとコンストラクタを作成できるようになった。これによってコントローラ経由ではなくテンプレートに直接DIが可能になった。

たとえば以下のテンプレートはコントローラでは使用しないTemplateRenderingComponentというコンポーネントをDIしている。@this@()より前に記述する必要がある点に注意。

@this(trc: TemplateRenderingComponent)
@(item: Item)

@{trc.render(item)}

デフォルトではすべての@thisが記述されたテンプレートクラスには自動的に@javax.inject.Inject()が付与される。この挙動はbuild.sbtで変更できる。

// Add one or more annotation(s):
TwirlKeys.constructorAnnotations += "@java.lang.Deprecated()"

// Or completely replace the default one with your own annotation(s):
TwirlKeys.constructorAnnotations := Seq("@com.google.inject.Inject()")

コントローラではテンプレートクラスをDIして使用する。

public MyController @Inject()(indexTemplate: views.html.IndexTemplate,
                              cc: ControllerComponents)
  extends AbstractController(cc) {

  def index = Action { implicit request =>
    Ok(indexTemplate())
  }
}

フィルタの改善

以下のフィルタはデフォルトで有効。

  • play.filters.csrf.CSRFFilter
  • play.filters.headers.SecurityHeadersFilter
  • play.filters.hosts.AllowedHostsFilter

application.confでは+=でフィルタを追加することができる。

play.filters.enabled+=MyFilter

テストなどの目的でフィルタを無効にしたい場合はこんな感じで。

play.filters.disabled+=MyFilter

詳細は以下を参照:

NOTE: CSRF.formFieldのようなCSRFフォームヘルパーを使用していない既存のプロジェクトはおそらくPUTおよびPOSTリクエストが"403 Forbidden"になるはず。この挙動をチェックするには<logger name="play.filters.csrf" value="TRACE"/>logback.xmlに追加する。また、もしlocalhost以外でPlayアプリケーションを実行している場合、AllowedHostsFilterでアクセス元のホスト名/IPアドレスを設定する必要がある。

gzipフィルタ

gzipフィルタを使っている場合、独自のフィルタを書く代わりにapplication.confで適用するContentTypeを指定できるようになった。

play.filters.gzip {

    contentType {

        # If non empty, then a response will only be compressed if its content type is in this list.
        whiteList = [ "text/*", "application/javascript", "application/json" ]

        # The black list is only used if the white list is empty.
        # Compress all responses except the ones whose content type is in this list.
        blackList = []
    }
}

JWTクッキー

セッションとFlashのクッキーにデフォルトでJSON Web Tokenを使用するようになった。

詳細は以下を参照:

Logging Marker API

play.Loggerplay.api.LoggerでSLF4Jのマーカーがサポートされた。

import play.api._
logger.info("some info message")(MarkerContext(someMarker))

これによってマーカーを暗黙的に渡すことが可能になる。たとえばLogstash Logback Encoderを使う場合、こんなトレイトを定義しておき、

trait RequestMarkerContext {

  implicit def requestHeaderToMarkerContext(request: RequestHeader): MarkerContext = {
    import net.logstash.logback.marker.LogstashMarker
    import net.logstash.logback.marker.Markers._

    val requestMarkers: LogstashMarker = append("host", request.host)
      .and(append("path", request.path))

    MarkerContext(requestMarkers)
  }

}

コントローラで使用すると、以下のような感じで異なるExecutionContextを使用するFutureにも持ち回ることができる。

def asyncIndex = Action.async { implicit request =>
  Future {
    methodInOtherExecutionContext()
  }(otherExecutionContext)
}

def methodInOtherExecutionContext()(implicit mc: MarkerContext): Result = {
  logger.debug("index: ") // same as above
  Ok("testing")
}

マーカーコンテキストは"tracer bullet"スタイルのロギング(明示的にログレベルを変更せずに特定のリクエストのログを出力したい場合)に便利。たとえば特定の条件の場合のみマーカーを追加することができる。

trait TracerMarker {
  import TracerMarker._

  implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
    val marker = org.slf4j.MarkerFactory.getDetachedMarker("dynamic") // base do-nothing marker...
    if (request.getQueryString("trace").nonEmpty) {
      marker.add(tracerMarker)
    }
    marker
  }
}

object TracerMarker {
  private val tracerMarker = org.slf4j.MarkerFactory.getMarker("TRACER")
}

class TracerBulletController @Inject()(cc: ControllerComponents)
  extends AbstractController(cc) with TracerMarker {
  private val logger = play.api.Logger("application")

  def index = Action { implicit request: Request[AnyContent] =>
    logger.trace("Only logged if queryString contains trace=true")

    Ok("hello world")
  }
}

logback.xmlで以下のTurboFilterでログをトリガーできる。

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
  <Name>TRACER_FILTER</Name>
  <Marker>TRACER</Marker>
  <OnMatch>ACCEPT</OnMatch>
</turboFilter>

詳細は以下を参照。

また、マーカーを使ったロギングについては以下を参照:

Configurationの改善

play.api.Configurationにカスタム型で読み取るための新しいメソッドが追加された。任意の型に対応したimplicitなConfigLoaderを使用することができる。詳細は以下を参照:

セキュリティログ

Playのセキュリティに関するオペレーションにはsecurityマーカーが追加され、セキュリティチェックに失敗するとWARNレベルでログが出力されるようになった。これによって開発者がなぜリクエストが失敗したのかを把握できるようになった。Play 2.6ではセキュリティフィルタがデフォルトで有効になったため、これは重要なことである。

securityマーカーを通常のログと分けてトリガーしたりフィルタすることができる。たとえばsecurityマーカーがセットされているログを無効にするにはlogback.xmlに以下の設定を追加する。

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
    <Marker>SECURITY</Marker>
    <OnMatch>DENY</OnMatch>
</turboFilter>

I18Nサポートの改善

PlayではMessagesApiLangクラスが国際化のために使用されており、フォームでのエラーメッセージの表示にも必要だったが、これまではいろいろ面倒なステップが必要だった。Play 2.6ではI18N APIがリファインされた。

MessagesActionBuilderが追加された。これはMessagesRequestMessageProviderを継承したWrappedRequest)を提供する。テンプレートには1つのimplicitパラメータを定義するだけでよく、コントローラはI18nSupportを継承する必要はない。

class FormController @Inject()(messagesAction: MessagesActionBuilder, components: ControllerComponents)
  extends AbstractController(components) {

  import play.api.data.Form
  import play.api.data.Forms._

  val userForm = Form(
    mapping(
      "name" -> text,
      "age" -> number
    )(UserData.apply)(UserData.unapply)
  )

  def index = messagesAction { implicit request: MessagesRequest[AnyContent] =>
    Ok(views.html.displayForm(userForm))
  }

  def post = ...
}

この場合のdisplayForm.scala.htmlはこんな感じ。

@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)

@import helper._

@helper.form(action = routes.FormController.post()) {
  @CSRF.formField                     @* <- takes a RequestHeader    *@
  @helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
  @helper.inputText(userForm("age"))  @* <- takes a MessagesProvider *@
}

詳細以下を参照:

テストサポート

MessagesApiインスタンスの作成も改善されており、DefaultMessagesApi()DefaultLangs()をデフォルト引数で生成できるようになった。メッセージを指定したい場合は以下のようにできる。

val messagesApi: MessagesApi = {
    val env = new Environment(new File("."), this.getClass.getClassLoader, Mode.Dev)
    val config = Configuration.reference ++ Configuration.from(Map("play.i18n.langs" -> Seq("en", "fr", "fr-CH")))
    val langs = new DefaultLangsProvider(config).get
    new DefaultMessagesApi(testMessages, langs)
  }

Futureのタイムアウトとディレイのサポート

play.api.libs.concurrent.Futuresトレイトを使用する。

import play.api.libs.concurrent.Futures._

class MyController @Inject()(cc: ControllerComponents)(implicit futures: Futures) extends AbstractController(cc) {

  def index = Action.async {
    // withTimeout is an implicit type enrichment provided by importing Futures._
    intensiveComputation().withTimeout(1.seconds).map { i =>
      Ok("Got result: " + i)
    }.recover {
      case e: TimeoutException =>
        InternalServerError("timeout")
    }
  }
}

同様に指定した時間後にFutureを実行するdelayedメソッドも利用可能。詳細は以下を参照:

CustomExecutionContextとスレッドプールサイズ

CustomExecutionContextakka.actor.ActorSystemにデリゲートするカスタムExecutionContextを定義する。これはデフォルトのExecutionContextを使用するべきではない状況(たとえばデータベースアクセスやブロッキングI/Oなど)で便利。

Playのテンプレートプロジェクトを更新

PlayのダウンロードページにあるブロッキングAPI(AnormやJPAなど)を使用するサンプルテンプレートはカスタムExecutionContextを使うように変更された。たとえばJPAのサンプルではDatabaseExecutionContextを使用するようになっている。

JDBCのコネクションプールに関わるスレッドプールは、ThreadPoolExecutorを使用してコネクションプールのサイズにあわせて固定したいはず。HikariCPではコネクションプールのサイズは物理コア数の2倍+ディスクのスピンドル数に設定するべきとされている。dispatcherの設定はこんな感じ。

# db connections = ((physical_core_count * 2) + effective_spindle_count)
fixedConnectionPool = 9

database.dispatcher {
  executor = "thread-pool-executor"
  throughput = 1
  thread-pool-executor {
    fixed-pool-size = ${fixedConnectionPool}
  }
}

CustomExecutionContextの定義はこんな感じ。

@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem)
   extends CustomExecutionContext(system, "database.dispatcher")

implicitパラメータで渡す。

class DatabaseService @Inject()(implicit executionContext: DatabaseExecutionContext) {
  ...
}

WSClientの改善

PlayのWSClientスタンドアロンで利用可能なplay-wsのラッパーになった。また、play-wsで使用されているライブラリはシャードされるようになったのでNettyがSpark、Playなどが使用しているNettyとコンフリクトすることがなくなった。

また、キャッシュ実装が存在する場合はHTTP Cachingがサポートされるようになった。

詳細は以下を参照:

Play JSONの改善

タプルのシリアライズ

タプルは配列にシリアライズされる。たとえば("foo", 2, "bar")["foo", 2, "bar"]レンダリングされる。

Scala.jsサポート

Play JSON 2.6.0はScala.jsもサポートしている。

libraryDependencies += "com.typesafe.play" %%% "play-json" % version

テストの改善

play.api.testパッケージにテストのためのいくつかのユーティリティクラスが追加された。DIされるコンポーネントを使用したファンクショナルテストを簡単に行うことができるようになった。

Injecting

今まではこんな感じで書いていたものが、

"test" in new WithApplication() {
  val executionContext = app.injector.instanceOf[ExecutionContext]
  ...
}

Injectingトレイトを使って以下のように記述できるようになった。

"test" in new WithApplication() with Injecting {
  val executionContext = inject[ExecutionContext]
  ...
}

StubControllerComponents

StubControllerComponentsFactoryControllerComponentsのスタブを生成することができ、コントローラーのユニットテストで以下のように使用できる。

val controller = new MyController(stubControllerComponents())

StubBodyParser

同様にStubBodyParserFactoryBodyParserのスタブを生成することができ、コントローラーのユニットテストで使用できる。

val stubParser = stubBodyParser(AnyContent("hello"))

ファイルアップロードの改善

ファイルアップロードはTemporaryFile(テンポラリファイルシステムに保存される)を使用する。理想的な挙動は不要になったTemporaryFileは可能な限り早く削除されることだが、Play 2.5ではファイナライザを使用してGC時に削除されていた。しかし特定の条件下ではGCはタイムリーに発生しないため、バックグラウンドのクリーンアップ処理はFinalizableReferenceQueueとPhantomReferenceを使用するように変更されていた。

TemporaryFileは再度修正され、TemporaryFileの参照はTemporaryFileCreatorトレイトから来るようになり、必要に応じて実装を差し替えることができるようになった。また、StandardCopyOption.ATOMIC_MOVEを使用するatomicMoveWithFallbackメソッドが追加された。

TemporaryFileReaper

Akkaスケジューラを使用して定期的にテンポラリファイルを削除できるplay.api.libs.Files.TemporaryFileReaperが追加された。デフォルトでは無効だがapplication.confで以下のようにすることで有効にできる。

play.temporaryFile {
  reaper {
    enabled = true
    initialDelay = "5 minutes"
    interval = "30 seconds"
    olderThan = "30 minutes"
  }
}

この設定は30分以上前のファイルを削除する。reaperはアプリケーションが起動してから5分後に開始し、30秒ごとにファイルシステムをチェックする。きちんと設定しないと時間のかかっているアップロード中のファイルを削除してしまう可能性があるので注意すること。

GitBucket 4.14をリリースしました

4.14にはリポジトリのフォークに失敗する場合があるバグがあり、これを修正した4.14.1をリリース済みです。こちらをご利用ください。 https://github.com/gitbucket/gitbucket/releases/tag/4.14.1

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.14をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.14

イシュー、プルリクエストに優先度を指定可能に

イシュー、プルリクエストに優先度を指定できるようになりました。優先度はリポジトリ毎にカスタマイズ可能で、指定した優先度によってフィルタリングやソートを行うことができます。

f:id:takezoe:20170701015316p:plain

f:id:takezoe:20170701015323p:plain

サイドバーの折りたたみ

サイドバーを折りたたんだ場合もアイコンを表示し、サイドバーを開かなくても画面遷移を行えるようになりました。

f:id:takezoe:20170701015458p:plain

f:id:takezoe:20170701015503p:plain

Webフックの改善

Webフックに以下の改善を行いました。

  • gollumイベント(Wiki関連のイベント)のサポート
  • ユーザ、グループ単位でのWebフックを設定可能に

f:id:takezoe:20170701015936p:plain

--max_file_sizeオプションの追加

アップロード可能なファイルサイズを指定するための--max_file_sizeオプションが追加されました。このオプションは組み込みJettyを使用して起動する場合に以下のように指定できます。

java -jar gitbucket.war --max_file_size=10485760

また、システムプロパティgitbucket.maxFileSizeでも同様に指定することができます。gitbucket.warをサーブレットコンテナにデプロイしている場合はこちらの方法で設定するとよいでしょう。

全ての設定を環境変数またはシステムプロパティで指定可能に

gitbucket.confおよびdatabase.confで設定可能なすべての項目を環境変数もしくはシステムプロパティで指定できるようになりました。たとえばldap.mail_attributeという設定項目の場合、GitBucketは以下の順番で設定値を取得します。

  1. システムプロパティgitbucket.ldap.mail_attribute
  2. 環境変数GITBUCKET_LDAP_MAIL_ATTRIBUTE
  3. gitbucket.confldap.mail_attributeプロパティ

これによってDockerコンテナなどの環境でGitBucketを使用する場合に設定の自動化が容易になります。

プラグインのための新しい拡張ポイント

イシューおよびプルリクエストに関するイベントをフックするための拡張ポイントを追加しました。

  • accountHooks
  • issueHooks
  • pullRequestHooks
  • issueSidebars

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

型駆動開発の裏側 -IdrisのIDEモードを試してみる-

以前紹介したように、AtomにIdris用パッケージをインストールするとキーボードショートカットで様々な機能を利用できます。

takezoe.hatenablog.com

これがどのように実装されているのかが気になったので調べてみようと思い、Atomパッケージのソースコードを眺めてみました。

github.com

どうやらIdrisにはIDEモードというモードがあり、標準入出力を使用してS式でやり取りを行うIDE向けのプロトコルが存在するようです。ちょっと試してみました。

まずはIDEモードでプロセスを起動。

$ idris --ide-mode
000018(:protocol-version 1 0)

入出力は「メッセージの文字数を6桁の16進数にエンコードしたもの+S式のメッセージ」という形式で行われるようです。

こんな感じの作りかけのファイルを用意しておきます。

module Main

main : IO ()

このファイルを読み込みます。メッセージにはユニークなカウンタを含める必要があるようです。

000022((:load-file "/tmp/hello.idr") 1)

出力はこんな感じ。

0000a9(:output (:ok (:highlight-source ((((:filename "/tmp/hello.idr") (:start 1 8) (:end 1 12)) ((:namespace "Main") (:decor :module) (:source-file "/tmp/hello.idr")))))) 1)
00001e(:set-prompt "*/tmp/hello" 1)
000015(:return (:ok ()) 1)

CTRL+ALT+A相当の:add-clauseというメッセージを送信してみます。

00001B((:add-clause 3 "main") 2)

こんな感じで生成されたコードが返ってきます。

000025(:return (:ok "main = ?main_rhs") 2)

シンプルな手法ではありますが、Idris側でこのような機能を持っているので様々なエディタで同じ機能を簡単に実装できるようです。実際Atom以外にもvimEmacsSublime Text用のプラグインなども用意されています。ショートカットは異なりますが、使い慣れたエディタを使用して型駆動開発を行うことができますね。

GitBucketがScala関西Summit 2017のスポンサーになりました

f:id:takezoe:20170625015539p:plain

GitBucketがScala関西Summit 2017のブロンズスポンサーになりました!Webサイトにロゴを掲載していただいています。リンク先がGitHubのREADMEというのも寂しいので、この日のためにランディングページも用意しました!!*1

summit.scala-kansai.org

ボランタリベースのオープンソースプロジェクトで捻出できる範囲なので微力ではありますが*2、国内でのScalaの普及に少しでも貢献できればと思います。ブロンズスポンサーでもノベルティを配布できるそうなので、GitBucketステッカーを配布させていただこうと思っています。

個人でCFPも出しているので、もし採択いただけたら今年も関西に参戦しようと思います。また、水面下で進めているプロジェクトがあり*3、ちょうど時期的にScala関西Summitの頃にScalaユーザの皆さんに素晴らしいニュースをお届けできるかもしれません。こちらもご期待いただければと思います!

*1:昨年ScalaMatsuriのスポンサーした時に作ったリクルーティングページを改造したものですがw

*2:とはいえ個人レベルでもサポートできるプランが用意されているのは嬉しいところです。

*3:私はほとんどタッチしていないので「進めていただいている」が正しいのですが…。

Akka Streams用のElasticsearchコネクタを作ってみた

github.com

元々はAlpakkaにプルリクしていたのですが、取り込んでもらうのに時間がかかりそう or 取り込んでもらえるかわからない感じなので自分で使う用に別リポジトリで単独のライブラリとして公開しました。Maven Centralにもpublishしてあります。

Elasticsearchからスクロールスキャンでデータの読み込みを行うSourceと、バルクでデータの投入を行うSink(Flowもあります)を提供しています。使い方は以下のような感じです。ElasticsearchのRestClientをimplicit valで定義しておく必要があります。

implicit val client = RestClient.builder(
  new HttpHost("localhost", 9201)).build()

val f1 = ElasticsearchSource(
  "source",
  "book",
  """{"match_all": {}}""",
  ElasticsearchSourceSettings(5)
)
.map { message: OutgoingMessage[JsObject] =>
  IncomingMessage(Some(message.id), message.source)
}
.runWith(
  ElasticsearchSink(
    "sink1",
    "book",
    ElasticsearchSinkSettings(5)
  )
)

SourceはOutgoingMessage[JsObject]で出力し、SinkはIncomingMessage[JsObject]でデータを受け取ります。JSONライブラリとしてはspray-jsonを使っています(Java向けのAPIも提供しており、そちらではJacksonを使っています)。

spray-jsonのFormatを定義することで、JsObjectではなくケースクラスなどでデータを扱うこともできます。

case class Book(title: String)
implicit val format = jsonFormat1(Book)

val f1 = ElasticsearchSource
  .typed[Book](
    "source",
    "book",
    """{"match_all": {}}""",
    ElasticsearchSourceSettings(5)
  )
  .map { message: OutgoingMessage[Book] =>
    IncomingMessage(Some(message.id), message.source)
  }
  .runWith(
    ElasticsearchSink.typed[Book](
      "sink2",
      "book",
      ElasticsearchSinkSettings(5)
    )
  )

Sourceはスクロールスキャンで読み込んだドキュメントをバッファリングしており下流からpullされた分だけpushし、バッファが空になるとElasticsearchから次のページの分を取得するという動きになっています。Sinkもバッファを持っており、バッファに溜まった分をElasticsearchにバルクリクエストでインデキシングするという感じになっています。Sink側の挙動に関しては改善の余地があるかなと思っています。

第十九回 #渋谷java を開催しました

6月17日(土)に弊社オフィスで第十九回 #渋谷javaを開催しました。当日の様子や皆さんの発表内容については以下のtogetterまとめをご覧ください。

togetter.com

今回は以下の皆さんに発表していただきました。

  • セッション枠
    • hagino_takahiro 「PredictionIO」
    • shimamoto 「LocalStack -クラウドサービスのモック環境-」
    • lagénorhynque 「MP in Clojure
  • 通常枠(10分)
    • Naoki Takezoe 「最強のAltJava言語Xtend」
    • Ikuru K 「ClojureのWeb開発の歴史」
    • vertical_blank 「GraalVMの話」

発表資料は公開され次第追加していきます。

www.slideshare.net

www.slideshare.net

www.slideshare.net

slides.com

ClojureやGraalVMなど相変わらず濃い話が多く、個人的には楽しいLTばかりだったのですが、Javaの最近の動向が知りたかったのに期待していたのと違う…と感じる方や、逆にテーマが散漫なので参加しづらい…という部分もあるかもしれないと感じています。より有意義な勉強会となるよう今後の運営方法についても検討していきたいと思います。