Scalaの次世代コンパイラ
DottyでWEBフレームワークを作ってみた

2017/08/01
iwamatsu0430

岩松 竜也

株式会社ビズリーチ HRMOS事業部
2015年4月 新卒入社
入社以来ScalaによるATS開発に携わる

Scala Days CopenHagen 2017に行ってきました!

※Applicant Tracking System

Dottyって知ってます??

これです、これ

何が変わるんですか?

  • Union Type

  • traitパラメータ

  • Enum構文

  • 他にも色々…

使ってみたい…!

WEBアプリを作りたい(ギョームに還元したい)

👇

Playframework等が対応していない

👇

WEBフレームワークがない😇

じゃあ作ってみましょう💪

環境構築

コンパイラのインストール

$ brew install lampepfl/brew/dotty

sbt 0.13.15~にて

$ sbt new lampepfl/dotty.g8

環境構築

簡単に作れました

実行

$ sbt run

どんな感じで使える
フレームワークにするか…🤔

とりあえずこんなController

class UserController extends Controller {

  @GET("/users")
  def list() = action {
    OK
  }

  @GET("/users/:id")
  def detail(id: Long) = async {
    Future.successful(OK)
  }

  @POST("/users")
  def add() = action[JSON] {
    OK("hogehoge")
  }
}

お手軽な感じにしてみたい


@GET("/users")
def list() = action {
  OK
}

まずコンパイルが
通るようにしてみる

とりあえずこんな感じ

trait Controller {
  def action[A](f: implicit Request => Response): Action = ???
  def async[A](f: implicit Request => Future[Response]): Action = ???
}

f: implicit A => B ❗

新機能使っちゃいました

REPLで確認してみましょう


$ dotr

implicit function

関数の引数を暗黙的にできます

implicit function

scala 2.11.xで近い事をしようとしても…


def foo(f: String => String): String = f("a")
def foo(s: String): String = s

foo { implicit s =>
  s + "b"
}

foo {
  "b" // スコープに引数はいない
}

明示的に暗黙的にする必要あり

OKの実装

色々な使い方が考えられる…🤔


OK // 空値
OK(???) // 文字列を渡されたり…
OK(???) // JSONを渡されたり…
OK(???) // ファイル(画像など)を渡されたり…

OKの実装

こんなんでどうよ


trait Response(val status: Status) {
  def body: Body
  def headers: Map[String, String]
}

case class OK(body: Body, headers: Map[String, String]) extends Response(Status.OK)
case object OK extends Response(Status.OK) {
  ...
  def apply[A <: String | Array[Byte] | JSON](body: A): Response = body match {
    case value: String => OK(Body.StringBody(value), headers)
    case value: Array[Byte] => OK(Body.BinaryBody(value), headers)
    case value: JSON => OK(Body.JSONBody(value), headers)
  }
}

??????


def apply[A <: String | Array[Byte] | JSON](body: A): Response = body match {
  case value: String => OK(Body.StringBody(value), headers)
  case value: Array[Byte] => OK(Body.BinaryBody(value), headers)
  case value: JSON => OK(Body.JSONBody(value), headers)
}

Union Type!

Union Type, Intersection Type

「いずれかの性質を持つ型」
「いずれの性質も持つ型」
を表せる


// String、Array[Byte]、JSONのいずれかをMixinした何か
[A <: String | Array[Byte] | JSON]

// String、IntのいずれもMixinした何か
[A <: String & Int]

Union Type

Intersection Type

OKの実装

再び見てみましょう


trait Response(val status: Status) {
  def body: Body
  def headers: Map[String, String]
}

case class OK(body: Body, headers: Map[String, String]) extends Response(Status.OK)
case object OK extends Response(Status.OK) {
  ...
  def apply[A <: String | Array[Byte] | JSON](body: A): Response = body match {
    case value: String => OK(Body.StringBody(value), headers)
    case value: Array[Byte] => OK(Body.BinaryBody(value), headers)
    case value: JSON => OK(Body.JSONBody(value), headers)
  }
}

