ぽんこつ@ponkotuyです。勝手に会社のプロダクトをリファクタしたり高速化したりするだけの簡単なお仕事をしています。Scalaも楽しいけど、SQLのチューニングはもっと楽しいです。
という自己紹介をガン無視してリスト操作関数とUnderscore.jsの話をします。
続きを読むフロントエンド担当の shuji-koike です。
マーベリックに2015年1月にjoinして最初に任されたお仕事は、DSP広告システム Sphereの管理画面のリニューアルでした。
今回の記事では、管理画面のリニューアルにあたって採用したスタック(技術セット)について紹介させていただきます。
html
のテンプレートエンジンとして、haml
,slim
,jade
などがありますが、個人的に使い慣れているという理由でslim
を採用しました。
インデントレベルだけでhtml
の構造を記述できるので非常に生産性が高く、可読性も良好です。
altJSの定番ということで迷わず採用。
html
のマークアップやデザイン面のフレームワークとしてBootstrap
を採用しました。
Bootstrap
のフレームワークに乗っかることで開発・改修工数を低く抑えつつ、
先々デザインを改善することになった場合は、Bootstrap
のスキンを置き換えることで比較的簡単に改修できるという想定です。
React
など仮想DOM系のフレームワークも検討しましたが、まだ開発手法やノウハウの蓄積が十分でないと判断し、十分に枯れてきているAngularJS
を採用しました。
AngularJS
はとっつきにくい面もありますが、中〜大規模なWebアプリのフレームワークとしては良い選択肢だと思います。
フロントエンドのビルドツールとしては、gruntやgulpが一般的ですが、弊社ではmiddlemanをビルドツールとして採用しました。 middlemanはRuby on Railsのエコシステムから派生した静的サイトジェネレータです。
本来MiddlemanはコーポレートサイトやブログのようなWebサイトを構築するためのツールですが、 Ruby on Railsのアセットパイプラインのような機能 (sprockets)を利用するために使っています。
slim,coffee,sassのソースコードをhtml,js,cssに変換・難読化(Uglify)して、さらにgzip圧縮するところまで、middleman build
コマンド一発で実行できます。
また、開発時はmiddleman server
コマンドを使って、コードの修正をプレビューしながら開発できます。
今回は技術的に踏み込んだことは書けませんでしたが、今後の記事では開発手法やノウハウについて共有していきたいと思います。
2015年8月1日、関西最大級のScalaカンファレンス Scala関西 Summit 2015 が開催されました。
今回はその様子をお伝えしたいと思います。
(Twitterハッシュタグ #scala_ks でも当日の雰囲気を掴めるかと思います。)
場所は国の重要文化財に指定されている大阪市中央公会堂。
関西だけでなく、関東・九州などから参加者・スピーカーが集い、3トラックで発表が行われました。
すでにプロダクション環境でガッツリScalaを使用されている現場からの知見の共有もありましたが、
PerlやPHPなどで書かれた既存のプロダクトをScalaで書き直す取り組み、
Java中心の開発現場にどのようにしてScalaを普及・浸透させようとしているかという話も
興味深く聞かせていただきました。
特にリモート環境でのペアプログラミングにより社内でのScalaの技術向上を図るといった話が
印象に残っています。
弊社では元々Scalaを採用してプロダクトの開発を行ってきていますが、
最近はScalaで書かれたレガシー化しつつある部分を再度Scalaで書き直す取り組みを計画しており
状況は異なりますが参考になるところもありました。
Scalaの国内イベントといえば ScalaMatsuri が恒例となりつつあるようですが、
今回のように各地でイベントが開催され、盛り上がりを見せればいいな、などと思いました。
余談ではありますが、今回個人スポンサーという形で参加させていただきました。
微力ながらイベント運営にお力添えできていれば幸いです。
運営・スピーカー・スポンサー・参加者の皆様、ありがとうございました。
こんにちは 第2回はインフラ担当のあすてるが担当します。
今回はansibleでowncloudサーバを構築です。
その前にansibleとowndloudについて少し紹介します。
今回は構成管理ツールと呼ばれるansibleを使って構築したいと思います。
pythonで書かかれたエージェントがいらないツールとなっています。
簡単にいうと構築する際に書くシェルスクリプトのようなものです。
標準モジュールといわれるものがあり公式が提供しています。
ファイルをコピーしたりyumコマンドやaptコマンドを使用する際に使います。
- yum: name=vim
と書くだけでインストールすることができます。
モジュールを使用することにより冪等性が担保されます。
もう一度動作した場合は既にインストールされているのでスキップされます。
ansibleはサーバを指定して実行することが可能です。
またそれは1台だけではなく数十台同時に実行することができます。
今回のサンプルコードにはhostという名前でファイルを作成し、一台のサーバのみしか書いていませんが
[api_server] 192.168.1.[1:20]
と書くことにより平行して複数サーバを構築することができます。
一番気を付けることは冪等性です。
標準モジュールにはshellやcommandといったものがあるので全てshellモジュールで書くことも可能です。
しかしそれでは冪等性が失われる場合があるので注意しましょう。
echo hogehoge >> /etc/piyo/foo.conf
などを実行した場合どうなるでしょうか?
初期構築時は問題ないかもしれませんが2回実行した場合、foo.confにはhogehogeが2つ書かれてしまいます。
標準モジュールはたくさんありますがよく使うものは限定されていきますので出来るだけモジュールを使用しましょう。
ansible-playbookを実行した際
okかchangedという2種類のどちらかが基本的に表示されます。
okの場合、既にインストールされていたり処理が実行済みの場合です。
逆にchangedは変更があった際に実行されます。
ansibleのコツは2回目に流した際にchangedをなるべく出さないことですね。
オンラインストレージサービスを自分のサーバで構築できるオープンソースソフトです。
自前dropboxとよくいわれているみたいです。
webサーバ上でphpで動作します。
今回はowncloudということで
nginx + php-fpm + mysql という構成で構築します。
初期設定として鍵認証でsshできるようにしてsudo権限がある、
もしくはrootになってローカルにansibleを流す様にすると簡単に実行できると思います。
OSはcentos6を想定しています。
まだ改善の余地があるかと思いますがサンプルコードはこちらです。
ansible-playbookを実行し全部が流し終わると http://hostip/owncloud/ にアクセスしてowncloud初期設定ができると思います。
以上、あすてるでした。
次回も乞うご期待!
ども。プロダクトグループ所属エンジニアの、あおいの(@AoiroAoino)です。
先月25歳になりましたが、見た目年齢は30代らしいです。よろしくお願いしますmm
弊社技術ブログ一発目の記事担当になってしまったので頑張ります!
さてさて、記念すべき一発目はMongoDBとScalaの組み合わせについて書こうかなと思います。
テーマは「モデルを定義/使用する人がラクできるようにする」です。
とりあえず何も無いのもアレなので、「イベントホスティングサービスを開発する」という妄想で話を進めていきます。
モデル部分のデータ構造はざっとこんな感じで。
// Event.scala case class Event ( _id: ObjectId, title: String, start: String, end: String, place: String, owners: List[User], attendees: List[User] )
// User.scala case class User ( _id: ObjectId, name: String, phone: String )
さぁ、定義したモデル使ってもりもり開発していきましょう!
Scalaで使用できるMongoDBクライアントはいくつかあるようですが、今回はMongoDB公式の「Casbah」を使用します。
内部的にはJavaで実装されたmongo-java-driverをラップし、Scalaっぽく書けるようにしたものです。
検索結果がOption
に包まれて返ってきたり、検索クエリ構築のための内部DSLが用意されていたりするので、
Java版を直接使用するよりもScalaっぽく書けるようになっています。
色々方法はあると思いますが、今回はモデルとDBObject
の相互変換をラクにするため、
toModel
とtoDBObject
メソッドを用意していきます。
// ModelMapper.scala trait ModelMapper[A] { // Model -> DBObject def toDBObject(model: A): DBObject // DBObject -> Option[Model] def toModel(obj: DBObject): Option[A] }
// User.scala object User extends ModelMapper[User] { def toDBObject(user: User): DBObject = { DBObject( "_id" -> user._id, "name" -> user.name, "phone" -> user.phone ) } def toModel(obj: DBObject): Option[User] = { for { oid <- obj.getAs[ObjectId]("_id") name <- obj.getAs[String]("name") phone <- obj.getAs[String]("phone") } yield User(oid, name, phone) } }
ここで、DBObject
からModel
への変換は失敗する可能性があるので、結果がOption
で包まれているのがポイントです。
後々重要になるので、頭の片隅に置いておいてくださいね。
さて、これで準備は完了です!
以下のようにして使用することができます。
// 使用例 scala> val user = User(new ObjectId, "Aoino", "xxx-xxx-xxx") scala> val obj = User.toDBObject(user) // => com.mongodb.DBObject = { "_id" : { "$oid" : "559e6298ce242b655aa45de1"} , "name" : "Aoino" , "phone" : "xxx-xxx-xxx"} // 変換成功例 scala> val model = User.toModel(obj) // => Option[com.github.aoiroaoino.model.User] = Some(User(559e6298ce242b655aa45de1,Aoino,xxx-xxx-xxx)) // 変換成功したmodelからnameの取得 scala> model.map(_.name) // => Option[String] = Some(Aoino) // DBObjectが想定してる構造で無かった場合 scala> User.toModel(DBObject.empty) // => Option[com.github.aoiroaoino.model.User] = None
良さそうですね!
これでも十分に開発を進めることができるかと思います。
さて、ここからがいよいよ本番です!!
さて、このままだと新たなモデルが増えるたびにtoDBObject/toModelの定義をしなければいけません。
私は貧弱Vimmerなので、手元のVimにはIDEほどの入力補完もなければ長年蓄積されたスニペット資産もないのでとても面倒です。
なので、面倒なお仕事はコンパイラに押し付けてしまおう!ってことでマクロを使ってみることにします。1
まず最初にマクロを呼び出す側のコードを書いてしまいます。
ModelMapper
をcase class
から得られる情報で自動的に構築するので、とりあえずAutoModelMapper
って名前にしました。
// AutoModelMapper.scala object AutoModelMapper { def apply[A]: ModelMapper[A] = macro AutoModelMapperImpl.autoModelMapper_impl[A] }
次に、呼び出されるマクロを実装します。
// AutoModelMapper.scala private object AutoModelMapperImpl { def autoModelMapper_impl[A: c.WeakTypeTag](c: Context): c.Expr[ModelMapper[A]] = { import c.universe._ val tpe = weakTypeOf[A] val constructor = tpe.decls.collectFirst { case m: MethodSymbol if m.isPrimaryConstructor => m }.getOrElse(c.abort(c.enclosingPosition, s"Cannot find constructor in $tpe")) val fields = constructor.paramLists.head match { case Nil => c.abort(c.enclosingPosition, s"Constructor argument is empty in $tpe") case x => x } val toDBObjectParams: List[Tree] = fields.map { field => val name = field.name.toTermName val decoded = name.decodedName.toString q"$decoded -> model.$name" } val toModelParams: List[Tree] = fields.map { field => val name = field.name.toTermName val decoded: String = name.decodedName.toString val returnType = tpe.decl(name).typeSignature q"""obj.getAs[$returnType]($decoded)""" } val n = if (toModelParams.length <= 1) "" else toModelParams.length.toString val applyN = TermName("apply" + n) val companion = tpe.typeSymbol.companion // Watch out for apply12 problem c.Expr[ModelMapper[A]](q""" import scalaz._, Scalaz._ import com.mongodb.casbah.Imports._ import com.github.aoiroaoino.mongodb.ModelMapper new ModelMapper[$tpe] { def toDBObject(model: $tpe): DBObject = DBObject(..$toDBObjectParams) def toModel(obj: DBObject): Option[$tpe] = Apply[Option].$applyN(..$toModelParams)($companion.apply _) } """) } }
コードはちょっと長いですが、やってる事は単純です。ざっくりと見ていきましょう。
まずは型パラメータAの情報を取得し、そこからapply
メソッド、およびその引数名一覧を取得します。
// 事前準備 val tpe = weakTypeOf[A] val constructor = tpe.decls.collectFirst { case m: MethodSymbol if m.isPrimaryConstructor => m }.getOrElse(c.abort(c.enclosingPosition, s"Cannot find constructor in $tpe")) val fields = constructor.paramLists.head match { case Nil => c.abort(c.enclosingPosition, s"Constructor argument is empty in $tpe") case x => x }
fieldsよりDBObject.apply
に渡すためのタプルを作ります。
// toDBObject作るための準備 val toDBObjectParams: List[Tree] = fields.map { field => val name = field.name.toTermName val decoded = name.decodedName.toString q"$decoded -> model.$name" }
fieldsよりscalaz.Apply
に渡すための式(obj.getAs[Type]("field_name")
、getAs
はCasbah提供のメソッド)を作ります。2
// toModel作るための準備 val toModelParams: List[Tree] = fields.map { field => val name = field.name.toTermName val decoded = name.decodedName.toString val returnType = tpe.decl(name).typeSignature q"""obj.getAs[$returnType]($decoded)""" }
で、最後にModelMapper[User]
のインスタンスを返します。
// 仕上げ c.Expr[ModelMapper[A]](q""" import scalaz._, Scalaz._ import com.mongodb.casbah.Imports._ import com.github.aoiroaoino.mongodb.ModelMapper new ModelMapper[$tpe] { def toDBObject(model: $tpe): DBObject = DBObject(..$toDBObjectParams) def toModel(obj: DBObject): Option[$tpe] = Apply[Option].$applyN(..$toModelParams)($companion.apply _) } """)
これで一通りマクロの実装は完了です!
先ほどのobject User
の中身を以下のように書き換えます。
// User.scala object User { // toDBObject/toModelが実装されたModelMapper[User]が返ってくる val m = AutoModelMapper[User] def toDBObject(user: User) = m.toDBObject(user) def toModel(obj: DBObject) = m.toModel(obj) }
置き換えた上で、再度試してみると手作業で実装したtoDBObject
/toModel
と同様に使えるかと思います。
これで非常に少ないコードでモデルを定義する際にラクできるようになりました!3
さて、突然のMonocleです。
そもそもMonocleとはなんだ?って方が多いかと思うので、簡単に説明すると,「Haskellのlensパッケージを元にJulien Truffaut氏が開発したScalaでOptics
(Lens
, Prism
など)な概念が使えるようになるライブラリ」です。
んじゃそもそもLens
, Prism
とは何かという話になりますが、ここでは簡単な使い方のみご紹介します。4
Lens
は誤解を恐れずに言うと、composable
な抽象化されたgetter
/setter
です。
まずはEvent
に対してLens
とPrism
を定義してしょう。Monocleでは以下のようにして定義します。
// EventOptics.scala trait EventLens { // GenLensマクロを使用 private def eventGenLens = GenLens[Event] def _eventOid = eventGenLens(_._id) def _title = eventGenLens(_.title) def _startDate = eventGenLens(_.start) def _endDate = eventGenLens(_.end) def _place = eventGenLens(_.place) def _owners = eventGenLens(_.owners) def _attendees = eventGenLens(_.attendees) }
今回は省力化のためにMonocleのGenLens
マクロを使用しましたが、マクロを使用せずに定義すると以下のようになります。
こっちの方がcomposable
なLens
って入れ物にgetter
/setter
を入れてる感(?)が伝わってきますね。
// 手動でLensを定義 // getterとsetterを引数に渡して、titleに対するLensを定義する def _title = Lens[Event, String](_.title)(t => e => e.copy(title = t))
Prism
は一言で表現するのは難しいのですが、とりあえずcomposable
で、変換(getOption: S => Option[A]
) とその逆変換(reverseGet: A => S
)の関数を持つ概念ってイメージです。
このS => A
とA => Option[S]
がまさにtoModel
とtoDBObject
の関係にあたるのが見てとれるかと思います。
// EventOptics.scala trait EventPrism { def _event = Prism[DBObject, Event] { case obj: DBObject => Event.toModel(obj) case _ => None }(Event.toDBObject _) }
さーて、ここまでだいぶ長い道のりでしたが、Lens
とPrism
を定義することで魔法のような操作が可能になります。
// サンプルデータ // サンプルデータを適当に用意 val owner1 = User(new ObjectId, "Owner1", "xxx") val owner2 = User(new ObjectId, "Owner2", "yyy") val event = Event(new ObjectId, "title", "start", "end", "place", List(owner1, owner2), List()) // MongoDBでfindOneした結果を想定 val findOneResult: Option[Event] = Some(event)
上記サンプルデータを使ってデータを操作してみましょう。5
// 使用例 // Eventの開催場所を取得 scala> findOneResult applyPrism some[Event, Event] composeLens _place getOption res31: Option[String] = Some(place) // Eventのタイトルを変更 scala> findOneResult applyPrism some[Event, Event] composeLens _title set("new title") res32: Option[com.github.aoiroaoino.model.Event] = Some(Event(559f39e3d4c646b1d30589bd,new title,start,end,place,List(User(559f39e3d4c646b1d30589bb,Owner1,xxx), User(559f39e3d4c646b1d30589bc,Owner2,yyy)),List())) // Event主催者の名前一覧を取得 scala> findOneResult applyPrism some[Event, Event] composeLens _owners composeTraversal each composeLens _name getAll res33: List[String] = List(Owner1, Owner2)
すごーく冗長に感じてしまった方も多いかと思いますが、ご安心ください。
MonocleではapplyXXX
, composeXXX
に対してエイリアスも用意されているので、以下のように書くことも可能です。
// エイリアスメソッドを使う // Eventの開催場所を取得 findOneResult &<-? some[Event, Event] ^|-> _place getOption // Eventのタイトルを変更 findOneResult &<-? some[Event, Event] ^|-> _title set("new title") // Eventオーナーの名前一覧を取得 findOneResult &<-? some[Event, Event] ^|-> _owners ^|->> each ^|-> _name getAll
いかがでしょうか?Lens
/Prism
の力によって、より柔軟にtoModel
/toDBObject
を操作できるようになりましたね。
アドホックにLens
やPrism
を合成できるので、findした結果 -> Prism
-> Lens
へのシームレスな連携が可能となりました!
さて、これがどういった時に美味しいのかというのをもう一例みてみましょう。
例えば、開発時に「Event
の全参加者の名前を大文字に変換したデータが欲しい」ってメソッドが欲しくなったとします。
このメソッドをLens
を使わずに書くと
// Lens使わない版 def Lens使わずに全参加者の名前を大文字に(e: Event) = { event.copy(attendees = event.attendees.map { attendee => val n = attendee.name.toUpperCase attendee.copy(name = n) }) }
割と素直で簡潔に書いたつもりですが、copy
とmap
が絡まってちょっと直感的でない印象ですね。
ですが、同様のメソッドをLens
を使って書くと
// Lensを使う版 def Lens使って全参加者の名前を大文字に(e: Event) = { event &|-> _attendees ^|->> each ^|-> _name modify(_.toUpperCase) }
と書けます。すごいですね、実質一行です!
一応動作確認もしてみましょう。
// 動作確認 scala> :paste // Entering paste mode (ctrl-D to finish) val owner = User(new ObjectId, "Owner", "xxx") val attendee1 = User(new ObjectId, "attendee1", "yyy") val attendee2 = User(new ObjectId, "attendee2", "zzz") val event = Event(new ObjectId, "title", "start", "end", "place", List(owner), List(attendee1, attendee2)) // Exiting paste mode, now interpreting. scala> Lens使わずに全参加者の名前を大文字に(event) == Lens使って全参加者の名前を大文字に(event) res16: Boolean = true
ちゃんと同じ結果になってますね!!
といった感じでcopy
からのmap
してcopy
なんて面倒なコードも書かなくて済むようになりました。
これでモデルを弄る側もハッピーです!!
いかがでしたでしょうか?
マクロとMonocleを使ってモデルの定義と使用がラクになりましたね!
実際はもっと複雑でこんな簡単にはいかないもんで、悩まなきゃいけないことは山ほどあるかと思います。
toDBObject
/toModel
に引数で渡すのダサいからメソッドとして呼ぶような形式にしたくない?とか、
case class
のfield名をcamelCase
からsnake_case
に変換するようにしたくない?とか、
それはそれで色々考え出すととても面白いのですが、記事の長さと時間の都合上この辺にしたいと思います。
今回の記事を書きつつ、柔軟な構文と豊富な機能によって色々な選択肢があるところがまたScalaの楽しいところだなーと改めて思いました。 最終的なコードはこちらに公開しておきましたので参考になれば幸いです。
以上、あおいの(@AoiroAoino)がお送り致しました。次回の記事も乞うご期待!!