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

歩いたら休め

If the implementation is easy to explain, it may be a good idea.

【R】Rで関数型プログラミング

最近Rを使っていて、dplyrなどの強力なパッケージで気軽にデータの成形ができることに感動しています。

先輩に渡してもらったコードの中で、tidyr::spreadにデータフレームをパイプ%>%で渡し、 データの成形を一気に終わらせていて、こいつはただごとではないと感じました。 その後、今日の仕事時間の半分はRで遊んでました。 もうPythonのpandasなんかでデータの集計なんてできません。

qiita.com

ただ、私はPythonユーザーなので、リストっぽいものは全部リストっぽく操作できる統一的な世界観に慣れてしまっているため、 Rの無駄に高機能な関数を使って操作することに慣れません。 毎回「こんな関数ないかなー」と調べたり、「さっきも同じようなコード書いたよなあ」と思ったり、 強い関数を使っている分、多くの関数を知らないと意味が分からないコードになってしまいがちです。

ところで、最近Haskellを触っていて、「問題を最小単位に分解し、それを全体に適用する」というプログラミングの考え方がイケてるなあと思っています。 Rも同じように書けば、再利用性の高いいい感じのコードが書けるんじゃないかと思いました。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

ということでやってみました。 タイトルの「関数型プログラミング」っていうのは大げさですが。

こんなコードです。 GoogleのBigQueryの様々なテーブルにクエリを投げて、戻ってきたテーブルを一つのデータフレームにまとめるという処理です。 みんなだいすきread.csvとかでも同じような処理ができると思います。

require(dplyr)
require(pryr)
require(bigrquery)

# 各テーブル名(いろいろな)を入力
mail_data_sources <- list("table1", "table2", "table3")

# DBから取ってきたい期間を入力
begin_date <- "2015-10-01"
end_date <- "2015-10-07"

# クエリを作る関数を定義
make_query <- function (
  table, 
  begin_time, 
  end_time
  ){
  return(
    # 期間を絞ったSQLのクエリを返す
    # SELECT col1, col2 FROM table WHERE date BETWEEN begin_time AND end_time みたいな感じ
  )
}

# make_queryにbegin_dateとend_dateをセットした関数を作る
make_period_query <- pryr::partial(
  make_query, 
  begin_date = begin_date,
  end_date = end_date
  )

# bigrquery::execにプロジェクトをセットした関数を作る
unko_query_exec <- pryr::partial(
  bigrquery::query_exec,
  project = 'unko-project'
  )

# 関数合成でひとつひとつのクエリに対して行う処理をまとめる
# 引数が一つの関数しか合成できないので、予めそれぞれの関数を部分適用しています
get_bq_data <- pryr::compose(
  make_period_query,
  unko_query_exec
  )

# データ元のリストを入れるとデータフレームに結合して返す関数
get_binded_df <- function (data_souces) {
  return (
    data_souces %>% #テーブル名のリストを、
      Map(f = get_bq_data) %>% #データフレームがたくさん入ったリストに変換して、
      Reduce(f = rbind) #中身のデータフレームを縦に結合する
    )
}

# BigQueryからの戻り値をデータフレームに格納
binded_df <- get_binded_df(mail_data_sources)

意識したのは3点

  1. パイプの渡し先の処理を、どんな処理なのかわかりやすくした
    • Mapにリストの中身の操作を、Reduceにリスト全体を一つの値にまとめる処理を集約させた
    • 他人のコードを読んでいて、パイプで渡っている値がベクトルなのか集約された値なのか判断できずに困ることが多かったので。
  2. Map、Reduceという一般的な名前の高階関数を使った
    • do.callだと何なのか分からない
    • Mapだとリストにもベクトルにも効く(っぽい)ので覚えるのが楽そう
  3. 関数合成で一つのテーブルに対する処理を一箇所にまとめた
    • pryrパッケージがすごく便利だった
    • 必要以上にパイプでメソッドチェーンが続くと、後から読みづらくなってしまう気がします。

他の言語のプログラマが見ても読めみやすいコードになったと思います。 というかPythonっぽくなってしまった気が…。

d.hatena.ne.jp