Kotlin Delegate propertyが素晴らしい。

最近Kotlin触れてませんでした。少しずつでもいじらないと全部忘れてしまいそうなので(ほとんど忘れてるけど…)、最近のバージョンアップで加わったDelegate propertyについてのブログを読んでました。試しにサンプルコードを書いてみたのですが、Delegate property素晴らしい!よく不満に思っていた部分が解消されていました。

例えば、Hogeクラスではメンバ変数のpiyoを使うのだけれど、piyoはString型。しかし、初期化のためにString?型にしてnullで初期化せざるをえなかった。本当はnullなんか入れたくないのに…。という場合。

ここにコードがあるじゃろ?

package demo

class Hoge {
  var piyo : String? = null // これが嫌い!
}

fun main(args : Array<String>) {
  val hoge = Hoge()
  hoge.piyo = "PIYOPIYO"
  println(hoge.piyo?.toLowerCase) // 型がString?なのでnullの可能性があるから?が必要
}

これをこうして…
こうじゃ!

package demo

class Hoge {
  var piyo : String by Delegates.notNull()
}

fun main(args : Array<String>) {
  val hoge = Hoge()
  hoge.piyo = "PIYOPIYO"
  println(hoge.piyo.toLowerCase) // 型がStringなのでnullの可能性がない!
}

これで不満が一つ解消された。

あと、Delegates.mapValが追加されてた。
これはjsonのデータをパースしてそのままインスタンスにする場合に使える。

package demo

class User(map : Map<String, Any?>) {
  val name: String by Delegates.mapVal(map)
  val age: Int     by Delegates.mapVal(map)
}

fun main(args : Array<String>) {
  val user = User(mapOf(
    "name" to "John Doe",
    "age" to 25
  ))
  println(user.name)
  println(user.age)
}

これもよさそう。あとはDelegates.lazy{}もあった。
これも使えそうだ。


ransackでソートする方法。

検索で使えるgemのransackですが、超便利っすね。
最近、自力で実装していたやつをransackで置き換えていってるのですが、随分コードが綺麗になりました。

ただ、情報が微妙に少ないというか、ググればあるんだけれど、本家のサイトには少ない感じがします。
今回はソートを複数の項目でやりたいと思っていたのですが、なかなか見当たらなかったのでメモです。

class ArticlesController < ApplicationController
  def index
    @articles = search
    respond_to do |format|
      format.html
      format.json { render json: @articles}
    end
  end

  private
  def search
    params[:q] ||= {}
    params[:q][:s] = %w(author_id category_name created) # 複数指定は配列を渡す
    @q = Article.search params[:q]
    # もしくはこう。
    # @q.sorts = %w(author_id category_name created)
    @q.result
  end
end

params[:q][:s]に、配列で文字列を渡します。
文字列をカンマ区切りで指定しても有効にはなりませんでした。


レビュー:Acer A1-810(マンガロイドZ)イイヨ!!

先日、Kampa!でカンパを募って、ありがたいことに買うことができたAndroidタブレットのレビューです。皆様からの結婚祝いの品、ありがたく使わせていただきます!

さて、Acer A1-810は、7.9インチの縦横比が4:3のAndroidタブレットです。OSのバージョンは4.2.2。Google Playマーケットもあります。
競合商品は完全にiPad miniです。サイズも同じだし。
比較とかはたぶんITMediaとかに載ってそうなので、ざっくりとした比較をすると(俺はiPadもってないけども)、

■価格
iPad mini(32,800円) < マンガロイドZ(22,800円。ただし5,250円分の図書券付き) iPad miniはWi-Fi版の16GBとします。 マンガロイドZは、Wi-Fi版のみで容量は16GBですが、micro SDカードで容量を増やせます。 また、ebook japanで使える5,250円分の図書券が付いています。(図書券は1050円単位) この図書券は曲者なので後述しますが、マンガロイドZは価格面では実質17,000くらいになります。 ■重量 iPad mini(308g) > マンガロイドZ(410g)

重量的にはiPad miniのほうが100g軽いです。寝転がりながらタブレット使いたいと思っていたので100gの重さの違いはでかいんじゃないだろうか?と思っていましたが、410gは思ったよりも気になりませんでした。XOOMを使っていた頃は重たすぎてダメだったのですが、マンガロイドZならば使う気になれます。
勝敗的にはiPad miniのほうが軽いですが、個人的には気にならないレベルです。

■アプリの揃い具合
iPad mini = マンガロイドZ

比較してないけど。マンガロイドZはちゃんとGoogle Playマーケットが入ってるので、別に困ることは無いでしょう。安いだけで微妙な中華タブレットとは全然違います。安心して買えます。

