Microsoft TranslatorをScalaから使ってみた

まずまとめ

RubyGoScalaで作ってみて、一番楽に感じたのはRubyだったかな(ほぼ生粋のPHPerなもので)。
慣れればもっといい書き方がありそう。
記述量的にはScalaが少なかったんだけど(.sbtファイルを含めると別)、一番悩んだのはScalaかも(多少なりともScalaらしい書き方をしようとしたため)。
Goはパフォーマンスは良さそうなんだけど、今回のようなループ回し続けて、時々問い合わせて出力する、という流れだと体感に大差はなさそうな気がする。
それよりも意外に簡単に書けたのが印象的でした。

追記。
Goのコンパイル速度のありがたさをScala版を作ってみて把握。
さすがにGo言語の目的の一つだなぁと。
追記終わり。

成果物のサイズは、
Scala(Translator-assembly-0.0.1.jar) => 8.65MB
Go(translator.exe) => 3.76MB
Ruby => 3.64KB (スクリプト単体なので当たり前)
でした。

以下、Scalaでの実装

src/main/scala/Translator.scala

import dispatch._, Defaults._
import scala.util.parsing.json.JSON
import java.util.Date
import scala.annotation.tailrec

 
object Translator {
  def main(args: Array[String]) {
    println("input English word")
    println("'!' to quit")

    @tailrec
    def readToTranslate(accessTokenMessage:AccessTokenMessage):Unit = {
      val line = Console.readLine("from: ")
      line match {
        case "!" => ()
        case word if word.trim.length > 0 => {
          val atm = AccessTokenMessage(Option(accessTokenMessage))
          println("to: " + translate(word.trim, atm))
          readToTranslate(atm)
        }
        case _ => {
          println(">> [empty]")
          readToTranslate(accessTokenMessage)
        }
      }
    }

    readToTranslate(AccessTokenMessage.empty)
  }

  def translate(word:String, atm:AccessTokenMessage):String = {
    MicrosoftTranslator.translate(word, atm.accessToken)
  }
}

object MicrosoftTranslator {
  def MsTransLatorPrimaryKey      = "Your Primary Key"
  def MsTransLatorClientId        = "Your Client Id"
  def MsTransLatorClientSecret    = "Your Client Secret"
  def MsTransLatorUrl             = "http://api.microsofttranslator.com/V2/Http.svc/Translate"
  def MsTransLatorScope           = "http://api.microsofttranslator.com"
  def MsTransLatorAccessTokenUrl  = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13"
  def MsTransLatorGrantType       = "client_credentials"

  // Dispatch http://dispatch.databinder.net/Dispatch.html
  // url(MsTransLatorAccessTokenUrl)は以下のようにも書ける。
  def authSecureHost = host("datamarket.accesscontrol.windows.net").secure
  def authSecureRequest = authSecureHost / "v2" / "OAuth2-13"

  // 新しいアクセストークンを取得する
  def newAccessTokenMessage: Map[String, String] = {
    val params = Map(
      "client_id"     -> MsTransLatorClientId,
      "client_secret" -> MsTransLatorClientSecret,
      "scope"         -> MsTransLatorScope,
      "grant_type"    -> MsTransLatorGrantType
      )
    val req = url(MsTransLatorAccessTokenUrl) << params
    val contents = Http(req OK as.String)
    val jsonResult = JSON.parseFull(contents())
    jsonResult.get.asInstanceOf [Map[String, String]]
  }

  def translate(word:String, accessToken:String):String = {
    val headerValue = "Bearer " + accessToken
    val headerParams = Map("Authorization" -> headerValue)
    val params = Map("text" -> word, "from" -> "en", "to" -> "ja")
    val req = url(MsTransLatorUrl) <<? params <:< headerParams
    val contents = Http(req OK as.xml.Elem)
    contents().text
  }
}

object AccessTokenMessage {
  def apply(message: Option[AccessTokenMessage]):AccessTokenMessage = message match {
    case Some(m) if !m.timeout => m
    case _ => new AccessTokenMessage(MicrosoftTranslator.newAccessTokenMessage)
  }

  def empty = null
}

class AccessTokenMessage(message:Map[String, String]) {
  val createTime = System.currentTimeMillis

  def accessToken = message("access_token").toString
  def expiresIn = message("expires_in").toLong

  def timeout =
    if (createTime + expiresIn * 1000 <= System.currentTimeMillis) true
    else false
}

build.sbt

import AssemblyKeys._ // put this at the top of the file

// for assembly (jarファイルを作る) http://qiita.com/items/9d19bcf16c3729d98c1b
assemblySettings

//メインクラスを明示的に指定
mainClass in assembly := Some("Translator")

name := "Translator"
 
version := "0.0.1"
 
scalaVersion := "2.10.1"

libraryDependencies += "org.scalatest" % "scalatest_2.10" % "1.9.1" % "test"

libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.6.4"

libraryDependencies ++= Seq(
  "net.databinder.dispatch" %% "dispatch-core" % "0.10.0"
)

project/plugins.sbt

resolvers += "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/"
 
// for assembly
resolvers += Resolver.url("artifactory", url("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns)

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.8.8")

あとはsbtで
compile
assembly

以下のjarファイルが作られる。
target/scala-2.10/Translator-assembly-0.0.1.jar

java -jar target/scala-2.10/Translator-assembly-0.0.1.jar
として実行。

Scala実践プログラミング―オープンソース徹底活用

Scala実践プログラミング―オープンソース徹底活用