Speee DEVELOPER BLOG

Speee開発陣による技術情報発信ブログです。 メディア開発・運用、スマートフォンアプリ開発、Webマーケティング、アドテクなどで培った技術ノウハウを発信していきます!

Speee OSS挑戦 for Red Data Tools

※この記事は、Speee Advent Calendar19日目の記事です。

昨日の記事はこちらtech.speee.jp


こんにちは!

デジタルトランスフォーメーション(DX)事業本部エンジニアの岡田です。

Speeeでは、クリアコード須藤さんOSS活動をサポートしてもらっています。

今回、OSS活動はほぼ未経験の自分が、どのように挑戦したかを書いてみました。

最初はチャットでの相談から入り、現在もサポプロを使って勉強しています。

OSS 活動に挑戦した背景

自分は、プログラミングを社会人から始めて、特に体系だった知識をもたないまま、業務で必要なスキルを都度都度勉強してきました。

結果、WEB開発という文脈であれば、フロントエンドも、サーバーサイドも、クラウド周りもある程度は一人で実装できるようになりました。

一方で、まだまだ技術の話で分かってないことが多いなと感じており、特にライブラリ周りはただ使わせてもらうだけで、どう作られてるのかを詳しく知りたかったので、勉強したい!という気軽な気持ちで挑戦しました。

OSS Gateに参加

まず、Speeeと須藤さんで開催されている「OSS Gateミートアップ for Red Data Tools」というイベントに参加しました。 speee.connpass.com

Red Data Toolsについて、詳しくはこちら!

Red Data Tools - Rubyでデータ処理!

下のチャットで、わからないことを日本語で質問しながら作業できるので、安心でした!

red-data-tools/ja - Gitter

Red Datasets にデータセット追加

須藤さんより、まずは簡単なものとして、Red Datasets にデータを追加することを提案頂いたので、まずはこちらに取り掛かりました。

こちらから自分の好きなIssueを探し、他のPRを参考にしながら、実装を行います。

github.com

こちらは、須藤さんに環境構築方法や、わからないことをチャットから質問して、あっさり終わらせることができました!

Red Arrow の開発ドキュメントを追加

次に、Rubyで作られたGemを実際に修正してみたかったので、須藤さんに相談したところ、Red Arrowへの機能追加を提案いただきました!

で、早速やろうとしたのですが、Macでうまく動かずつまづきました...

須藤さんに相談しながら、上手く修正できたので、次行われる方のために、ドキュメント追加を行いました。

github.com

Red Arrow の slice が、hashを受け取るようにする

環境構築ができたので、早速 Red Arrow への機能追加に取り掛かりました。

github.com

既に、table.slice(1..8) というように、rangeを渡した時は、その値でIDが絞り込まれるように実装されていました。

今回は、table.slice(count: 1..8) のように、カラムを指定して、絞り込みをする機能を追加するのがゴールとなります。

ハマったこと(学んだこと)

Rangeの最後の値を確認する方法がわからない

{ count: 8.. } としたときに、最後の値がendlessになるかをテストしたかったです。

Range#lastを使って比較しようとしましたが、cannot get the last element of endless range というエラーが起きてしまいました。

最終的に、Range#endを使うと問題なくできました。

こちらが endの実装で、

static VALUE
range_end(VALUE range)
{
    return RANGE_END(range);
}

こちらがlastの実装(引数がない時はendと同じ挙動だけど、引数渡すと挙動が変わる)

static VALUE
range_last(int argc, VALUE *argv, VALUE range)
{
    VALUE b, e;

    if (NIL_P(RANGE_END(range))) {
        rb_raise(rb_eRangeError, "cannot get the last element of endless range");
    }
    if (argc == 0) return RANGE_END(range);

    b = RANGE_BEG(range);
    e = RANGE_END(range);
    if (RB_INTEGER_TYPE_P(b) && RB_INTEGER_TYPE_P(e) &&
        RB_LIKELY(rb_method_basic_definition_p(rb_cRange, idEach))) {
        return rb_int_range_last(argc, argv, range);
    }
    return rb_ary_last(argc, argv, rb_Array(range));
}

という二つの差を理解していなかったのが原因でした。

Rubyの実装を見る機会が今まで特になかったので、とても勉強になりました。

複数条件での絞り込み方法がわからない

table.slice(count: ..8, name: 'okadak')というように複数で絞り込みを可能にしたかったです。