以上が、iPad miniとの比較です。
次からは、マンガロイドZの感想。

■ebook japanの図書券について
図書券が曲者と書きました。それについて言及します。
ebook japanでは、本を買うときに図書券+カード払いとかができず、図書券ならば図書券でしか買い物ができません。
例えば、1,280円の本があるとして、1,050円分の図書券 + 230円分のカード決済とかができないのです。この本を買おうと思ったら、1,050円の図書券2枚を使います。そうなると、2,100 – 1,280 = 820円分が残りますが、じつはこの残りは使えなくなります。お釣りとかありません。ただ、本は複数同時に買うことができるので、この場合でいえば、なるべく2,100円に近くなるように本を買うとお得、ということになります。

ebook japanはマンガのまとめ売りとかしてるやつがあるので、それで買うとお得かつ2,100円ちょうどのやつとかがあります。俺はそれで進撃の巨人を買いました。1冊400円くらいになるし、2,100円で無駄が出なかったので、オススメです。

■では、実物を披露
CIMG3030
大きさ的には、ちょうどいい感じ。

■持った感じ
ギリギリ片手で持てます。
軽いからそれほど苦ではないです。
XOOMとかだと無理。

■XOOMとマンガロイドZとの比較
CIMG3031
並べると結構違います。この大きさの違いがでかい。あとXOOMは重い…。

■マンガを読んでみて
著作権的に載せたらまずいかなと思ったので、感想だけ書きます。
余白がほとんど出ないので、違和感がないです。2ページ見開きでいえば、XOOMとほとんど遜色ないので、読みやすい。普通のマンガを読んでいる感覚で読めます。重くないし、いいサイズだなー。マンガはもうこれで買っていこうかなと思います。

■自炊したPDFを読んでみて
自炊した本については、まぁファイルサイズが重たいので正直微妙でした(ソフトはezPDF Readerを使用)。それでも、本体が持ちやすいから読む気持ちになりますね。マンガロイドZはクアッドコアのメモリ1Gだから、それなりに処理も速いし、待ち時間的にはXOOMに比べると短い気もします。

■バンドルされているソフトについて
マンガロイドということだけれど、バンドルされているソフトに、Zinioというのがありました。どうも雑誌を販売しているところのソフトのようです。海外の雑誌だけかと思ったら日本の雑誌とかも結構あったのでよさげ。Bicycle Clubとか売ってあった。まだ利用してないけど、いつか買ってみたいと思います(雑誌捨てるの面倒だからね…。)

■カメラについて
カメラは5M画素なので、普通です。インカメラもあるので、Google Hangoutが使えます。FaceTimeみたいなもんです。画素数が低いけど、まぁ使えるレベルだと思いました。

■復帰用機能?
Touch wake upという機能が付いてました。電源ボタンを押さずに、5本指で画面に触れると復帰するという機能です。これってAndroid 4.2標準なんかな?わからないので一応YouTubeに動画をアップしたので確認してみてください。

以上になります。

とりあえずこいつのおかげで、KoboとはさようならできるしXOOMの出番も少なくなりそうです(最近そもそも起動すらしてなかったけど)。XOOM、君は重すぎた…。
マンガロイドZは、名前に恥じぬ、読書専用端末としてはすごくよさそうです。
あと、両手で持った状態で、親指タップでのキーボード入力がしやすかったので、入力もよさげです。手広く活躍してくれそう。23,000円というコストパフォーマンスのよさもいいし、読書端末と考えればNexus7よりもいいんじゃないかなー?


RailsでJenkinsのジョブの経過みたいに処理経過を表示したかった。

今作っているシステムで、データにインポート処理があるのだけれど、それをコマンドラインからじゃなくて画面からできるようにしたかった(俺以外の人もできるように)。
それはrake taskをコントローラーから呼び出せばいいのかなと思ったのだけれど、途中経過をJenkinsみたいに表示できたらなぁと思って調べていた。

かずさんから、Rails3.1からstreamingができるようになってるという話を聞いたので、まずはそれを確認したところ、それはそこまで難しくなかった。
ただし、streamingを使う場合はWebrickではできないらしい。ローカルでunicornを起動しないといけないのでお手軽さが少々欠ける。

unicornのインストール・設定・起動

unicornはGemfileに記述してからbundle installすればいい。あとはconfig/unicorn.rbを記述する。
unicorn.rb

# coding: utf-8
listen 3000, :tcp_nopush => false
worker_processes 4

bundle installしてからunicornを起動する。

bundle install # Gemfileに gem 'unicorn'を記述すること
unicorn_rails --config-file config/unicorn.rb # unicornを起動

サーバサイド側の実装

streamingをするためのコントローラーの記述はそこまで複雑ではなかった。
参考にしたサイト

