14
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

実際に使ったScalaのバッドノウハウ

Last updated at Posted at 2015-12-18

はじめに

最近http-client-wrapperという、SkinnyのHttp ClientライブラリをラップしてCLIで使いやすくするツールを作りました。これの紹介と、ここで使ったScalaのバッドノウハウの話をします。Scala Collection探訪の予定でしたが進捗が無さ過ぎて辛いので急遽変更します。ごめんなさい。

Skinny HTTP Client

このHTTP ClientライブラリはSkinny Frameworkの一部を構成していますが、これだけ取り出して使うことができます。

特徴は簡単でシンプルなことでしょうか。私感では、ScalaのHttp Clientというと、Javaのやつをそのまま使うか、Scalaの演算子を活用(乱用?)し過ぎて分かりづらいのしかないイメージが強いですが、Skinny HTTP Clientは癖無く使いやすいです。

http-client-wrapper

Scalaは比較的Webサーバの用途で使う人が多い言語かと思います。Webサーバ開発してると、実際に立ち上げてAPIを叩いて動作テストしたいことが多いです。実際。

そういう用途で単純なものであればcurlとかwgetとか使うんですが、また良くある話として、sessionを保存したいとかになるとなかなかに面倒くさい。

というので作られたのがhttp-client-wrapperです。

sbtからScalaのREPLを召喚すると以下のように使えます。

get("myfleet.moe") // get simple
get("google.com", "q" -> "myfleet") // with args
post("hoge.com/session", "username" -> "ponkotuy", "password" -> "*****") // post form
val session = post("hoge.com/session", ("username" -> "ponkotuy") ~ ("password" -> "*****")) // post json by using json4s
session.get("hoge.com/image/1") // get with cookies
session.status // => 200 use skinny.http.Response method if not exists method
get("myfleet.moe").res // get raw Response
host = "ponkotuy.com"
get("/index.html") // use host settings
protocol = "https"
get("google.com", "q" -> "myfleet") // https protocol

Scalaくわしい人ならなんかヤバそうなことしてるなーという気がしてきますね。順番に見ていきましょう。

implicit conversion

初っ端から乱用厳禁な奴です。何故implicit conversionの乱用が悪いのかというと、一言で言うなら型安全性を弱くするからです。ScalaやJavaといった強い型付け言語がプログラマに型の記述を要求するのは、パフォーマンス面の理由の他に、型に起因するバグをコンパイル時に見つけることができるからです。implicit conversionを乱用すると型安全性を容易に破壊できます。

http-client-wrapperの起動時にこんな感じの画面が出てきます

import skinny.http._
import HttpHelper._
import Implicits._
import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._

Implicitsとかいうヤバそうな奴がいますね。コード見てみましょう。

object Implicits {
  import scala.language.implicitConversions

  implicit var host: Host = Host("localhost")
  implicit var protocol: Protocol = Protocol("http")

  implicit def toHost(str: String): Host = Host(str)
  implicit def toProtocol(str: String): Protocol = Protocol(str)
  implicit def removeSession(session: Session): Response = session.res

}

ちらっともっとヤバそうな奴も見えますがとりあえず下の3つがimplicit conversionです。

使用例でhostやprotocolという変数に文字列を突っ込んでいましたが、実はhostはHostという型を使っています。通常ではHost("ponkotuy.com")と書くのが正しいのですが、打つのは苦痛なので、文字列を自動でHostやProtocol型にimplicit conversionします。何故Host型が必要だったのかは次章で。

removeSessionはここで使われます

session.status // => 200 use skinny.http.Response method if not exists method

http-client-wrapperは自前で定義したSession classにくるんで返り値等を返します。Session classにstatus methodは付いてません。でもきっと返り値に対してskinny.http.Responseのmethodが使いたくなることもある筈です!なので暗黙変換してます。CLIではなく通常のプログラム中なら素直にresを呼びましょう。

var

varは極力使うべきではないです。しかし、パフォーマンス上の理由やimmutableだと辛いアルゴリズムも世の中には存在します。使うにしても「極力スコープが狭い範囲で」使いましょう。

スコープの広いvarはちょっとした不注意で「いつのまにか値を書き換えられている」という惨事を引き起こします。変数の値がおかしい、というバグが発生したとしましょう。よくあることだと思います。書き換え不可能なvalであれば定義した箇所を見るだけで値が特定できます。varだとその変数が書き換え可能範囲を全て調査する必要があります!

class変数として使うにしてもprivate[this]を付ければ外で値を書き換えられる心配はありませんし、小さなmethod内で使う分には数行で済むので問題が起こりにくいです。

というのがvarの考え方ですが、ここでは堂々とglobalスコープに置いてます。神をも恐れぬ所業。しかもimplicit変数。

hostやprotocolを状態として置いておき、ユーザが自由に変更できるようにしたかったため、このような作りをしています。

これが何故implicit変数かというと、getやpostに暗黙的な引数として渡すためです。implicit引数は、スコープにあるimplicit変数の中から、同じ型のものを自動で引数として使う機能です。明示的に書かなくても引数として勝手に渡っていくので暗黙的(implicit)な引数と呼ばれます。HttpWrapperというtraitを見てみましょう。

trait HttpWrapper {
  import HttpWrapper._

  def cookie: Cookie

  def get(url: String, params: (String, String)*)(implicit host: Host, protocol: Protocol): Session = {
    val req = Request(fixURL(url)).queryParams(params:_*).header("Cookie", cookie.toString)
    request(Method.GET, req)
  }
...
}

HostやProtocolをimplicit引数として受け取っていますね。このimplicit引数はfixURL関数で使われます。なお、implicit引数自体は言うほどバッドノウハウではないと考えています。

突然のprintln

classや関数といったものを多用して、プログラムを細かい部分に分けて分割統治するのは、プログラミングの基本といっていいでしょう。その目的は名前を付けて読み易くするなどありますが、再利用性を高める、というのも大きな目的の1つです。

printlnという形で強制的に標準出力される関数を考えてみます。標準出力を汚染されたくない場合、この関数は再利用できないですね。printlnはできるだけ再利用されづらい上位のレイヤーでできるように返り値を工夫すべきです。

残念ながらHttpWrapperという比較的下のレイヤーでprintlnしてるmethodが多数ありますね。これに関してはユーザビリティを優先したのではなく、純粋に面倒くさかったからです。ごめんなさい。

Scalaの世界ならprintlnよりは各種loggerを使うのが比較的マシな対応と言えるでしょう。loggerを使えば出力の形式が上位レイヤーで自由に決められます。ただしloggerライブラリへの依存が面倒だったりするので、必要性は考えるべきです。

最後に

このようにhttp-client-wrapperは様々なユーザビリティを考慮して、本来使うべきでない道具を最大限に悪用して作成されました。もしユーザビリティを損なわずに改善できそうな点があれば修正提案は大歓迎です。

あと、使う分には単純に便利なツールになるように努力したつもりです(実際本人も使ってます!)内部実装がつらいことは忘れて便利に使っていただけると幸いです。

issue、PRもお待ちしております。

あ、言い忘れてました。通常のプログラムで使うならSkinny HTTP Client使いましょう。速度をカリカリにチューニングしたい場合など、カスタム性が足りない場合以外であれば十分だと思います。

14
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?