Arrow::Table は簡単にいうと、表形式のCSVのデータが入ってるイメージに近くて、今回はCSVのheaderの値を指定して、その列の値でフィルタリングしたいという感じです。

須藤さんに教えてもらったところ、こんな感じに動けばいいことが分かりました。

slicers = []
conditions = []
slicer = Arrow::Slicer.new(table)
conditions << (slicer[:count] >= 8)
conditions << (slicer[:name] == 'okadak')
slicers << conditions.inject(:&)

# 省略: slicersを用いて、filterする

このメソッドの中身に関しては、まず、slicer[:count] >= 8これを実行すると、[]」メソッドが実行されます。

def [](column_name)
    column = @table[column_name]
    return nil if column.nil?
    ColumnCondition.new(column)
end

これが実行されると、Arrow::Table[]」メソッドを実行します。

def [](selector)
  case selector
    ...
  else
    find_column(selector)
  end
end

def find_column(name_or_index)
  case name_or_index
  when String, Symbol
    name = name_or_index.to_s
    index = schema.get_field_index(name)
    return nil if index == -1
    Column.new(self, index)
  ....
end

Arrow::Columnが以下のように初期化され、

def initialize(container, index)
  @container = container # Arrow::Table が入る
  @index = index # 何列目の値を指定してるか
  @field = @container.schema[@index]
  @data = @container.get_column_data(@index)
end

この値を使って、Arrow::Slicer::ColumnConditionが初期化されます。 なので、slicer[:count]は、Arrow::Slicer::ColumnCondition.new(Arrow::Column.new)を返すようになります。

よって、Arrow::Slicer::ColumnCondition.new >= 8となるので、Arrow::Slicer::ColumnCondition>=」メソッドが引数「8」で実行され、Arrow::Slicer::ColumnCondition::GreaterEqualConditionが返ります。

def >=(value)
  GreaterEqualCondition.new(@column, value)
end

このArrow::Slicer::ColumnCondition::GreaterEqualConditionは、Arrow::Slicer::Conditionを継承していて、conditions.inject(:&)のように実行すると、&」メソッドが実行され、Arrow::Slicer::Condition::AndConditionが返されます。

def &(condition)
   AndCondition.new(self, condition)
end

class LogicalCondition < Condition
  def initialize(condition1, condition2)
    @condition1 = condition1
    @condition2 = condition2
  end

  def evaluate
    function.execute([@condition1.evaluate, @condition2.evaluate]).value
  end
end

class AndCondition < LogicalCondition
  private
  def function
     Function.find("and")
   end
 end

このArrow::Slicer::Condition::AndConditionは、Arrow::Slicer::LogicalConditionを継承しており、LogicalConditionは複数のArrow::Slicer::Conditionを保持できるようになっているので、このArrow::Slicer::Conditionを使って、複数条件の絞り込みが可能になりました。

(Filter側の処理は今回修正していないので、説明を省略します。)

最終的には、こちらのようなコードになりました。

slicer[:count] >= 8」みたいなコードを特に意識せずに使えるのは、誰かが裏で綺麗に設計してくれていたからなんだなと、すごく勉強になりました!

Part2へ...

今はこちらの実装をしてます!

github.com

まだ終わってないので、次回も続きを書きたいと思います!

終わりに

このようにクリアコードの須藤さんに教えてもらいながら、OSS活動をしはじめています!

tech.speee.jp

現在はサポプロを使ってやっているわけですが、C++ の書き方や、コードの意図など、分からないことを丁寧に説明してくださるのでとても勉強になっています。

また、OSS活動を始めてから、Rubyのライブラリやドキュメントを読む力が上がっている感覚があり、困った時の解決速度が実際に上がりました。

何より、初めて知ることが多くてとても楽しいです!プログラミングについて、まだまだ勉強できることがたくさんあるなと感じています。

今後もエンジニアとしてのスキルをもっと上げていきながら、OSSコミュニティへ貢献できたらと思います!


Speeeでは一緒にサービス開発を推進してくれる仲間を大募集しています! もしSpeeeに興味を持っていただいた方は以下で社内メンバーのカジュアル面談を公開しているので、お気軽にご連絡ください💁

tech.speee.jp

Speeeでは様々なポジションで募集中なので、「どんなポジションがあるの?」と気になってくれてた方は、こちらチェックしてみてください!もちろんオープンポジション的に上記に限らず積極採用中です!!!


読んでいただきありがとうございました!