読者です 読者をやめる 読者になる 読者になる

Neobundle.vimのload_cacheを複数ファイル対応にするPRを投げたらマージされた

2015年も年の瀬が押し迫ったところで本年ブログ初めです、ご無沙汰しております。

特に大きなPullReqでもないし記事にするほどでも無い気はするけど、多分同じ問題に当たったことのある人は多いだろう(ほぼ同じ内容のissueが過去にもあった)し、何よりShougowareに初コントリビュートできたので記念記事。

github.com

問題

Neobundle.vimに今年前半に実装された、TOMLファイルでプラグインを管理する機能とbundle設定をcacheする機能を同時に試していて、以下のようなvimrcを書きました。

set runtimepath+=~/.config/nvim/bundle/neobundle.vim

call neobundle#begin('~/.config/nvim/bundle/')

if neobundle#load_cache()
  call neobundle#load_toml('plugin.define.toml')
endif

call neobundle#end()

しかしこの状態でplugin.define.tomlを更新してもNeobundle.vimのcacheが更新されず、:NeoBundleInstallしても新しく登録したプラグインがインストールされません。TOMLファイルの扱いにバグがあるのかと思い、call neobundle#load_toml('plugin.define.toml')source 'plugin.define.vim'として素直にvimscriptで書いても状況は変わらず。

原因

Neobundle.vimのコードを読んでいると、neobundle#load_cache()$MYVIMRCの更新日時がcacheの更新日時よりも新しい場合に限りcacheを再構築することが判明。neobundle#begin()neobundle#end()に挟まれた部分で使っているファイルは適当に読み込みなおしてくれるだろうとかタカをくくっていたのが大間違いでした。

結論

ということで、load_cache()複数ファイル指定ができるような修正を加えてPullReqしました。幸い大きな問題もなくマージしていただけたのでNeobundle.vimを更新すれば使えます。

if neobundle#load_cache(
     \ $MYVIMRC,
     \ 'plugin.define.toml'
     \ )
  call neobundle#load_toml('plugin.define.toml')
endif

引数を省略した場合は従来通り$MYVIMRCのみチェックします。指定できるファイル数に制限はありません。

偉大なプラグインを生み出し続けている暗黒美夢王先生に感謝しつつ、何か引っかかった時は積極的にコントリビュートできるように頑張りたいと思いました。

Vaio Pro (2013 spring)でUbuntuとDual bootで運用している時にWindows updateした後の対処

大学のとある講義で「面白い論文とは、タイトルが短くて一般性の高いものが多い」という話がありましたが…

1. 状況

先月から、Vaio Pro (2013 spring, win8.1)をUbuntuとのDual boot(grub)で運用しています。 昨日Windows updateをしたところBootloader周りに手を入れられたらしく、grubを経由せずにWindowsが立ち上がるようになってしまいました。

2. 解決策

Vaio Proの起動順位トップは\EFI\Boot\bootmgfw.efiらしいので、これをgrubに入れ替えればOKでした。

sudo mv /boot/efi/EFI/Boot/bootmgfw.efi /boot/efi/EFI/Boot/bootmgfw.efi.orig
sudo cp /boot/efi/EFI/ubuntu/grubx64.efi /boot/efi/EFI/Boot/bootmgfw.efi

さらに、これだとgrubメニューからwindowsが立ちあげられなくなってしまうので、 VAIO Pro11 [red edition] SVP1121A2J をUEFIでトリプルブートにしてみた:shirokichi's hobby life:So-netブログ に従って

sudo blkid # check uuid of /dev/sda3
sudo vi /etc/grub.d/40_custom
#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.
menuentry "Windows Boot Manager (SONY Original)" {
        insmod part_gpt
        insmod fat
        set root='(hd0,gpt3)'
        search --no-floppy --fs-uuid --set=root <<< uuid of /dev/sda3 >>>
        chainloader /EFI/Microsoft/Boot/bootmgfw.efi.org
}
sudo update-grub

以上、自分用メモでした

「尤度」の説明の仕方

最近人から「尤度ってつまるところ何なのよ」と訊かれたことがありまして、その時はいい例えが思いつかず、条件付き確率がね…とか全くもって不親切な話をしてしまったわけですが、今閃いたのでメモ。

一般に「確率」と呼ばれるものは「パラメータを固定した上で、あるデータが出てくる確率(条件付き確率)」を表します。つまり\[p(X)=p(X|\beta)\] 一方「尤度」とは「データを固定した上で、特定のパラメータが出てくる確率」なので、↑の「確率」と逆になり\[L = p(\beta|X)\]

これはサイコロで例えるとわかりやすいです。「面の数」をパラメータ、「特定の目が出る確率」をデータだと思ってください。

