Slimを.rbファイルの中に書く (Railsネタ)
タイトル通りの話です。
次のようにすればできますが、効率は悪いような気がします。
engine = Slim::Engine.new eval engine.call('a href="http://www.google.com" Google') #=> <a href="http://www.google.com">Google</a>
CRuby(MRI)はスレッドセーフなのか
少々炎上しそうな表題ですが、色々と調べたので覚書。
まず、前提条件として、CRubyはGIL(Global Interpreter Lock)という仕組みがあります。
細かい説明は
グローバルインタプリタロック - Wikipedia
を参照いただくとして、Rubyインタプリタは必ずGILを取得する必要があります。
となると、Thread意味無いじゃんと思ってしまうのですが、RubyにおけるGIL=GVL(Giant VM Lock)は、class Threadにあるように、IO関連処理のみ並列実行してくれます。
ネイティブスレッドを用いて実装されていますが、 現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行される ネイティブスレッドは常にひとつです。 ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。その場合にはスレッドは同時に実行され得ます。 また拡張ライブラリから GVL を操作できるので、複数のスレッドを 同時に実行するような拡張ライブラリは作成可能です。
また、CRuby以外の実装について言及しますと、JRuby,RubiniusともにGILは存在せず、並列実行が可能です。
それでは、本題に戻りまして、CRubyがスレッドセーフなのかについて考えていきたいと思います。
まず、
Ruby core classes aren't thread-safe
を見ますと、Hashへのデクリメント操作の結果が、
MRI 1.9.3 : 0
JRuby 1.7.2 : 486
RBX 2.0.0rc1 : 2
となってきており、CRubyのHashはスレッドセーフに実行されていることがわかります。
(内容を端折っていますが、Hashの1つの要素に4000という値を入れて、マルチスレッドで4000回デクリメントした結果になります。)
この結果から、引用先の記事には次のような記述があります。
This is what the tweet alluded to. Our experience with MRI may lead us to think that the core classes are thread-safe, but this is an 'accident' of MRI that really shows itself as an accident when you run the same code on other runtimes
要するに、たまたまCRubyはスレッドセーフ的に動いてますよということですね。
では、次に
Nobody understands the GIL - Part 2: Implementation
を見てみますと、Charles Nutter氏(JRuby開発者)がコメントで次のように述べています。
Put simply, only standalone C functions are "atomic" in MRI.
なるほど、C言語実装の関数単独ではatomicということですね。
C functionsがatomicということは、CRubyはスレッドセーフなのか....そうはいかないらしいです。
Does the GIL Make Your Ruby Code Thread-Safe?
を見てみます。
引用先に次のようなコードが有ります。
class Sheep def initialize @shorn = false end def shorn? @shorn end def shear! puts "shearing..." @shorn = true end end sheep = Sheep.new 5.times.map do Thread.new do unless sheep.shorn? sheep.shear! end end end.each(&:join)
元記事を読んでいただくほうが良いと思いますが、私なりにコードの説明をしますと、
Sheepクラスは、shornメソッドとshear!メソッドがあります。
毛を刈り込まれていない羊(!shorn?)が存在すれば、毛を刈り込みます(shear!)。
このコードの実装者の意図は、一匹の羊は、一度しか刈り込み(share!)を実行できないということになります。
しかし、このコードを実行すると、1つの羊に対してshear!が複数回呼ばれてしまうことがあります。
その理由は、shorn?とshare!が同期実行されないからです。
shorn?とshare!の同期実行を保証するためには、threadのQueueクラスを利用する必要があります。(引用先を参照ください。)
また、必ず shorn -> share! の順に実行されるのであれば、Mutexを導入すれば次のように解決できます。
class Sheep def initialize @shorn = false @mutex= Mutex.new end def shorn? @mutex.lock @shorn end def shear! puts "shearing..." @shorn = true @mutex.unlock end end
色々と駄文を書いてきましたが、まとめますと、
・CRubyにおいて、C言語実装ライブラリ(Core Class、C拡張)はスレッドセーフで動作する。但し、GILの副次的な効果であり、今後どうなるかは分からない。
・一方で、Rubyで実装されたライブラリはスレッドセーフとならない可能性がある。
・ちなみに、Railsはスレッドセーフに動作できるので、Core Class + Railsが提供する関数は全てスレッドセーフで動作可能である。
C#のiEnumerable#SelectはEnumerator::Lazy#mapだった件
C#は.net framework 3.5より、LINQ用のEnumerableメソッドが追加されています。
Select,Where,OrderByなどです。
これらのメソッドは、LINQでの使用はもちろん、ラムダ式を渡して、利用することができます。これは、Rubyのmapやfilterに当たるものになります。
参考:http://www.atmarkit.co.jp/ait/articles/1107/22/news141.html
ここで、iEnumerable#Selectのザックリとした実装を見てみますと、以下のようになっています。
public static IEnumerable<TResult> Select<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, TResult> block) { foreach (TSource e in source) yield return block(e); }
ここで、問題になるのが、yield return block(e);の部分になります。この構文は、実際にその要素が利用されるまで、blockを実行しません。すなわち、遅延評価となっています。LINQの実装から考えると当然ですね。
参考:http://ufcpp.net/study/csharp/sp3_lazylist.html
ですので、次のようなコードを書くとハマります。
int y = 0; var l = new list<int>{1,2,3}.Select((x) => y += x); Console.WriteLine(y);//y= 0 (6になると思いきや、まだ評価されていない) l.Last();//コレで全部評価される。 Console.WriteLine(y);//y= 6
そういえば、これはどこかで聞いたことのある話のような...。あー、Enumerator::Lazyですね。分かります。
参考:http://magazine.rubyist.net/?0041-200Special-lazy
ということで、Rubyのmap,filter相当のメソッドは実は存在しませんので、使いたいのであれば、自分で定義するしかないようです。こんな感じかな。
public static IEnumerable<TResult> Map<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, TResult> block) { List<TResult> l = new List<TResult>(); foreach (TSource e in source) l.Add( block( e ) ); return l; } public static IEnumerable<TSource> Filter<TSource>( IEnumerable<TSource> source, Func<TSource, bool> predicate) { List<TSource> l = new List<TSource>(); foreach (var item in source) { if (predicate(item) ) { l.Add(item); } } return l; }
C#でeach_with_indexをする
Rubyのeach_with_indexって便利ですよね。
C#には、each=ForEach、map=Select、select=WhereとRubyのEnumeratorのメソッドに相当するものが有りますが、私の大好きなeach_with_index相当がありません。
ということで、拡張メソッドを使ってサクッとモンキーパッチしましょう。
static class Extensions
{
public static void EachWithIndex<TSource>(
this IEnumerable<TSource> source,
Action<TSource,int> block)
{
int i = 0;
foreach (TSource e in source)
block(e,i++);
}
}
こんな感じで使います。
new List<string> {"a","b","c"}.EachWithIndex( (x, j) => Console.WriteLine(x + j)); //=> "a0" "b1" "c2"
いやー、C#って便利。
ActiveRecordのTimeZoneConversionが遅い件 (Rails小ネタ)
ActiveRecordでレコードを取得する場合、以下の様なコードを書いたとする。
Article.where(cond).map{|record| #condは検索条件
record.attributes.each{|name,value|
#hoge
}
}
この処理はレコード数が多い場合に非常に遅い。その理由は、record.attributesを実行した時に、ActiveRecord::TimeZoneConversion::Type#type_castが実行されるからである。
type_castの中の、ActiveRecord::ConnectionAdapters::Column#string_to_timeが非常に遅い。というより、Rubyの文字列をTimeに変換する関数はだいたい遅い。
では、どうすればよいか。Timeクラスに変換させなければよいのである。
そのためには、3つの方法がある。
①何も変換させない。(それでいいのか...)
activerecord-raw-dataを活用する。
http://subtech.g.hatena.ne.jp/secondlife/20120328/1332892079
②updated_atとcreated_atを取得しない。(他にTimeZoneのカラムがある場合は状況に応じて)
上記のコードをこのように変換する。
Article.where(cond).map{|record|
record.attribute_names.each{|name| #nameだけを取得
next if %w[updated_at created_at].include? name #TimeZoneのカラムをスキップ
value = record[name]
#hoge
}
}
③attributeをギリギリまで呼び出さない。
record.updated_atやrecord["updated_at"]などでアクセスされた時に始めて、TimeZoneConversionは起こるので、実際に利用するまではそこにアクセスしない。
records = Article.where(cond).to_a #まだTimeZoneConversionは動かない
・余談
めちゃくちゃ適当だが、postgresの場合は下のようにしてやればstring_to_valueが少しは早くなる。
ActiveRecord::ConnectionAdapters::Column.class_eval{
def self.string_to_time value
Time.local(*(value.split(/[ :\-\.]/,-1).map{|x| x.to_i}))
end
}
Rubyのnilの挙動を利用する
皆さんご存知の通り、RubyはnilはNilClassのオブジェクトである。
NilClass.instance_methods(false).sortとしてみると、結構な関数が定義されている。
=> [:&, :^, :inspect, :nil?, :rationalize, :to_a, :to_c, :to_f, :to_h, :to_i, :to_r, :to_s, :|] #ruby2.0
ここで、それぞれのメソッドの動作を確認してみる。(記号メソッドは除く)
[:inspect, :nil?, :rationalize, :to_a, :to_c, :to_f, :to_h, :to_i, :to_r, :to_s].map{|meth| nil.send(meth) }
=> ["nil", true, (0/1), [ ], (0+0i), 0.0, {}, 0, (0/1), ""]
ここで、面白いと思うのは、nil.to_a #=> [ ]である。
例えば、配列が来るかnilが来るかわからない状況があったとする。
そういう時は、activesupportのtryやpresenceを利用する場合が多い。
arr.try(:first) #=>最初の要素をとる(nilまたは空配列の時はnilを返す)
arr.presence || [ ] #=>nilか空配列の時は[ ]とする。
こういう時に、to_aメソッドを利用すれば同じことができる。(多分)
arr.to_a[0] #=>最初の要素をとる(nilまたは空配列の時はnilを返す)
arr.to_a #=>nilか空配列の時は[ ]とする。
さらに、enumerableの関数を利用する場合は、presenceより使い勝手が良い。
arr.to_a.map{|x| hoge x } #=> (arr.presence || []).map{|x| hoge x}でも可能。
Ruby2.0ではto_hメソッドも追加されているので、色々と面白いことができるかもしれない。
ちなみに、Array#to_aとHash#to_hはselfが返ってくるようだ。(Rubyist的には常識?)
a = [1,2,3]
b = a.to_a # => [1, 2, 3]
b << 2
a # => [1, 2, 3, 2]
a.equal? b #=> true
a = {x:1}
b = a.to_h #=> {:x=>1}
b[:y] = 2
a #=> {:x=>1, :y=>2}
a.equal? b #=> true
ActiveRecordで複数条件のorクエリまたはinクエリを投げる(Rails小ネタ)
ActiveRecordでor検索をしようと思うと、意外と簡単にはできない。
Table.where(id: 1).or(id: 2) とかでできそうなものだができない。
真面目にやるには、Arelを使ってやるのが一番のようだ。
http://techracho.bpsinc.jp/tsunekawa/2013_05_24/8502
非常に出来合いのコードだが、or検索のscopeを作成した。(SQLインジェクション対策はなし。プレースホルダを使うscopeってどう書くの?)
下記のように呼び出す。
Table.or([{id: 1,val: 2},{id:4,val:5}]) #=> (id=1 and val=2) or (id=4 and val=5)
でも、結局 Table.where(id: 1).or(id: 2)はできない...(おぃ)
scope :or, ->(condition){
where(condition) unless condition.class == Array
table_name = self.table_name
sql = condition.map{|cond|
partial = cond.map{|col,val|
%{"#{table_name}"."#{col}" = #{sanitize(val)}}
}.join(" AND ")
"(#{partial})"
}.join(" OR ")
where(sql)
}
また、PostgreSQLは複数カラムのwhere INができるので、inのscopeも作成した。http://blog.fusic.co.jp/archives/1765
下記のように呼び出す。
Table.in([{id: 1,val: 2},{id:4,val:5}]) #=> (id,val) IN ( (1,2),(4,5) )
scope :in, ->(condition){
where(condition) unless condition.class == Array
table_name = self.table_name
columns_sql = condition.first.map{|col,val| %{"#{table_name}"."#{col}"} }.join(" , ")
columns = condition.first.map{|col,val| col }
values = condition.map{|cond|
value = columns.map{|col| sanitize(cond[col]) }.join(" , ")
"(#{value})"
}.join(" , ")
where("(#{columns_sql}) IN (#{values})")
}