# coding: utf-8
require 'pty'

class ImportsController < ApplicationController
  authorize_resource class: false # Deviseでログインしていることを確認
  layout :layout # 

  def index
  end

  def hoge
    render_stream do |r|
      # rake taskでimport:hogeを作ってたとする
      # rake import:hogeで逐次表示される内容をstreamで出力する
      PTY.spawn("rake import:hoge") do |stdin, stdout, pid|
        begin
          stdin.each do |line|
            r << line
          end
        rescue Error::EIO
        end
      end
    end
  end


  private
  def render_stream(&block)
    # streamingで出力するための処理をここでまとめてる。
    headers["Cache-Control"] ||= "no-cache"
    headers["Transfer-Encoding"] = "chunked"
    self.response_body = Rack::Chunked::Body.new(Enumerator.new do |r|
      block.call r
    end)
  end

  def layout
    if action_name == 'index'
      'application'
    else
      false
    end
  end
end

クライアント側の実装

import_hoge_pathにアクセスすれば、徐々に結果が表示されることは確認できたのだが、Ajaxで取得しにいったときの処理方法がなかなかわからなかった。
jQueryを使ってAjaxをした場合、successやcompleteのタイミングで処理をするとストリーミングにならない。全部出力した後にコールバックされるからだ。
どうすればストリーミング的に処理できるかを調べたら、参考になるサイトがあったので載せておく。

jQueryの$.ajaxで通信途中のresponseTextを取得する : あらびき日記

CoffeeScriptで関数を実装したらこんな感じ。
setIntervalで定期的にresponseTextを確認して、追加された分だけ抽出して#import_resultに結果を出力している。

ajax_with_stream = (method, url) ->
  $('#import_result').html('')
  $.ajax({
    type: method,
    url: url,
    xhrFields: {
      onloadstart: ->
        xhr = this
        length = 0
        line_count = 0
        $.mytimer = setInterval(->
          if length != xhr.responseText.length
            length = xhr.responseText.length
            lines = xhr.responseText.split("\n")
            for i in [line_count..lines.length-1]
              line = lines[i]
              if line
                $('#import_result').append line + "\n"
            line_count = lines.length - 1
        , 400)
    },
    success: ->
      console.log 'finished!!'
      setTimeout('clearInterval($.mytimer)', 400)
  })

これでいけたわーと思ったのですが、rake taskの処理が重たすぎたりすると、Unicornがタイムアウトしてしまって、結果が表示されたりされなかったりと誤差が出るようになってしまった…。ものすごく重たい処理を同期処理でサーバに表示させながらやるにはちょっとダメかもしれん…と思いながら、急場をしのぐためにunicornのtimeoutを600に変更した。そしたらうまくいったけど何かが違う。そうじゃない。

悩んでたら、delayed_jobやsidekiqでやるのがいいんじゃない?とtwitterのTLで助言をいただいたのでそちらを参考にしてみようかと思う。Jenkins的に結果を出したいのは、できればいいなーという感じだったので。
あとstreamingを使うために開発環境までunicornにするのは面倒だしなぁ…というのもある。sidekiqが気になるでまた調べてみようと思う。


model_historyというgemを作ってないけど公開した。

タイトルがわけわからないと思いますが、作ってないけど公開したのです。

事の発端は、dirty_historyというgemでした。
dirty_historyというgemは、ActiveRecordの変更履歴を保存してくれるというgemで、私が使いたかった機能を完全に実装してくれていたのですが、ただひとつ欠点がありました。
それは、テーブル定義にobject_idというカラムがあったことです。

そのため、DirtyHistoryRecordsテーブルのモデルは、Rubyのobject_idメソッドを再定義することになり、そのせいでWarningが出ていました。仕方がないのでそれを直してプルリクを送ろうか、と当初は考えたのですが、カラム名を変更するというプルリクは変更が大きいし、gemの後方互換性がなくなるのでやめたほうがよくないか?とチーム内でアドバイスをもらったので、全く似たようなカラム名の違うgemを作る事にしました。

dirty_historyはjewelerを使ってて今時じゃないよとカズさんに聞いたので、bundlerを使ったスッキリとした構成に書き直しました。カズさん製のgemであるneed_labelを参考にgemspecとかを書き換えて、リリース。でも処理はほとんどdirty_historyをコピーしました。だから作ってはいません。。。

まさかこんなことで初めてのgem作りをすることになるとは思ってなかったのですが、gemをリリースするのは簡単でびっくりしました。Chrome Extensionを作るのと似ています。

既にrecord_historyというgemがあったので、model_historyという名前にしたのですが、ネーミング微妙かもしれないですね…。

ということで、gemデビューでした。