Status.OKはEnum?🤔

Enumでした

普通…?


trait Status(val enumTag: Int, val text: String) extends Enum
object Status {
  ...
  case object OK extends Status(200, "200 OK")
  ...
}

traitパラメータ❗

traitパラメータ

Scala 2.11.xだとabstract classにする必要あり


abstract class Status(val enumTag: Int, val text: String)
object Status {
  ...
  case object OK extends Status(200, "200 OK")
  ...
}

継承順を気にする必要がなくなりました 😚

ちなみにEnum構文

こういった使い方もあります。


// 起動モードのEnum
enum class Mode
object Mode {
  def values: Seq[Mode] = Seq(Test, Development, Production)
  def valueOf(value: String): Mode = value.toLowerCase match {
    case "test" => Test
    case "production" | "prod" => Production
    case _ => Development
  }

  case Test
  case Development
  case Production
}

詳しくは小田好先生のIssue

あとはアノテーション…

class UserController extends Controller {

  @GET("/users")
  def list() = action {
    OK
  }

  @GET("/users/:id")
  def detail(id: Long) = async {
    Future.successful(OK)
  }

  @POST("/users")
  def add() = action[JSON] {
    OK("hogehoge")
  }
}

めんどくさそうなので、先にサーバ処理を実装💪

サーバ処理

ざっくりとこんな感じで処理

サーバ処理

エラーが出て来るケースが多い

エラーが出て来るケースが多い

👇

ちゃんと原因を蓄積したい

👇

(Scalaz | Cats).Validation
使おう!

▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂

依存関係が解決できない😇

sbtプラグインに答えが!!

libraryDependencies += ("org.scalaz" %% "scalaz-core" % "7.2.13").withDottyCompat()

既存ライブラリを使用したい場合は"withDottyCompat"を使用する必要あり

これでDottyでもライブラリ(Scalaz)が使えるように!

だめでした

なんでだめだった?

なんでだめだった?

ぜんぜんわからない
俺たちは雰囲気でDottyを
使っている

implicit classがだめっぽい

🤔


def foo() = {
  import scalaz._, Scalaz._
  val s1 = "OK1".success[String] // ここで怒られる 
  val s2 = "OK2".success[String]
  s1 |@| s2
}

いったん\/で対応。。。

アノテーションを使おう

アノテーションの役割(ざっくり)

class UserController extends Controller {

  @GET("/users")
  def list() = action {
    OK
  }

  @GET("/users/:id")
  def detail(id: Long) = async {
    Future.successful(OK)
  }

  @POST("/users")
  def add() = action[JSON] {
    OK("hogehoge")
  }
}

指定したパスに紐付けて関数を呼び出すよう
Macroを使えれば…🤔

だめでした

なんでだめだった?

Macro廃止!😇

scala.metaを使いましょうね

👇

NoSuchMethodによく引っかかる(未実装😇😇😇)

Javaアノテーションを使おう

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GET {
  public String path();
}

Javaアノテーションを使おう

Cotntroller一覧を設定として渡す

👇

ClassLoaderで読み込み
アノテーションよりエンドポイントと処理を紐付け

👇

Routingができる!

※できたとは言ってない

Java最高!

まとめ

できた


  • Union Type

  • traitパラメータ

  • Enum構文

  • 他にも色々…

まとめ

うれしい


  • REPLがカラー

  • 色々賢くなってる

  • 始めやすい

まとめ

できなかった


  • 既存ライブラリのフル活用

  • Macro

まとめ

うれしくない


  • 色々なバグを踏む

  • 未実装な所を踏む

  • ドキュメントが正ではない場合がある

  • Scala 2.12.xではない(Future.unitとかない、Either)

レポジトリ

iwamatsu0430/sfw

鋭意製作中!

Thank you for Listening!