「確率」とは、「正六面体のサイコロがあります。3の目が出る確からしさは?」と訊かれた時に答えられる数値。一方「とあるサイコロを何度か投げたら、3の目が出る確率が1/6でした。このサイコロが六面体である確率は?」と訊くのが「尤度」ですね。

1/6と聞くと当然六面体だろうと思ってしまいがちですが、もしかしたら八面体のサイコロを6回投げたら3が1回出たのかもしれないわけです。

数学的にはあまり厳密では無いかもしれませんが、イメージを掴むには便利だと思うので今度からこの喩えでやってみます。

いつNeovimに乗り換えるか?

今でしょ

はい、時代遅れのこれがやりたかっただけです。 久しぶりにneovimのHEADをbuildしてみたところ完璧に使えちゃったので皆さん乗り換えましょうという宣伝です。

7/8追記
※注意:半分深夜のテンションで書いた記事ですので適用はご自身の責任でおねがいします。特に末尾のNoticeのところに書いてあるようにいくつかのpluginが動かないので注意してください。

1. build

# Ubuntuの場合。その他OSの依存関係は  https://github.com/neovim/neovim/wiki/Installing#manual-install 参照
sudo apt-get install libtool autoconf automake cmake libncurses5-dev g++ pkg-config

cd ~/.src # お好きなところで
git clone https://github.com/neovim/neovim.git
cd neovim

make
sudo make install # Pacoとかその他お好みで

これで/usr/local/bin/nvimにバイナリがインストールされます。 /home以下に入れたいときは

cmake -DCMAKE_INSTALL_PREFIX:PATH=~/usr/
make install

とすると~/usr/binにインストールできます。

また、Mac OSとArch LinuxにはOS専用のパッケージが提供されています。詳しくはInstalling · neovim/neovim Wiki · GitHub

2. runtime

buildの時に、DCMAKE_INSTALL_PREFIXを指定してインストール先を変えた場合のみ必要な手順です。特に指定せず/usr/local/binにインストールした場合は不要です。

7/8訂正 本家VimのRuntimeが以下のような位置関係でインストールされている場合のみ不要な手順です。

/path/to/bin/nvim # バイナリ
/path/to/share/vim/vim74 # Runtime; このなかにautoloadとかindentとかsyntaxとか

このままでもnvimコマンドで動きますが、シンタックスハイライトとかインデントとかが働きません(NeoVim本体のレポジトリVim Runtimeファイル群が入っていないため)。neovim/vimscriptレポジトリが提供されているので自分で設定します。

cd ~/usr/share
git clone https://github.com/neovim/vimscript.git
alias nvim='VIM=~/usr/share/vimscript/runtime/ nvim'

参考: Installing · neovim/neovim Wiki · GitHub

3. .nvimrc.nvim/の準備

NeoVimの設定ファイルは~/.nvimrcです。存在しない時は.vimrcを読んでくれるとかそういう親切設計にはなっていないようなので、シンボリックリンクを貼りましょう。

ln -sf ~/.vimrc ~/.nvimrc
ln -sf ~/.vim/ ~/.nvim/

参考:Differences from Vim · neovim/neovim Wiki · GitHub

4. Try it!

これでnvimとコマンドを打つと今までのVimと全く同じ環境が再現できるはずです。 満足したらVimとお別れしてNeoVimと幸せに過ごしましょう。

# Uninstall Vim
sudo paco -r vim # 各自の環境に合わせて

# Use NeoVim instead of Vim
echo 'alias vim=nvim' >> ~/.bashrc

Notice

NeoVimはpluginの管理方法を変えると宣言しているので、将来的にこの記事の方法では動かなくなる可能性があります。 また、現状でもいくつかのpluginとは互換性が無いようです。

7/8追記 Shougoさんに解説をいただきました。

Rails 4.1で導入されるActiveRecord Enumsに隠された罠

TL;DR

ActiveRecord::Enumで、安易に値を追加・削除するのは危険。将来の変更に備えて、DBに登録される値をHashで指定しましょう。

class User < ActiveRecord::Base
  # This is BAD
  enum authority: [:registrant, :admin]
  # This is OK
  enum authority: { registrant: 10, admin: 20 }
end

本編

年度も変わりさて心機一転、という季節なのに私の地元は昨日大雪でしたが、皆様いかがお過ごしでしょうか。さて今日は表題の通り、Rails 4.1における目玉のひとつ、ActiveRecord Enumsについてです。

ActiveRecord Enumsとは

