20200629のRubyに関する記事は12件です。

数値を使わず 3 で割った余りを求める

お題

0 以上の整数が文字列として与えられ,それを 3 で割った余りを返す関数なりメソッドなりを定義せよ。この記事では Ruby を使うので,メソッドですね。

ただし,数値は一切使わないこととする。つまり,処理の途中で数値オブジェクトを使ったりしない。当然ながら返り値も文字列とする。

組込みのメソッドが内部で数値オブジェクトを生成していないか分からないので,自分で書いたコードが途中で数値オブジェクトを作っていなければよいことにする。

メソッド名を remainder_modulo_3 と名付けよう。英語が合ってるかよく分からんけど,remainder は剰余のことで,modulo 3 は「3 で割ったときの」という意味1

以下のようになればいい:

p remainder_modulo_3("0")  # => 0
p remainder_modulo_3("8")  # => 2
p remainder_modulo_3("12") # => 0

手でやる方法

プログラムを考える前に,手計算でやる方法を。

例えば 1234567 を 3 で割った余りは,ひと目見て 1 だと分かる。えっ? どういうこと?

まず,もっと簡単な 87 を 3 で割った余りを考える。真面目に割り算をやってもいいのだが,もっとサボる方法がある。

\begin{eqnarray}
87 &=& 8 \times 10 + 7 \\
   &=& 8 \times (9 + 1) + 7 \\
   &=& 8 \times 9 + 8 + 7
\end{eqnarray}

なんだけれども,$8 \times 9$ は $9$ の倍数だから $3$ で割り切れる。よって,$8 \times 9$ は除外して考えてよくて,結局 $8 + 7$ を $3$ で割った余りを求めればよい。

$8 + 7$ は $15$ なので $3$ で割り切れる。余りは $0$ だ。

同じように考えると 1234567 のような大きな数だって,

\begin{eqnarray}
1234567 &=& 1 \times (999999 + 1) + 2 \times (99999 + 1) + 3 \times (9999 + 1) + 4 \times (999 + 1) + 5 \times (99 + 1) + 6 \times (9 + 1) + 7 \\
  &=& (9 の倍数) + 1 + 2 + 3 + 4 + 5 + 6 + 7
\end{eqnarray}

となるので,$1 + 2 + 3 + 4 + 5 + 6 + 7$ を $3$ で割った余りを求めればいい。

$1 + 2 + 3 + 4 + 5 + 6 + 7$ を真面目に計算する必要はなくて,

(1 + 2) + (3) + (4 + 5) + (6) + 7

のように $3$ の倍数が見えてるから,これらを除外して $7$ を $3$ で割った余りを求め,$1$ が得られる。

では $78932705$ のような数はどうか。この数の代わりに

7 + 8 + 9 + 3 + 2 + 7 + 0 + 5

を考えればいいのだが,これらの数字のうち,$9$,$3$,$0$ は $3$ の倍数だから除外してよくて,

7 + 8 + 2 + 7 + 5

を考えればよい。
また,$7 = 2 \times 3 + 1$ などを考えると,$7$ を $1$ に,$8$ を $2$ に,$5$ を $2$ に置き換えた

1 + 2 + 2 + 1 + 2

を考えればいいわけだ。(出てくる数字は 1 と 2 しかあり得ない)

人間ならこれをパッと見て $1$ を $2$ を組み合わせて $3$ を作って除外し,余りは $2$ だとすぐに分かる。
しかし,これをプログラムによる文字列処理をやるには?

やはりソートして

1 + 1 + 2 + 2 + 2

とし,"11222" という文字列を作るのがいいだろう。

あとは "111" とか "222" とか "12" などの文字列を削除していけばよさそうだ。

コード

こんなんできました:

def remainder_modulo_3(number)
  s = number.delete("0369").tr("4578", "1212")
    .chars.sort.join
    .gsub(/(.)\1\1|1122|12/, "")
  {"" => "0", "1" => "1", "11" => "2", "2" => "2", "22" => "1"}[s]
end

文字列で 0 以上の整数を与え,文字列で "0""1""2" のいずれかを返す。

ちょっと試してみよう:

1000000.times do |n|
  puts n unless (n % 3).to_s == remainder_modulo_3(n.to_s)
end

普通に Integer で計算したものと,remainder_modulo_3 メソッドで得たものを比較して,一致してなければそれを表示する。
やってみたところ何も表示されなかったので,コードは合っているようだ。

解説

ちょっとメソッドチェーンが長いけれども,やっていることは単純。

まず

delete("0369")

だが,0, 3, 6, 9 は 3 の倍数なので,これらの数字は除外する。

次の

tr("4578", "1212")

は,例えば 4 は 3 + 1 だから 1 に置き換えてよい,5 は 2 に置き換えてよい,ということ。

その次の

chars.sort.join

は,まず文字列を 1 文字ずつバラし,それをソートして再び繋げる。
これにより,例えば "212" から "122" が得られる。

この時点で得られる文字列は,「0 文字以上の "1" のあとに 0 文字以上の "2" が続く」というものになる。それ以外のパターンはあり得ない。

その次の

gsub(/(.)\1\1|1122|12/, "")

は,特定の文字列を左から順に削除しているのだが,正規表現がちょっとややこしい。

ハイライトは (.)\1\1 の部分。ここは後方参照を使っている。(.) でキャプチャーした文字がそのあとに二つ続くことを意味し,ここ全体としては「同じ数字が三つ並んだもの」にマッチする。

それから,"1" の列と "2" の列の境界部分に存在しうる "1122""12" にもマッチする。

これらを削除するわけだ。

この変換のあと得られる文字列はどんなものがありうるか。
ちょっと退屈だが,"1" の個数で場合分けしてみよう。

まず,"111" が真っ先に削除されるので(3 の倍数個の "1" が削除されるから)"1" の個数は 0, 1, 2 個の場合だけ考えればよいと分かる。

"1" が 0 個のとき,"2" が 0 個以上連なった文字列である。3 の倍数個の "2" が削除されることを思えば,"""2""22" の三つがありうる。

次に "1" が 1 個のとき,うしろに続く "2" が 0 個なら "1" だし,"2" が 1 個なら全体("12")が削除されて "" になり,"2" が 2 個なら "2" となる。"2" が 3 個以上のときは "222" が削除されるので,考えなくてよい(可能性は尽きている)。つまり,"1""""2" の三つがありうる。

