Scala で書かれた Sinatra 風味の Web framework Scalatra

Written by @dr_taka_n at 2011/01/06 01:31 [, ]

Scala の Web framework と言えば、Lift などが有名だが、コンパクトで Sinatra 風味の Web framework と呼ばれる Scalatra も面白い。

Sinatra は、Rack をベースに、簡潔で分かりやすい記述を可能とした DSL で彩りを加えている。Scalatra は、Servlet をベースに、簡潔で分りやすい記述を可能とした DSL で彩りを加えている。

Hello Wold を表示させる Web アプリケーションは以下のように書ける。

Sinatra は、

require 'rubygems'
require 'sinatra'
get '/' do
  "Hello, World!"
end

Scalatra は、

import org.scalatra._

class ScalatraExample extends ScalatraServlet {
  get("/") {
    <h1>Hello, world!</h1>
  }
}

という感じ。

実際に動作させてみるには、scalatra/scalatra - GitHub の README や、以下のサイトなどが参考になる。

ここでは、上記の DSL での記述を可能とさせた仕組みの部分を Scalatra でどのように実装しているのかについてメモしておく。

Scalatra の骨格

Scalatra は、元々 Step という名前でデビューした。そのアイディアは、以下のブログの1ページで確認できる。

現在の Scalatra ソース群は、上記の Step クラスほどシンプルではないが、基本的な概念は変わらない。 話をシンプルにするため、上記のページのソースをベースに見ていくことにする。

見ていくといっても、Scalatra の骨格は、以下のシンプルな Step クラス(現在は ScalatraServlet)で全てになる。

(print “Me” – Step, a scala web picoframework から引用) <pre class="scala">abstract class Step extends HttpServlet { val Map = new HashMap[(String,String),(Any=>String)]() override def service(request:HttpServletRequest ,response: HttpServletResponse) { val method=request.getMethod val path=request.getRequestURI response.getWriter.println(Map(method,path)()); }

def get(path:String)(fun: => Any) = Map.put((“GET”,path),x=>fun.toString) def post(path:String)(fun: => Any) = Map.put((“POST”,path),x=>fun.toString) } </code></pre>

まず、リクエストの処理の部分は、アプリケーションを記述するクラスが継承する ScalatraServlet (上記では Step) で、普通に HttpServlet を継承し、普通に service メソッドを実装している。 この実装のレスポンスを返すところがミソとなり、Map(method,path)() で何やら文字列が生成されることになっている。

この Map は、キーが HTTP メソッドとパスの文字列のタプルとなり、値は、上記の例では、引数として Any 型を受け取り、String 型を返す関数値となっている。上記のレスポンスを返している部分の

response.getWriter.println(Map(method,path)());

では、Map(method, path)() によって、リクエストの HTTP メソッド、パスをキーに取得される関数値が評価されることになる。

Map への情報の登録は、ScalatraServlet (上記では Step) を継承したアプリケーションを記述するクラスで、HTTP メソッドに合わせて、継承元の ScalatraServlet (上記では Step) で定義されている get、もしくは、post メソッドを呼び出し、第1引数にパス、第2引数(ブロック)にそのパスがリクエストされた際の処理を記述することで行う。

(print “Me” – Step, a scala web picoframework から引用) <pre class="scala">class ScalaHello extends Step { get("/") { <form action="/post" method="POST"> <textarea name="key"/> <input type="submit"/> </form> } post("/post") { "hello, this is your input "+params("key") } } </pre>

継承元の ScalatraServlet (上記では Step) で定義されている getpost メソッドは以下の通り。

  def get(path:String)(fun: => Any) = Map.put(("GET",path),x=>fun.toString)
  def post(path:String)(fun: => Any) = Map.put(("POST",path),x=>fun.toString)

2つのパラメータリストをとり、2つ目のパラメータリストが名前渡しパラメータとなっている。カリー化と名前渡しパラメータの組み合わせにより、上記の ScalaHello のような HTTP メソッドに合わせて、パスとその処理内容を自然に記述できる構文を実現している。

以上が骨格の部分となる。現在の Scalatra はもっといろいろなことをやっており、ここでは元祖 Step のアイディア部分をベースに書いているので、正確な情報は実際のソースを参照のこと。

REPL で Scalatra の DSL の実装を擬似的に実験

HTTP メソッド + パス(URL)に対応する処理の登録と、実行の部分を擬似的に REPL で試してみることにする。

$ scala
Welcome to Scala version 2.8.0.final (Java HotSpot(TM) Server VM, Java 1.6.0_22).
Type in expressions to have them evaluated.
Type :help for more information.

>

まずは、HTTP メソッド + パスと実行する処理(関数値)を保管しておく Map を用意しておく。

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val map = mutable.Map.empty[(String, String), () => String]
map: scala.collection.mutable.Map[(String, String),() => String] = Map()

空の Map を生成した際に指定した Map の型は、

  • Key: (String, String)
    1番目の要素には、HTTP のメソッド(GET/POST etc)の文字列が入り、2番目の要素には、パス(URL)の文字列が入るタプル。
  • Value: () => String
    引数を取らずに、String 型を返す関数値。

となる。

get メソッドを実装する。

scala> def get(path: String)(f: => Any) {
     |   map += (("GET", path) -> (() => f.toString))
     | }

定義した get メソッドは、2つのパラメータリストをもつ。

1つ目のパラメータリストの引数 path は、パス(URL) の文字列をとり、2つ目のパラメータリストの引数は、引数を取らずに Any 型を返す関数値を名前渡しパラメータとしてとる。

get メソッドの処理部分で、先に定義した map を ("GET", path) のタプルをキーとして、() => f.toString の関数値を値とする要素で更新している。

定義した get メソッドを使って、処理を登録する。

scala> get("/time") {
     |   new java.util.Date
     | }

/time” のパスをリクエストすると、現在時刻が返される API をイメージしている。。

中括弧で記載した部分が、「引数を取らずに Any 型を返す」関数値となる。中括弧で書くとブロックのように見えて読み易いので中括弧を使っているが、通常の()で記載することもできる。

scala> get("/time")(new java.util.Date)

では、登録した処理を呼び出してみる。

scala> map("GET", "/time")()
res2: String = Wed Jan 05 00:40:40 JST 2011

現在の時刻を返してくれた。

目が慣れてこないと、最後の () の部分は奇異に見える。これは、map("GET", "/time") で返された関数値(引数を取らずに、String 型を返す関数値)の呼び出しを行っている。
map("GET", "/time") の戻り値を見るとわかる。

scala> map("GET", "/time")
res3: () => String = <function0>

引数を取らずに、String 型を返す関数値が返っていることがわかる。以下のように書くともっと分りやすいかもしれない。

scala> val getTimeString = map("GET", "/time")
getTimeString: () => String = <function0>

scala> getTimeString()
res4: String = Wed Jan 05 00:45:40 JST 2011

また、戻り値の時刻を見ているとわかるが、map の値は関数値で名前渡しを行っているので、get メソッドにて引数として渡した時ではなく、参照された時にはじめて評価されている。

scala> new java.util.Date
res5: java.util.Date = Wed Jan 05 01:19:58 JST 2011

scala> map("GET", "/time")()
res6: String = Wed Jan 05 01:20:10 JST 2011

Scala を触り始めた頃には、関数値の扱いに一瞬戸惑ったが、このような処理が簡潔に書けるのがオブジェクト指向言語と関数型言語のハイブリットである Scala の面白いところ。

blog comments powered by Disqus