例えばUserモデルにauthority(権限)という属性を持たせたい時によくやるのは、DBにintegerのフィールドを用意して、0なら登録ユーザー、1なら管理者、...とする方法ですね。 この方法は便利ですが、0とか1とかいう値をそのまま数値で管理し続けるのは辛いので、scope :registrant, -> { where(authority: 0 }みたいなscopeとか、その他のユーティリティメソッドを自分で用意する必要がありました。

そこでActiveRecord Enumsを用いると、

class User < ActiveRecord::Base
  enum authority: [:registrant, :admin]
end

と定義してやることで、自動的に

User.registrant  # scope :registrant, -> { where(authority: 0) }

u = User.registrant.first
u.authority      # => "registrant"
u.admin!         # u.update_attribute(:authority, 1)

User.authorities # => { registrant: 0, admin: 1 }

みたいな便利メソッドがごそっとまとめて定義されます。

参考:ActiveRecord::Enum

問題点

enum authority: [:registrant, :admin]

と定義すると、DBに登録される値は自動的にregistrantは0、adminは1に設定されます。 この状態で、rails consoleからadminユーザーを一人登録しておきます。

u = User.new(name: "hoge", authority: User.authorities[:admin])
u.save!
u.authority # => "admin"

さてここで、サイトにゲスト権限を設定したくなった場合はどうなるでしょうか。自然に変更するとすれば仮にリストの先頭に加えてしまうと

enum authority: [:guest, :registrant, :admin]

となりますね。

2014-05-11追記

はてブコメントにて何件か「なぜ末尾ではなく先頭に足すのが『自然』なのか」というご指摘をいただきましたが、意図としてはauthorityの低い順に「ゲスト、登録ユーザ、管理者」と並んでいる方が「登録ユーザ、管理者、ゲスト」という並びよりも自然かなというものでした。
とはいえ、やはりenumの何たるかを考えると、後ろに足すのが普通だというご指摘は明らかに的を射ていますので、本文を修正してあります。
---追記ここまで---

この時、先ほど登録したhogeさんの状態を確認してみましょう。

u = User.find_by(name: "hoge")
u.authority # => "registrant"

なんと!hogeさんがadminからregistrantに格下げされてしまいました!

メカニズム

最初に

enum authority: [:registrant, :admin]

の状態でhogeさんをadminとして登録した時、DBには1という値が保存されています。決して"admin"という文字列が保存されているわけではありません。integerのカラムなのですから当然です。

先程は、この状態でmodel

enum authority: [:guest, :registrant, :admin]

と書き換えた結果、DBに保存されている1という値が示すauthorityregistrantになってしまっています。つまり

User.authorities # => { guest: 0, registrant: 1, admin: 2 }

というわけです。そのため、hogeさんのレコードが持っていた1という値により、hogeさんはregistrantであるとされてしまいました。

解決策

ActiveRecord Enumsには、DBに保存される値をHashで指定できます。

最初(guestがなかった頃)に

enum authority: { registrant: 10, admin: 20 }

としておけば、adminとして登録したhogeさんのレコードには20という値が保存されます。

これならば、guestを以下のように追加しても問題ありません。

enum authority: { guest: 5, registrant, 10, admin: 20 }

ユーザーの権限の種類なんて最初っからきちんと設計しろよ!という話も無くはありませんが、変更は常に起こりうるものです。今回のケースでは10文字くらい多くタイプするだけで変更耐性を付けられるのですから、ぜひとも採用しておきたいものだと思います。

Rubyで電話番号整形

どもです。最近は夕方になっても暗くならなくなって時間間隔が微妙に狂いかかっているところですが皆さんお元気ですか?

さて、自身初のRubyGemを公開したので宣伝Postします。ステマじゃないです。ガチマです。

URL

tel_formatter | RubyGems.org | your community gem host
iTakeshi/tel_formatter · GitHub

できること

  • 電話番号の整形(市外局番は辞書ファイルから参照)
  • 全角とか余計な記号が入ってても大丈夫

これだけですが意外と既存のGemが落ちてなかったので練習問題として作ってみました。

TODO

  • ハイフン以外の区切り文字とか、(03)1234-5678みたいな出力形式に対応したい。

使い方

# Gemfile
gem 'tel_formatter'
require 'tel_formatter'

TelFormatter.format('0312345678')
# => '03-1234-5678'
TelFormatter.format('0126712345')
# => '01267-1-2345'
TelFormatter.format('03.1234.5678')
# => '03-1234-5678'

# 桁数がおかしいものや、存在しない市外局番なんかは弾く
TelFormatter.format('031234567')
# => ArgumentError

Gemの公開ってこんなに簡単だったんですね、という感じですね。 これからはもっと積極的に公開していけるといいと思いました。

Travisはすごく便利なんですが連携させたいCoverallsがいつまでたってもcoverage: unknownなのが気に食わないです。HQに連絡すれば治るという噂ですがどうなんですかね。

それではごきげんよう

「それNArrayでできるよ」をもっと便利にした - 札幌市中央区Ruby会議01

もう先週の話になってしまいましたが、Tricknotesさん主催の札幌市中央区Ruby会議01に参加してきました。 コンパクトな規模ながらも「非常に良いRubyistの活動が観測」(byしまださん)された、有意義かつ楽しい会議でした。

どの発表も聴き応えのあるすばらしいものでしたが、中でもtmaedaさんの「それNArrayでできるよ」は数値計算ライブラリのNArrayをbetter Excelとして使ってしまうコペルニクス的発想で衝撃的でした。

この発表のなかで、NArrayの軸をHuman-Readableな形式で扱えるラッパークラスを作ると便利だよ、というお話がありましたが、

これをもっと便利にするNArrayCubeクラスを試験的に実装してみたので紹介します。

使い方

# 店、世代、性別の3軸からなる売上表を作成
cube = NArrayCube.new(:int,
                      { title: :shop,       labels: ['A', 'B', 'C'] },
                      { title: :generation, labels: ['teens', 'twenties', 'thirties', 'fourties'] },
                      { title: :gender,     labels: ['men', 'women'] }
                     )

# 店A、20代、男性の売上を入力
cube.set({ shop: 'A', generation: 'teens', gender: 'men' }, 100)

# 店A、10代の売上を入力
cube.set({ shop: 'A', generation: 'teens' }, [90, 110])

# 店Bの売上を一気に入力(4世代 × 2性別のArray)
cube.set({ shop: 'B'}, [ [50, 80, 60, 75], [70, 40, 35, 50] ])

# 店Cの売上を一気に入力:第2引数を一次元のArrayで与えると勝手に4x2に変換してくれる
cube.set({ shop: 'C'}, [70, 80, 55, 60, 30, 45, 70, 60])

# 10代の売上を取得
cube.get({ generation: 'teens' })
#=>
# #<NArrayCube: ...
# [ [ 90, 50, 70 ], 
#   [ 110, 70, 30 ] ]>

# 10代の売上の合計
cube.get({ generation: 'teens' }).sum
#=> 420

# 10代の売上を店ごとに集計 => 男性+女性の値を計算すればよい
cube.get({ generation: 'teens' }).sum(:gender)
#=>
# NArray.int(3): 
# [ 200, 120, 100 ]

ツボ

  • NArrayCube#setで複数のセルに一気に値を流し込める
  • NArray#sumNArrayCube#sumでラップしたことにより、軸の指定をHuman-Readableに行える

TODO

  • spec書く
  • document書く
  • NArray#sumだけじゃなくprod/max/minとかその他のメソッドにも対応したい => method_missingとか使えるかな?
  • 「10代の売上を店ごとに集計」はcube.get({ generation: 'teens' }).sum_by(:shop)みたいに書けたほうが直感的
  • getで軸ラベルを複数指定できたほうがいいcube.get({ shop: ['A', 'B'] })
  • tmaedaさんがいいよって言ってくれたらGem化して公開

コード

require 'narray'

class NArrayCube
  NARRAY_TYPES = %i(byte sint int sfloat float scomplex complex object)

  class NArrayAxis
    def initialize(id, title, labels)
      @id = id
      @title = title.to_s
      @labels = labels.map(&:to_s)
    end

    def value_index(value)
      @labels.index(value.to_s)
    end

    attr_reader :id, :title, :labels
  end

  def initialize(type, *axes)
    if NARRAY_TYPES.include?(type)
      @type = type
    else
      raise ArgumentError
    end

    @axes = axes.map.with_index do |a, i|
      NArrayAxis.new(i, a[:title], a[:labels])
    end

    @narray = NArray.method(type).call(*@axes.map { |a| a.labels.length })
  end

  def get(conditions)
    conditions = build_condition(conditions)
    values = @narray[*conditions]

    axes = conditions.map.with_index do |c, id|
      if c.class == Array
        a = @axes[id]
        { title: a.title, labels: a.labels.select.with_index { |v, i| c.include? i } }
      else
        nil
      end
    end

    NArrayCube.new(@type, *axes.compact).set({}, values)
  end

  def set(conditions, values)
    conditions = build_condition(conditions)

    unless [Array, NArray].include?(values.class)
      @narray[*conditions] = values
    else
      partial = @narray[*conditions]
      values = NArray.to_na(values)

      if partial.shape == values.shape
        @narray[*conditions] = values
      elsif partial.total == values.total && values.shape.length == 1
        @narray[*conditions] = values.reshape(*partial.shape)
      else
        raise ArgumentError
      end
    end

    self
  end

  def sum(*titles)
    if titles.length > 0
      @narray.sum(*titles.map { |t| axis(t).id })
    else
      @narray.sum
    end
  end

  private
  def build_condition(conditions)
    cond = @axes.map { |a| (0..(a.labels.length - 1)).to_a }
    conditions.each do |title, label|
      a = axis(title)
      cond[a.id] = a.value_index(label)
    end
    cond
  end

  def axis(title)
    @axes.select { |a| a.title == title.to_s }.first
  end
end