最後に "1" が 2 個のとき,うしろに続く "2" が 0 個なら,"11" だし,"2" が 1 個なら "12" が削除されて "1" になるし,"2" が 2 個なら全体("1122")が削除されて空文字列になる。"2" が 3 個なら "1122" が削除されて "2" となる。4 個以上は考えなくてよい。つまり,"11""1""""2" の四つがありうる。

ふぅ,めんどくさかった。

結局,変換後の文字列は

  • ""
  • "1"
  • "11"
  • "2"
  • "22"

の五つの可能性しかない。

これらについてのあまりをハッシュで導いている。
以上。

応用

9 で割った余りも全く同様にできる。

2 や 5 で割った余りは最後の桁(1 の位)だけを見ればいい(10 が 2 や 5 で割り切れるから)。

4 で割った余りは最後の 2 桁を見る(100 が 4 で割り切れるから)。

8 で割った余りは最後の 3 桁を見る(1000 が 8 で割り切れるから)。

6 で割った余りはだいぶややこしいはず。いずれ挑戦してみたい。

7 で割った余りを数値オブジェクトを生成せずに求めることができるのかは,よく分からない。

最後に

「だからなに?」と言われても答えに窮する。
「整数論」のタグを付けておいたけど,そんな大したものじゃないし,実用性も無いし,すごく面白いというわけでもない。
なにげに思い立ってやってみただけ。


  1. modulo 3 は,正式な数学の用語では「3 を法として」と言う。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx ライブラリ - JIT ライブラリ

Kinx ライブラリ - JIT ライブラリ

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。JIT コンパイルのためのライブラリを作ってみました。

JIT やりたいよね。今回は Kinx - native でも使っている SLJIT を使いやすくしようということで、ライブラリ化しました。SLJIT 自体、ドキュメントが少なくてソースから解読して使っているので、SLJIT そのものの使い方を備忘録的に書こうかとも思ったのだけど、今回は保留。どこかでやるかもしれない。

しかし、SLJIT をそのまま使うよりも勿論使いやすくなっているので、こっちのほうが良いと思う。ホスト言語もスクリプト なので、手軽に楽しめるでしょう。

どんな感じ?

先にどんな感じのプログラムになるか、サンプルを出しておきます。色々細かい話が続いてここまでたどり着いてくれなさそうなので。。。

using Jit;

var c = new Jit.Compiler();
var entry1 = c.enter();
    var jump0 = c.ge(Jit.S0, Jit.IMM(3));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
    c.call(entry1);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
    c.call(entry1);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

Jit.Compiler オブジェクトを作って、enter で関数エントリを作り、色々レジスタをいじくりまわして ret するコードを書きます。で、実行するときは generate() して run()、となります。generate() して dump() とすると、アセンブル・リストを見ることもできます。

色々とばしたい方は サンプル へ Go! → サンプルでは Ruby、Python、PyPy とのベンチマークもしてますよ。

SLJIT

そもそも SLJIT とは何か。

一言でいえば 抽象化アセンブラ で、一つの書き方で複数の環境をサポートできてしまうという、CPU ごとに異なるので作り直さなければならないというアセンブラの問題を解決してくれるライブラリです。現状サポートしているというプラットフォームは以下の通り。

  • SLJIT のサポート・プラットフォーム
    • Intel-x86 32
    • AMD-x86 64
    • ARM 32 (ARM-v5, ARM-v7 and Thumb2 instruction sets)
    • ARM 64
    • PowerPC 32
    • PowerPC 64
    • MIPS 32 (III, R1)
    • MIPS 64 (III, R1)
    • SPARC 32

ただし、ここで紹介する Kinx 版 JIT ライブラリは 64bit しかサポートしていないこと、および x64 Windows と x64 Linux しか確認して(できて)いませんので、そこは悪しからずご了承ください。

公式? 説明文書

私が知る限り、参考になる文書は以下程度しか見つかりませんでした。

参考にはなります。

GitHub のリポジトリは以下です。

Jit

さて、Kinx ライブラリとしての JIT ライブラリ。C のまま使うより便利にはしてます。もちろん C のライブラリを使えばもっとより細かく制御できると思いますが、それなりのことはできます。

using Jit

Jit ライブラリは標準組み込みではないため、using ディレクティブを使用して明示的に読み込む。

using Jit;

Jit オブジェクト

Jit オブジェクトはパラメータ用のメソッドとコンパイラ・クラスが定義されている。

Jit パラメータ用メソッド

Jit のパラメータとして、即値、レジスタ、メモリアクセスの 3 種類がある。以下の形で利用する。

即値、メモリアクセス

即値、メモリアクセスは以下のメソッドで利用する。Jit.VAR() はローカル変数領域を使うための特別なメソッド。スタック領域にローカル変数領域が自動的に確保され、その領域を使う。

メソッド 備考
Jit.IMM(v) 64bit 整数、浮動小数点数どちらも同じ書き方。代入先のレジスタと合わせる。
Jit.VAR(n) ローカル変数領域。1 変数 8 バイト固定。
Jit.MEM0(address) address として即値を代入、ただし現在実アドレスをスクリプトから指定できないので、スクリプトからは使えない。
Jit.MEM1(r1, offset) r1 に指定したレジスタをアドレスとみて、offset 位置(バイト単位)のメモリアドレスを示す。
Jit.MEM2(r1, r2, shift) shift は 0 なら 1 バイト、1 なら 2 バイト、 2 なら 4 バイト、3 なら 8 バイトを示し、r1 + r2 * (shiftで示すバイト分) の位置のメモリアドレスを示す。
レジスタ

以下のレジスタを使用可能。関数内で使えるレジスタの数は自動的に計算され、関数(enter() で区切った範囲)ごとに変わる。

レジスタ 用途
Jit.R0Jit.R5 汎用レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。
Jit.S0Jit.S5 汎用レジスタ。別関数呼び出し後に破棄されない保証。
Jit.FR0Jit.FR5 浮動小数点レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。
Jit.FS0Jit.FS5 浮動小数点レジスタ。別関数呼び出し後に破棄されない保証。

尚、Floating Point 用のレジスタは FR/FS 合わせて最大で 6 個までなので、FR4 まで使用した場合、FS0 しか使えません。FR5 まで使うと FS* は全て使えません。以下のような感じですのでご注意を。

FR* レジスタ FS* レジスタ
(使えない) FS0, FS1, FS2, FS3, FS4, FS5
FR0 FS0, FS1, FS2, FS3, FS4
FR0, FR1 FS0, FS1, FS2, FS3
FR0, FR1, FR2 FS0, FS1, FS2
FR0, FR1, FR2, FR3 FS0, FS1
FR0, FR1, FR2, FR3, FR4 FS0
FR0, FR1, FR2, FR3, FR4, FR5 (使えない)

Jit コンパイラ

Jit 命令を作っていくためには Jit コンパイラ・オブジェクトを作成する。

var c = new Jit.Compiler();

Jit コンパイラには以下のメソッドがある。

Jit コンパイラ・メソッド 復帰値 概要
Jit.Compiler#label() label 現在の場所にラベルを付加する。
Jit.Compiler#makeConst(reg, init) ConstTarget コード生成後に即値を設定するための仮定義コードを出力する。
Jit.Compiler#localp(dst, offset) ローカル変数の実アドレスを取得するコードを出力する。dst に示したレジスタに格納される。offset はローカル変数番号。
Jit.Compiler#enter(argType) label 関数の入り口を作成。引数タイプを指定できる(省略可)。
Jit.Compiler#fastEnter(reg) label 関数の入り口を作成。ただし、余計なエピローグ、プロローグは出力せず、復帰アドレスを reg に保存する。
Jit.Compiler#ret(val) Return コードを出力する。val を返す。val は浮動小数点数は FR0 レジスタ、それ以外は R0 レジスタで返却される。
Jit.Compiler#f2i(dst, op1) double を int64_t にキャストするコードを出力する。dst は汎用レジスタ。op1 は浮動小数点レジスタ。
Jit.Compiler#i2f(dst, op1) int64_t を double にキャストするコードを出力する。dst は浮動小数点レジスタ。op1 は汎用レジスタ。
Jit.Compiler#mov(dst, op1) dstop1 を代入するコードを出力する。浮動小数点とそれ以外の型は自動的に認識する。
Jit.Compiler#neg(dst, op1) op1 の符号反転した結果を dst に格納するコードを出力する。
Jit.Compiler#clz(dst, op1) op1 の先頭から 0 であるビットの数を数え、dst に格納するコードを出力する。
Jit.Compiler#add(dst, op1, op2) op1op2 を加算した結果を dst に格納するコードを出力する。
Jit.Compiler#sub(dst, op1, op2) op1op2 を減算した結果を dst に格納するコードを出力する。
Jit.Compiler#mul(dst, op1, op2) op1op2 を乗算した結果を dst に格納するコードを出力する。
Jit.Compiler#div(dst, op1, op2) 浮動小数点数のみ、op1op2 を除算した結果を dst に格納するコードを出力する。
Jit.Compiler#div() 汎用レジスタで符号なしとして除算した値を R0 レジスタに格納するコードを出力する。
Jit.Compiler#sdiv() 汎用レジスタで符号付きとして除算した値を R0 レジスタに格納するコードを出力する。
Jit.Compiler#divmod() 汎用レジスタで符号なしとして除算した値を R0 レジスタに格納し、余りを R1 レジスタに格納するコードを出力する。
Jit.Compiler#sdivmod() 汎用レジスタで符号付きとして除算した値を R0 レジスタに格納し、余りを R1 レジスタに格納するコードを出力する。
Jit.Compiler#not(dst, op1) op1 のビット反転した結果を dst に格納するコードを出力する。
Jit.Compiler#and(dst, op1, op2) op1op2 でビット AND した値を dst に格納するコードを出力する。
Jit.Compiler#or(dst, op1, op2) op1op2 でビット OR した値を dst に格納するコードを出力する。
Jit.Compiler#xor(dst, op1, op2) op1op2 でビット XOR した値を dst に格納するコードを出力する。
Jit.Compiler#shl(dst, op1, op2) op1op2 ビット分、左シフトした値を dst に格納するコードを出力する。
Jit.Compiler#lshr(dst, op1, op2) op1op2 ビット分、論理右シフトした値を dst に格納するコードを出力する。
Jit.Compiler#ashr(dst, op1, op2) op1op2 ビット分、算術右シフトした値を dst に格納するコードを出力する。
Jit.Compiler#call(label) JumpTarget enter() 定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。label を指定した場合は後から設定する必要はない。
Jit.Compiler#fastCall(label) JumpTarget fastEnter() で定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。
Jit.Compiler#jmp(label) JumpTarget jmp コマンドを出力する。label を指定した場合は後から設定する必要はない。
Jit.Compiler#ijmp(dst) JumpTarget jmp コマンドを出力する。dst はアドレスを示すレジスタ、または即値。
Jit.Compiler#eq(op1, op2) JumpTarget op1 == op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#neq(op1, op2) JumpTarget op1 != op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#lt(op1, op2) JumpTarget 符号なしとして op1 < op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#le(op1, op2) JumpTarget 符号なしとして op1 <= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#gt(op1, op2) JumpTarget 符号なしとして op1 > op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#ge(op1, op2) JumpTarget 符号なしとして op1 >= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#slt(op1, op2) JumpTarget 符号付きとして op1 < op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#sle(op1, op2) JumpTarget 符号付きとして op1 <= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#sgt(op1, op2) JumpTarget 符号付きとして op1 > op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#sge(op1, op2) JumpTarget 符号付きとして op1 >= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#generate() JitCode コード生成を行う。

Jit.Compiler#enter(argType)

関数の入り口を enter メソッドで定義するが、argType を指定しなかった場合は Jit.ArgType.SW_SW_SW を指定されたものとみなす。引数は 3 つまで(仕様)で、それぞれの型を指定する。

  • SW ... Signed Word (64bit)
  • UW ... Unsigned Word (64bit)
  • FP ... Floating Point (64bit)

実際問題としては受け取るレジスタのビット列が同じになるため SWUW は変わらないが、もしかしたら将来何か違いを出すかもしれない。尚、最後の引数から SW は省略可能。なので、以下は全部同じ意味となる。

  • Jit.ArgType.SW_SW_SW
  • Jit.ArgType.SW_SW
  • Jit.ArgType.SW

引数で渡されるレジスタは決まっており、以下のようになっている。

  • 呼び出し側
第 1 引数 第 2 引数 第 3 引数
Integer Jit.R0 Jit.R1 Jit.R2
Double Jit.FR0 Jit.FR1 Jit.FR2
  • 受け取り側
第 1 引数 第 2 引数 第 3 引数
Integer Jit.S0 Jit.S1 Jit.S2
Double Jit.FS0 Jit.FS1 Jit.FS2

呼び出し側でセットするレジスタと、受け取り側で受け取るレジスタが異なることに注意。

ConstTarget

ラベルアドレスを setLabel() で設定する。
ラベルのアドレスを即値としてレジスタやメモリに格納したいときに使う。あまり使う機会は無いか。ジャンプテーブルの代わりになるかとも思うが、テーブルの作り方がイマイチうまい仕組みを用意できていないので。

ちなみに、即値を setValue() で設定することもできるが、普通に Jit.IMM(100) とか、浮動小数点数でも Jit.IMM(0.1) とかできるようにしたので、こちらを使う意味はあまりない。

仮にジャンプテーブルに使うとした際の例は後述。

JumpTarget

ジャンプ先、もしくは関数コールのためのアドレスを setLabel() で設定する。

例えば、比較した結果で分岐させる場合、以下のようになる。

var c = new Jit.Compiler();
// 関数のエントリポイント。
c.enter();
// S0レジスタ値 >= 3
var jump0 = c.ge(Jit.S0, Jit.IMM(3));
... // 条件が偽のときのコード
var jump1 = c.jmp();
var label0 = c.label();
... // 条件が真のときのコード
var label1 = c.label();
...

jump0.setLabel(label0);
jump1.setLabel(label1);

JitCode

generate() メソッドでコード生成に成功すると、JitCode オブジェクト返る。JitCode オブジェクトのメソッドは以下の通り。尚、引数は 3 つまでしか指定できないことに注意(仕様)。抽象化アセンブラなので、様々なアーキテクチャに対応するために必要な仕様です。必要ならローカル変数領域を確保して、その先頭アドレスを渡すなどの工夫が必要。サンプルは後述。

メソッド 概要
JitCode#run(a1, a2, a3) 復帰値を Integer として受け取る。
JitCode#frun(a1, a2, a3) 復帰値を Double として受け取る。
JitCode#dump() ジェネレートされたアセンブル・リストを出力する。

サンプル

フィボナッチ数列(再帰版)

では、恒例のフィボナッチ数列を算出する再帰版のコードを書いてみましょう。サンプルとして最初に提示したものそのままです。

var c = new Jit.Compiler();
var entry1 = c.enter();
    var jump0 = c.ge(Jit.S0, Jit.IMM(3));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
    c.call(entry1);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
    c.call(entry1);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

結果は以下の通り。

[   0.000] fib( 1) = 1
[   0.000] fib( 2) = 2
[   0.000] fib( 3) = 3
[   0.000] fib( 4) = 5
[   0.000] fib( 5) = 8
[   0.000] fib( 6) = 13
[   0.000] fib( 7) = 21
[   0.000] fib( 8) = 34
[   0.000] fib( 9) = 55
[   0.000] fib(10) = 89
[   0.000] fib(11) = 144
[   0.000] fib(12) = 233
[   0.000] fib(13) = 377
[   0.000] fib(14) = 610
[   0.000] fib(15) = 987
[   0.000] fib(16) = 1597
[   0.000] fib(17) = 2584
[   0.000] fib(18) = 4181
[   0.000] fib(19) = 6765
[   0.000] fib(20) = 10946
[   0.000] fib(21) = 17711
[   0.000] fib(22) = 28657
[   0.000] fib(23) = 46368
[   0.000] fib(24) = 75025
[   0.000] fib(25) = 121393
[   0.001] fib(26) = 196418
[   0.001] fib(27) = 317811
[   0.001] fib(28) = 514229
[   0.002] fib(29) = 832040
[   0.002] fib(30) = 1346269
[   0.004] fib(31) = 2178309
[   0.006] fib(32) = 3524578
[   0.009] fib(33) = 5702887
[   0.016] fib(34) = 9227465
[   0.035] fib(35) = 14930352
[   0.042] fib(36) = 24157817
[   0.066] fib(37) = 39088169
[   0.119] fib(38) = 63245986
[   0.181] fib(39) = 102334155
[   0.289] fib(40) = 165580141
[   0.476] fib(41) = 267914296
[   0.773] fib(42) = 433494437

ちなみに、fib(42) の結果を Ruby, Python, PyPy, PHP, HHVM, Kinx, Kinx(native) で計測して比較してみた。JIT ライブラリ版は上記は run() の時間しか計ってないので、スクリプト解釈と JIT コード生成まで含め、全て公平にプロセス全体の user time で算出。

速い順に並べると以下の通り。やはり JIT で直接ネイティブ・コードを出力させると際立って速い。何気に Kinx(native) が PyPy より速かったのは嬉しい誤算。HHVM とどっこいくらい。スクリプトでは Ruby 速くなったなー。1.8 時代とか知ってると感慨深いですねー。

言語 版数 User 時間
Kinx(Jit-Lib) 0.10.0 0.828
HHVM 3.21.0 2.227
Kinx(native) 0.10.0 2.250
PyPy 5.10.0 3.313
PHP 7.2.24 11.422
Ruby 2.5.1p57 14.877
Kinx 0.10.0 27.478
Python 2.7.15+ 41.125

尚、先の JIT ライブラリでジェネレートされたアセンブル・リストはこちら。Windows と Linux で違うのだが、今回は Linux。

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 83 fb 03                                 cmp rbx, 0x3
      16:   73 0d                                       jae 0x25
      18:   48 89 d8                                    mov rax, rbx
      1b:   48 83 c4 10                                 add rsp, 0x10
      1f:   41 5e                                       pop r14
      21:   41 5f                                       pop r15
      23:   5b                                          pop rbx
      24:   c3                                          ret
      25:   48 8d 43 fe                                 lea rax, [rbx-0x2]
      29:   48 89 fa                                    mov rdx, rdi
      2c:   48 89 c7                                    mov rdi, rax
      2f:   e8 cc ff ff ff                              call 0x0
      34:   49 89 c7                                    mov r15, rax
      37:   48 8d 43 ff                                 lea rax, [rbx-0x1]
      3b:   48 89 fa                                    mov rdx, rdi
      3e:   48 89 c7                                    mov rdi, rax
      41:   e8 ba ff ff ff                              call 0x0
      46:   49 03 c7                                    add rax, r15
      49:   48 83 c4 10                                 add rsp, 0x10
      4d:   41 5e                                       pop r14
      4f:   41 5f                                       pop r15
      51:   5b                                          pop rbx
      52:   c3                                          ret

Const の例

Const の例として、あえて書くならこんな感じ。ローカル変数にジャンプテーブルを作っているので、毎回テーブルを作り直していてイマイチ。テーブルだけ作成してアドレスを渡せるようなインターフェースを別途用意すれば解決しそうではある(やるかも)。

var c = new Jit.Compiler();
c.enter();
    c.mov(Jit.R1, Jit.IMM(-1));
    var jump0 = c.slt(Jit.S0, Jit.IMM(0));
    var jump1 = c.sgt(Jit.S0, Jit.IMM(3));
    var const0 = c.makeConst(Jit.VAR(0));
    var const1 = c.makeConst(Jit.VAR(1));
    var const2 = c.makeConst(Jit.VAR(2));
    var const3 = c.makeConst(Jit.VAR(3));
    // ローカル変数のアドレスを S0 レジスタ(第一引数)のオフセットで取得し R0 レジスタに格納。
    c.localp(Jit.R0, Jit.S0);
    // ローカル変数の値自体を取得。
    c.mov(Jit.R0, Jit.MEM1(Jit.R0));
    // ローカル変数の中身をアドレスと見立ててジャンプ。
    c.ijmp(Jit.R0);
    var l0 = c.label();
    c.mov(Jit.R1, Jit.IMM(102));
    c.ret(Jit.R1);
    var l1 = c.label();
    c.mov(Jit.R1, Jit.IMM(103));
    c.ret(Jit.R1);
    var l2 = c.label();
    c.mov(Jit.R1, Jit.IMM(104));
    c.ret(Jit.R1);
    var l3 = c.label();
    c.mov(Jit.R1, Jit.IMM(105));
    var l4 = c.label();
    c.ret(Jit.R1);

// ジャンプアドレスはコード生成前にセットする。
jump0.setLabel(l4);
jump1.setLabel(l4);

var code = c.generate();
// const 値はコード生成後にセットする。
const0.setLabel(l0);
const1.setLabel(l1);
const2.setLabel(l2);
const3.setLabel(l3);

for (var i = -1; i < 5; ++i) {
    var r = code.run(i);
    System.println(r);
}

結果。

-1
102
103
104
105
-1

コード出力はこんな感じ。こちらは試しに Windows 版で出してみた。

       0:   53                                          push rbx
       1:   56                                          push rsi
       2:   57                                          push rdi
       3:   48 8b d9                                    mov rbx, rcx
       6:   48 8b f2                                    mov rsi, rdx
       9:   49 8b f8                                    mov rdi, r8
       c:   4c 8b 4c 24 b0                              mov r9, [rsp-0x50]
      11:   48 83 ec 50                                 sub rsp, 0x50
      15:   48 c7 c2 ff ff ff ff                        mov rdx, 0xffffffffffffffff
      1c:   48 83 fb 00                                 cmp rbx, 0x0
      20:   0f 8c 94 00 00 00                           jl 0xba
      26:   48 83 fb 03                                 cmp rbx, 0x3
      2a:   0f 8f 8a 00 00 00                           jg 0xba
      30:   49 b9 95 ff 57 61 89 01 00 00               mov r9, 0x1896157ff95
      3a:   4c 89 4c 24 20                              mov [rsp+0x20], r9
      3f:   49 b9 a7 ff 57 61 89 01 00 00               mov r9, 0x1896157ffa7
      49:   4c 89 4c 24 28                              mov [rsp+0x28], r9
      4e:   49 b9 b9 ff 57 61 89 01 00 00               mov r9, 0x1896157ffb9
      58:   4c 89 4c 24 30                              mov [rsp+0x30], r9
      5d:   49 b9 cb ff 57 61 89 01 00 00               mov r9, 0x1896157ffcb
      67:   4c 89 4c 24 38                              mov [rsp+0x38], r9
      6c:   48 8d 44 24 20                              lea rax, [rsp+0x20]
      71:   48 6b db 08                                 imul rbx, rbx, 0x8
      75:   48 03 c3                                    add rax, rbx
      78:   48 8b 00                                    mov rax, [rax]
      7b:   ff e0                                       jmp rax
      7d:   48 c7 c2 66 00 00 00                        mov rdx, 0x66
      84:   48 89 d0                                    mov rax, rdx
      87:   48 83 c4 50                                 add rsp, 0x50
      8b:   5f                                          pop rdi
      8c:   5e                                          pop rsi
      8d:   5b                                          pop rbx
      8e:   c3                                          ret
      8f:   48 c7 c2 67 00 00 00                        mov rdx, 0x67
      96:   48 89 d0                                    mov rax, rdx
      99:   48 83 c4 50                                 add rsp, 0x50
      9d:   5f                                          pop rdi
      9e:   5e                                          pop rsi
      9f:   5b                                          pop rbx
      a0:   c3                                          ret
      a1:   48 c7 c2 68 00 00 00                        mov rdx, 0x68
      a8:   48 89 d0                                    mov rax, rdx
      ab:   48 83 c4 50                                 add rsp, 0x50
      af:   5f                                          pop rdi
      b0:   5e                                          pop rsi
      b1:   5b                                          pop rbx
      b2:   c3                                          ret
      b3:   48 c7 c2 69 00 00 00                        mov rdx, 0x69
      ba:   48 89 d0                                    mov rax, rdx
      bd:   48 83 c4 50                                 add rsp, 0x50
      c1:   5f                                          pop rdi
      c2:   5e                                          pop rsi
      c3:   5b                                          pop rbx
      c4:   c3                                          ret

7b 行目の jmp rax がポイント。テーブルを静的に定義できるようになればジャンプテーブルとして機能するようになるかと(今は簡単にできる方法が無い...)。

4 つ以上の引数の例

ちょっと面倒くさいが、4 つ以上引数を渡したい場合は、ローカル変数領域に値を格納し、そのアドレス(ポインタ)を引数として渡す。以下の例では、最初に引数をローカル変数領域にセットするためのフック関数を経由させている。ちなみに、ローカル変数は全て 8 バイトで確保されるため、直接 Jit.MEM1() などでアクセスする場合のオフセットは 8 の倍数でないと合わないので注意。

var c = new Jit.Compiler();
var entry1 = c.enter();
    c.mov(Jit.VAR(0), Jit.S0);
    c.mov(Jit.VAR(1), Jit.IMM(3));
    c.mov(Jit.VAR(2), Jit.IMM(2));
    c.mov(Jit.VAR(3), Jit.IMM(1));
    c.localp(Jit.R0);
    var call1 = c.call();
    c.ret(Jit.R0);
var entry2 = c.enter();
    c.mov(Jit.R1, Jit.S0);
    c.mov(Jit.S0, Jit.MEM1(Jit.R1, 0));
    var jump0 = c.ge(Jit.S0, Jit.MEM1(Jit.R1, 8));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 16));
    c.mov(Jit.VAR(0), Jit.R3);
    c.mov(Jit.VAR(1), Jit.IMM(3));
    c.mov(Jit.VAR(2), Jit.IMM(2));
    c.mov(Jit.VAR(3), Jit.IMM(1));
    c.localp(Jit.R0);
    c.call(entry2);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 24));
    c.mov(Jit.VAR(0), Jit.R3);
    c.mov(Jit.VAR(1), Jit.IMM(3));
    c.mov(Jit.VAR(2), Jit.IMM(2));
    c.mov(Jit.VAR(3), Jit.IMM(1));
    c.localp(Jit.R0);
    c.call(entry2);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
call1.setLabel(entry2);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

出力はさっきと同じ。

Double の引数と復帰値

Double 紹介していないのでそれも。こちらもフィボナッチでいきましょう。しかし、俺はフィボナッチ大好きだな。気づかなかったけど。0.1 刻みバージョンです。

var c = new Jit.Compiler();
var entry1 = c.enter(Jit.ArgType.FP);
    c.mov(Jit.FR0, Jit.IMM(0.3));
    var jump0 = c.ge(Jit.FS0, Jit.FR0);
    c.ret(Jit.FS0);
    var l1 = c.label();
    c.mov(Jit.FR0, Jit.IMM(0.2));
    c.sub(Jit.FR0, Jit.FS0, Jit.FR0);
    c.call(entry1);
    c.mov(Jit.FS1, Jit.FR0);
    c.mov(Jit.FR0, Jit.IMM(0.1));
    c.sub(Jit.FR0, Jit.FS0, Jit.FR0);
    c.call(entry1);
    c.add(Jit.FR0, Jit.FR0, Jit.FS1);
    c.ret(Jit.FR0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 0.1; i < 3.5; i += 0.1) {
    var tmr = new SystemTimer();
    var r = code.frun(i);
    System.println("[%8.3f] fib(%3.1f) = %.1f" % tmr.elapsed() % i % r);
}

浮動小数点数の即値は直接比較メソッドで使えるようにしていないので(すればいいんだけど)一旦レジスタに格納して使う必要がある。

frun() することで Double 値を受け取れる。結果は以下の通り。

[   0.000] fib(0.1) = 0.1
[   0.000] fib(0.2) = 0.2
[   0.000] fib(0.3) = 0.3
[   0.000] fib(0.4) = 0.5
[   0.000] fib(0.5) = 0.8
[   0.000] fib(0.6) = 1.3
[   0.000] fib(0.7) = 2.1
[   0.000] fib(0.8) = 3.4
[   0.000] fib(0.9) = 5.5
[   0.000] fib(1.0) = 8.9
[   0.000] fib(1.1) = 14.4
[   0.000] fib(1.2) = 23.3
[   0.000] fib(1.3) = 37.7
[   0.000] fib(1.4) = 61.0
[   0.000] fib(1.5) = 98.7
[   0.000] fib(1.6) = 159.7
[   0.000] fib(1.7) = 258.4
[   0.000] fib(1.8) = 418.1
[   0.000] fib(1.9) = 676.5
[   0.000] fib(2.0) = 1094.6
[   0.000] fib(2.1) = 1771.1
[   0.000] fib(2.2) = 2865.7
[   0.000] fib(2.3) = 4636.8
[   0.000] fib(2.4) = 7502.5
[   0.000] fib(2.5) = 12139.3
[   0.001] fib(2.6) = 19641.8
[   0.001] fib(2.7) = 31781.1
[   0.002] fib(2.8) = 51422.9
[   0.003] fib(2.9) = 83204.0
[   0.004] fib(3.0) = 134626.9
[   0.006] fib(3.1) = 217830.9
[   0.015] fib(3.2) = 352457.8
[   0.020] fib(3.3) = 570288.7
[   0.027] fib(3.4) = 922746.5

出力コードは以下の通り。これも Windows 版。浮動小数点数を引き渡すために、最初に簡単なフック関数がある。SLJIT は関数のエントリポイントでの引数で浮動小数点数を指定できないので、こういう形で回避している。

そういう意味でも、SLJIT を直接使うよりこっちを使うほうが色々面倒見てくれて良い。ローカル変数領域で必要なサイズを自動的に計算したり、非破壊レジスタのための一時保存コードなども必要な数行うように自動で計算もさせているので。

       0:   53                                          push rbx
       1:   56                                          push rsi
       2:   57                                          push rdi
       3:   48 8b d9                                    mov rbx, rcx
       6:   48 8b f2                                    mov rsi, rdx
       9:   49 8b f8                                    mov rdi, r8
       c:   4c 8b 4c 24 d0                              mov r9, [rsp-0x30]
      11:   48 83 ec 30                                 sub rsp, 0x30
      15:   0f 29 74 24 20                              movaps [rsp+0x20], xmm6
      1a:   f2 0f 10 03                                 movsd xmm0, qword [rbx]
      1e:   48 89 f2                                    mov rdx, rsi
      21:   49 89 f8                                    mov r8, rdi
      24:   48 89 c1                                    mov rcx, rax
      27:   e8 0d 00 00 00                              call 0x39
      2c:   0f 28 74 24 20                              movaps xmm6, [rsp+0x20]
      31:   48 83 c4 30                                 add rsp, 0x30
      35:   5f                                          pop rdi
      36:   5e                                          pop rsi
      37:   5b                                          pop rbx
      38:   c3                                          ret
      39:   53                                          push rbx
      3a:   56                                          push rsi
      3b:   57                                          push rdi
      3c:   48 8b d9                                    mov rbx, rcx
      3f:   48 8b f2                                    mov rsi, rdx
      42:   49 8b f8                                    mov rdi, r8
      45:   4c 8b 4c 24 b0                              mov r9, [rsp-0x50]
      4a:   48 83 ec 50                                 sub rsp, 0x50
      4e:   0f 29 74 24 20                              movaps [rsp+0x20], xmm6
      53:   f2 0f 11 6c 24 38                           movsd [rsp+0x38], xmm5
      59:   f2 0f 10 f0                                 movsd xmm6, xmm0
      5d:   49 b9 33 33 33 33 33 33 d3 3f               mov r9, 0x3fd3333333333333
      67:   4c 89 4c 24 40                              mov [rsp+0x40], r9
      6c:   f2 0f 10 44 24 40                           movsd xmm0, qword [rsp+0x40]
      72:   66 0f 2e f0                                 ucomisd xmm6, xmm0
      76:   73 17                                       jae 0x8f
      78:   f2 0f 10 c6                                 movsd xmm0, xmm6
      7c:   f2 0f 10 6c 24 38                           movsd xmm5, qword [rsp+0x38]
      82:   0f 28 74 24 20                              movaps xmm6, [rsp+0x20]
      87:   48 83 c4 50                                 add rsp, 0x50
      8b:   5f                                          pop rdi
      8c:   5e                                          pop rsi
      8d:   5b                                          pop rbx
      8e:   c3                                          ret
      8f:   49 b9 9a 99 99 99 99 99 c9 3f               mov r9, 0x3fc999999999999a
      99:   4c 89 4c 24 40                              mov [rsp+0x40], r9
      9e:   f2 0f 10 44 24 40                           movsd xmm0, qword [rsp+0x40]
      a4:   f2 0f 10 e6                                 movsd xmm4, xmm6
      a8:   f2 0f 5c e0                                 subsd xmm4, xmm0
      ac:   f2 0f 11 e0                                 movsd xmm0, xmm4
      b0:   48 89 c1                                    mov rcx, rax
      b3:   e8 81 ff ff ff                              call 0x39
      b8:   f2 0f 10 e8                                 movsd xmm5, xmm0
      bc:   49 b9 9a 99 99 99 99 99 b9 3f               mov r9, 0x3fb999999999999a
      c6:   4c 89 4c 24 40                              mov [rsp+0x40], r9
      cb:   f2 0f 10 44 24 40                           movsd xmm0, qword [rsp+0x40]
      d1:   f2 0f 10 e6                                 movsd xmm4, xmm6
      d5:   f2 0f 5c e0                                 subsd xmm4, xmm0
      d9:   f2 0f 11 e0                                 movsd xmm0, xmm4
      dd:   48 89 c1                                    mov rcx, rax
      e0:   e8 54 ff ff ff                              call 0x39
      e5:   f2 0f 58 c5                                 addsd xmm0, xmm5
      e9:   f2 0f 10 6c 24 38                           movsd xmm5, qword [rsp+0x38]
      ef:   0f 28 74 24 20                              movaps xmm6, [rsp+0x20]
      f4:   48 83 c4 50                                 add rsp, 0x50
      f8:   5f                                          pop rdi
      f9:   5e                                          pop rsi
      fa:   5b                                          pop rbx
      fb:   c3                                          ret

おわりに

JIT 面白いですね。これでパーサ・コンビネータとか実装して組み合わせたらたら、ちょっとした JIT 付き言語処理系が作れますね。そういう道を目指してもいいかも。

おそらく想定する使い方としては次の 2 通りでしょうか。

  1. Kinx のライブラリ作成の際に数値計算等の範囲で JIT 化し、高速化する。
  2. DSL(ドメイン固有言語)やオレオレ言語のホストとなり、バックエンドの出力に使う。

ではまた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】レイアウトテンプレートの備忘録

初稿です。不備がありましたらご指摘いただけると幸いです。

Ruby on Railsを勉強中です。ビューファイルでコーディングした内容が、なぜDOCTYPE宣言やHTMLタグで囲ってないのにブラウザできちんと表示されるか不明だったのですが、レイアウトテンプレートを理解してスッキリしたので備忘録として残しておきます。

レイアウトテンプレートとは?

railsでアプリケーションを作成した際に、以下のディレクトリに自動で作成されるファイルです。
app/views/layouts/application.html.erb

通常、コントローラーで定義したアクションから、ビューファイルがある場合はそのファイルが呼ばれているように見えますが、実際はレイアウトテンプレートの中に作成したビューファイルが埋め込まれて返しているとのこと。

例)postsというコントローラーを作成した場合
app/controllers/posts_controller.rb

posts_controller.rb
class PostsController < ApplicationController
  def index
  end
end

app/views/posts/index.html.erb

index.html.erb
<h1>トップページ</h1>

実際、レスポンスとして返しているのは、下記テンプレートファイルの

タグ内にある<%= yield %>に呼び出されたビューファイルindex.html.erbが埋め込まれて返されているとのこと。

app/views/layouts/application.html.erb

application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>FirstApp</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

index.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】画像プレビュー機能の実装

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装

実装

1.ビューを編集

users/edit.html.slim
/ 追記
= f.file_field :profile_image, class: 'img_field', style: 'display:none;'
= attachment_image_tag @user, :profile_image, fallback: 'no-image.png', onClick: "$('.img_field').click()", class: 'center-block img-thumbnail img-responsive img_prev'
br

【解説】

file_fielddisplay:noneの非表示にし、クラスを付与する。

= f.file_field :profile_image, class: 'img_field', style: 'display:none;'

で付与したクラスのHTML(file_field)がクリックされたらJavaScriptの処理を実行する。

onClick: "$('.img_field').click()"

2.appliacation.scssを編集

appliacation.scss
// 追記
.img_prev:hover {
  cursor: pointer; 
  opacity: 0.7;
  transform: scale(1.01, 1.01);
}

【解説】

① 画像にマウスが乗っている時にCSSを反映させる。

.img_prev:hover {}

② マウスカーソルをポインターに変更する。

cursor: pointer; 

③ 不透明度を低くして、画像を少し白くする。

opacity: 0.7;

④ 少しだけ画像を拡大する。

transform: scale(1.01, 1.01);

3.JavaScriptファイルを作成・編集

ターミナル
$ touch app/assets/javascripts/image_preview.js
image_preview.js
$(function () {
  function readURL(input) {
    if (input.files && input.files[0]) {
      var reader = new FileReader();
      reader.onload = function (e) {
        $('.img_prev').attr('src', e.target.result);
      }
      reader.readAsDataURL(input.files[0]);
    }
  }

  $('.img_field').change(function () {
    readURL(this);
  });
});

【解説】

1.ビューを編集の② で実行する処理。

$('.img_field').change(function () {
  readURL(this);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】stub_request(:post)でスタブ化したGoogle APIへのリクエストのレスポンスを実際と同一のレスポンス形式にする

タイトルが長い

RailsアプリケーションからGoogle Calendar APIでカレンダー上のイベントに対して更新処理を行い、そのレスポンスから特定の属性を取り出して取り回すような処理を追加した。
レスポンスで受け取ったJSONをgoogle-api-ruby-clientがGoogle::API::V3::Eventクラスのオブジェクトに変換してしまうため、スタブリクエストのレスポンスもruby-clientで変換させるか同様の構造を持ったオブジェクトにする必要があった。

TL;DR

  • google-api-ruby-clientを利用してGoogleAPIにリクエストした場合、レスポンスはJSON形式で返ってきたあとgoogle-api-ruby-clientで変換される
  • stub_requestからの戻り値のヘッダーに 'X-Goog-Upload-Status': 'final' を加えると実際のAPIからのレスポンスと同様の形式になる

スタブ化したリクエストと実際にAPIにリクエストした場合のレスポンスの違い

下記のコードはgoogle-api-ruby-clientを利用してリクエストし、レスポンスからイベントのIDを取得しようとしている

リクエスト

service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = access_token

response = service.update_event('primary', event_id, body)
event_id = response.id

このとき response は以下のようになっている。

#<Google::Apis::CalendarV3::Event:0x000055c94da58f18
 @attachments=[],
 @attendees=[#<Google::Apis::CalendarV3::EventAttendee:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @organizer=true, @self=true>],
 @created=Mon, 01 Jan 2020 00:00:00 +0000,
 @creator=#<Google::Apis::CalendarV3::Event::Creator:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @self=true>,
 @description="",
 @end=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>,
 @etag="xxxxxxxxxxxxxxxxxxx",
 @id="abcdefghijklmnopqrstu",
 @organizer=#<Google::Apis::CalendarV3::Event::Organizer:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com">,
 @start=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>,
 @updated=Mon, 01 Jan 2020 00:00:00 +0000>

このため、response オブジェクトから ゲッターメソッドの id で値を取得できる。

一方で大抵の場合、RSpec内でAPIレスポンスは下記のように定義されているだろう。

let(:response) do
  {
    id: 'abcdefghijklmnopqrstu',
    start: { date_time: Time.zone.today },
    end: { date_time: Time.zone.today },
    html_link: 'https://www.google.com/calendar/event?eid=xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    i_cal_uid: 'xxxxxxxxxxxxxxxxxxx@google.com',
    summary: 'event name',
    updated: Time.zone.today,

    ...

  }.to_json
end

この場合イベントIDを取得するためには JSON.parse(response)['id'] のようにする必要があり、 response.id はNoMethodErrorとなる。

このJSON形式のレスポンスをEventクラスのオブジェクトとして返させるためには以下のように 'X-Goog-Upload-Status': 'final' をレスポンスのヘッダーに付与する必要がある。

stub_request(
  :put, url
).to_return(
  status: 201,
  body: response,
  headers: {
    content_type: 'application/json',
    'X-Goog-Upload-Status': 'final'
  }
)

google-api-ruby-clientのテストスペック内でもレスポンスに同様のヘッダー情報が付与されている

たまたま同僚がすぐに記事やGitHubのissueを見つけてくれたおかげで解決したけどそうじゃなかったら3日はハマってた気がする。

元ネタ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】stub_request(:post)でスタブ化したGoogle APIへのリクエストのレスポンスを実際と同一の形式にする

タイトルが長い

RailsアプリケーションからGoogle Calendar APIでカレンダー上のイベントに対して更新処理を行い、そのレスポンスから特定の属性を取り出して取り回すような処理を追加した。
レスポンスで受け取ったJSONをgoogle-api-ruby-clientがGoogle::API::V3::Eventクラスのオブジェクトに変換してしまうため、スタブリクエストのレスポンスもruby-clientで変換させるか同様の構造を持ったオブジェクトにする必要があった。

TL;DR

  • google-api-ruby-clientを利用してGoogleAPIにリクエストした場合、レスポンスはJSON形式で返ってきたあとgoogle-api-ruby-clientで変換される
  • stub_requestからの戻り値のヘッダーに 'X-Goog-Upload-Status': 'final' を加えると実際のAPIからのレスポンスと同様の形式になる

スタブ化したリクエストと実際にAPIにリクエストした場合のレスポンスの違い

下記のコードはgoogle-api-ruby-clientを利用してリクエストし、レスポンスからイベントのIDを取得しようとしている

リクエスト

service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = access_token

response = service.update_event('primary', event_id, body)
event_id = response.id

このとき response は以下のようになっている。

#<Google::Apis::CalendarV3::Event:0x000055c94da58f18
 @attachments=[],
 @attendees=[#<Google::Apis::CalendarV3::EventAttendee:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @organizer=true, @self=true>],
 @created=Mon, 01 Jan 2020 00:00:00 +0000,
 @creator=#<Google::Apis::CalendarV3::Event::Creator:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com", @self=true>,
 @description="",
 @end=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>,
 @etag="xxxxxxxxxxxxxxxxxxx",
 @id="abcdefghijklmnopqrstu",
 @organizer=#<Google::Apis::CalendarV3::Event::Organizer:xxxxxxxxxxxxxxxxxxx @email="hoge@gmail.com">,
 @start=#<Google::Apis::CalendarV3::EventDateTime:xxxxxxxxxxxxxxxxxxx>,
 @updated=Mon, 01 Jan 2020 00:00:00 +0000>

このため、response オブジェクトから ゲッターメソッドの id で値を取得できる。

一方で大抵の場合、RSpec内でAPIレスポンスは下記のように定義されているだろう。

let(:response) do
  {
    id: 'abcdefghijklmnopqrstu',
    start: { date_time: Time.zone.today },
    end: { date_time: Time.zone.today },
    html_link: 'https://www.google.com/calendar/event?eid=xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    i_cal_uid: 'xxxxxxxxxxxxxxxxxxx@google.com',
    summary: 'event name',
    updated: Time.zone.today,

    ...

  }.to_json
end

この場合イベントIDを取得するためには JSON.parse(response)['id'] のようにする必要があり、 response.id はNoMethodErrorとなる。

対応策

このJSON形式のレスポンスをEventクラスのオブジェクトとして返させるためには以下のように 'X-Goog-Upload-Status': 'final' をレスポンスのヘッダーに付与する必要がある。

stub_request(
  :put, url
).to_return(
  status: 201,
  body: response,
  headers: {
    content_type: 'application/json',
    'X-Goog-Upload-Status': 'final'
  }
)

google-api-ruby-clientのテストスペック内でもレスポンスに同様のヘッダー情報が付与されている

たまたま同僚がすぐに記事やGitHubのissueを見つけてくれたおかげで解決したけどそうじゃなかったら3日はハマってた気がする。

元ネタ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]seedファイルごとにデータを管理する

某フリマアプリのクローンサイトをチームで作成中です。
その時に、seedファイルの扱いに少し困ったので備忘録として残しておきます。

したいこと

商品を出品する時に、「商品の状態」「配送料」..etcなんかは、
データベースに先に値を入れておいて、プルダウンから選択できるようにする

こんな感じ
スクリーンショット 2020-06-29 17.41.45.png

実装

1. rake taskの作成

lib/tasks/直下にseed.rakeを作成し、以下を記載

lib/tasks/seed.rake
 Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file|
   desc "Load the seed data from db/seeds/#{File.basename(file)}.
   task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do
     load(file)
   end
 end

2. それぞれのseedファイルを作成

db/seedsディレクトリを作成し、そこに「データを入れたいモデル名.rb」のファイルを作成
今回はとりあえず、商品の状態を表すCondition.rbを作成

db/seeds/Condition.rb
conditions = Condition.create([
  {condition: "新品・未使用"},
  {condition: "未使用に近い"},
  {condition: "目立った傷や汚れなし"},
  {condition: "やや傷や汚れあり"},
  {condition: "傷や汚れあり"},
  {condition: "全体的に状態が悪い"}])

3. 実行

あとは実行するだけなので、以下のコマンドをターミナルに

bundle exec rake  db:seed:condition

conditionsテーブルにデータを入れられました!
スクリーンショット 2020-06-29 18.18.27.png

が、問題発生・・・

ローカルの環境ならこのままで良かったのですが、本番環境にデプロイする際に
作成したそれぞれのseedファイルの読み込ませ方が分からず、データを反映できませんでした

解決法

seeds.rbに以下の記述を追加

seeds.rb
require "./db/seeds/condition.rb

本番環境のターミナルにて下記のコマンドを実行したらきちんと反映しました

require "./db/seeds/condition.rb

終わりに

active hashを使えばもうちょっと楽にできたりしたのかな〜なんて思いますが、
納期もあるので一旦この方法で実装しました
他にいい方法があれば教えていただきたいです

参考記事

Railsでseedデータを分割して実行できるようにする
Rails・seedファイルを分割して管理する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

To install the missing version, run `gem install bundler:2.1.4`と出たときの対処法

日々学んだことやぶち当たったエラーについてまとめていきます。
記載に誤りがありましたら、ご指摘していただけると助かります!
いつも他のかたの記事に助けられているので、少しでもお役に立てればと思います。

どういうエラーなのか

Gemfile.lockにbundler 2.1.4が指定しているが、そのバージョンのbundlerが見当たらないというエラー。

Traceback (most recent call last):
    2: from 
    1: from 
#省略
Could not find 'bundler' (2.1.4) required by your /Users/<ユーザー名>/<ルートディレクトリ名>/Gemfile.lock. (Gem::GemNotFoundException)
To update to the latest version installed on your system, run `bundle update --bundler`.
To install the missing version, run `gem install bundler:2.1.4`

どんなときに起こるのか

Rubyのバージョンの変更をしたときなどに起こる。rbenvではRubyのバージョンごとにgemをインストールする必要があるとのこと。

対処法

gem install bundler -v 2.1.4

と入力して、bundler 2.1.4を入れたら解決!

参考

https://qiita.com/kodai_0122/items/c4c13e89dd5c4cba1f32
https://qiita.com/YumaInaura/items/64e5721549e4927ce85f
https://k-koh.hatenablog.com/entry/2020/01/27/200511

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bcrypt導入時のエラーたち

bcrypt導入時のエラー

パスワードを暗号化するためのgem, bcryptを導入する際に起こった、小さなエラーの解決法まとめです

その1 An error occurred while installing bcrypt, and Bundler cannot continue.

Gemfile
gem 'bcrypt'

と書き足し、bundle install を実行したところ、
5A9EC28E-C920-4220-8E26-235FCFFCA819_4_5005_c.jpeg
An error occurred while installing bcrypt (3.1.13), and Bundler
cannot continue.
というエラーが発生。
続けて、エラーメッセージ内に書いてある、
gem install bcrypt -v'3.1.13'
を実行したところ、今度はPermission denied が発生

Permissionということはsudoの出番ではないかと下記のコードを実行

$ sudo gem install bcrypt

これで無事bundle install 時のエラーが消え、bcryptを追加できました!

その2 cannot load such file -- bcrypt

無事、エラー1を克服し、localhostのブラウザを再起動したにも関わらず、このエラー

そういえば、このような時昔あったなと思い出し、 rails s を再実行。

すると通りました!!

今回はbcrypt導入時の小さいエラーもろもろでした〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecordを使うときは頭にSQLを描こう

みなさん、ActiveRecordを使う時に頭にSQLが浮かんでいますか?

ActiveRecordはとても便利でSQLを意識することなくDBにアクセスしてデータを取得したり、更新したりすることができます。
そのためついつい実装時にSQLを意識せずに書いてしまうことがあります。

実装時はActiveRecordを使うことで素のSQLを意識しなくてもよいのですが、最終的に実行する時にはSQLが実行されています。
そのため実際に発行されるSQLをみると、こんなSQL発行されるのと驚くことがあります。

SQLを思い浮かべながら書いていたら回避できる実装例をいくつか挙げてみます。

無駄なテーブルをJOINしている

下記のようなモデルがあったとします。

def User < ApplicationRecord
  has_many :user_organizations
end

def UserOrganization < ApplicationRecord
  belongs_to :user
  belongs_to :organization
end

def Organization < ApplicationRecord
  has_many :user_organizations
end

『organization_idで絞り込んだuserモデルを取得してください。』と言われた時にどのように実装しますか?

target_organization_id = 1
users = User.join(user_organizations: :organization)
  .where(user_organizations: { organization: { id: target_organization_id })

この実装は指定された通りに正しく動作します。発行されるSQLは下記の通り

SELECT `users`.*
FROM `users`
  INNER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id`
  INNER JOIN `organizations` ON `organizations`.`id` = `user_organizations`.`organization_id`
WHERE `organization`.`id` = 1

このSQLをみてどう思いますか?
よく考えるとorganizationsテーブルをJOINしなくてもできることに気づくと思います。
改善後のSQLは下記の通り。

SELECT `users`.*
FROM `users`
  INNER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id`
WHERE `user_organizations`.`organization_id` = 1

これを実現するActiveRecordは下記の通り。

target_organization_id = 1
users = User.join(:user_organizations)
  .where(user_organizations: { organization_id: target_organization_id })

ActiveRecordのモデル中心に実装を考えると、最初の実装のようについ指定されたidがあるモデルまでJOINしてしまいがちです。
実際にコードレビューをしていてもこのような実装はよく見かけます。
SQLはJOINが少なければ少ないほどパフォーマンスはよくなるので、できる限りJOINが少なくて済むように意識してActiveRecordを実装するようにしましょう。

eager_loadのLEFT OUTER JOIN

先ほどと同様に『organization_idで絞り込んだuserモデルを取得してください。』に加えて、後にuser.exam_organizationを使いたいのでキャッシュしておきたい場合、どのように実装しますか?

先ほどの実装のままだと、exam_organizationsがキャッシュされていないのでexam_organizationを取得するたびにSQLが発行されてしまい、N+1になってしまいます。
そこで下記のようにjoinsをeager_load(またはincludes)に変更することでキャッシュされるようになります。

target_organization_id = 1
users = User.eager_load(:user_organizations)
  .where(user_organizations: { organization_id: target_organization_id })

これで無事キャッシュされるようになるのですが、発行されるSQLをみてみるとINNER JOINがLEFT OUTER JOINに変わってしまっていることに気づきます。

SELECT `users`.id AS t0_r0, ...(全カラム列挙される。長いので省略)
FROM `users`
  LEFT OUTER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id`
WHERE `user_organizations`.`organization_id` = 1

SQLを考えずにRailsを書いている場合、INNER JOINで良いところが今回の例のようにLEFT OUTER JOINになっていても気にしないことが多い気がします。
ただSQLが頭に浮かんでいると、必ずデータがある結合なのにLEFT OUTER JOINを選ぶことはあり得ないので違和感しかないです。
このような場合は下記のようにjoinsも追記することでINNER JOINで結合しつつデータをキャッシュすることができます。

target_organization_id = 1
users = User.eager_load(:user_organizations).joins(:user_organizations)
  .where(user_organizations: { organization_id: target_organization_id })

最後に

いくつか例を上げてみましたが、どちらもSQLを頭に浮かべながら書いていたら簡単に避けれるようなものばかりです。
多少非効率なことをしていても大抵の場合は問題なく動くので気づかないことが多いですが、積み重ねでパフォーマンスに差が出たりするので、これまでActiveRecordを使う時にSQLを意識してこなかった方も発行されるSQLを意識してみると良いと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Ransackでセレクトボックスを使用する方法

はじめに

Ransackで、セレクトボックスを使用する方法についてまとめていきます。

selectについて

まずは、セレクトボックスを作るためのselectヘルパーについて、説明をしていきます。

基本型

select(オブジェクト, プロパティ名, 要素情報, オプション, 要素属性)

実装例

<%= f.select :name, [['sample1', 1], ['sample2', 2], ['sample3', 3]],
 {include_blank: '選択なし'}, class: 'sample' %>

(form_forの中に入っているとして、オブジェクトは入れていません。)
このように、第三引数に配列を入れると、パラメータとしてvalueの1や2が送信されます

便利なヘルパー

さらにセレクトボックスをカスタマイズしていくための便利なヘルパーがあります。

options_for_select

セレクトボックスの初期値を設定したいときはoptions_for_selectを使用します。

基本形

options_for_select(配列/ハッシュ, オプション)

実装例

<%= f.select :name, options_for_select({sample1: 1, sample2: 2, sample3: 3}, 1),
 include_blank: true %>

options_for_selectの第2引数に「1」がはいっているので、1がデフォルト値として表示されます。

options_from_collection_for_select

モデルから選択肢を作成したいときは、options_from_collection_for_selectを使用すると便利です。

基本形

options_from_collection_for_select(オブジェクトの配列, value属性, text項目 , オプション])

実装例

<%= f.select :name, options_from_collection_for_select(User.all, :id, :name , 1) %>

これで、Userモデルの中にあるnameを自動的にセレクトボックスにすることが出来ます。

Ransackでの使用方法

options_from_collection_for_selectを例として使用します。

実装例

<%= search_form_for(@q, url: users_path, local: true) do |f| %>

  <%= f.select :name_eq, options_from_collection_for_select(User.all, :id, :name , 1) %>

  <%= f.button ' 検索する' %>

<% end %>

以上のようにすることで、Userモデル内のnameをセレクトボックスにして、検索することが出来ます。
また、オプションで初期値も設定されています。

name_eq_eqの部分はmatcherと呼ばれ、等しい値を検索できます。

Matcherについて

他にも以下のmatcherがあります。

matcher      意味
_eq 等しい
noteq 等しくない
_cont 値を含む(LIKE)
_iteq 以下
_gteq 以上

詳しくはこちらの「Search Matchers」の項目をご覧ください。

参考

https://railsguides.jp/form_helpers.html
https://shinmedia20.com/rails-select-box

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのクラスメソッドって何?

はじめに

Rubyでつまずいた項目のひとつ、クラスメソッド。
わからなかったことを書いていきます。

クラスとインスタンスについてはこちら

メソッドとは

前提条件として、メソッドとは何か。
メソッドとは、データが持つ「動作」です。

車に当てはめて考えてみると、
車というデータが持つ動作は「走る、止まる、ウィンカーを出す」などです。

クラスメソッドとは

クラスメソッドを定義したクラス自身が使えるメソッド。
クラスで共通の情報を持った処理に使うことができます。
定義の方法は他のメソッドと同じですが、メソッド名の前に.selfをつけます。

test.rb
class Car
  def self.run # メソッド名の前にselfをつけてクラスメソッドを定義します
    puts "走ります"
  end
end

Car.run # クラス名.メソッド名で、定義したクラスメソッドを実行できます

(2020.6.29追記。ご指摘ありがとうございます:bow_tone1:
もしくは class << self から end の間にメソッドを書く記法もあります。
ネストは1段深くなりますが、クラスメソッドをたくさん定義したい場合はメソッド名の前に毎回.selfをつけなくても済みます。

test.rb
class Car
  class << self
    def run
      puts "走ります"
    end
  end
end

最後に

クラスメソッドの注意点は以下の通り。

・定義するときには.selfをメソッド名の前につける。
・もしくは class << self から end の間にメソッドを書く記法もある。
・このメソッドは、クラスのみに用いることができます。

最後まで読んでいただきまして、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む