歩いたら休め

なんでこんな模様をしているのですか?

【R Advent Calendar】Rプログラマーのための関数型プログラミングの学び方

R Advent Calendar 2016の5日目です。よろしくお願いします。

最近、R界隈ではHadley Wickham氏が中心となって開発しているtidyverseと呼ばれるライブラリ群が流行っています。 彼がWelcome to the Tidyverseという記事の中で、次のような宣言を行っています。

Hadley Wickham, co-author (with Garrett Grolemund) of R for Data Science and RStudio's Chief Scientist, has focused much of his R package development on the un-sexy but critically important part of the data science process: data management. In the Tidy Tools Manifesto, he proposes four basic principles for any computer interface for handling data:

  1. Reuse existing data structures.
  2. Compose simple functions with the pipe.
  3. Embrace functional programming.
  4. Design for humans.

「データマネジメントってセクシーじゃないけどめっちゃ重要なプロセスだよね。Tidy Tools Manifestoでも言ってるけど、データを扱うときのインターフェイスって4つ重要な原則があるよね」みたいな感じだと思います。

その原則の1つがコレです。

Embrace functional programming.(関数型プログラミングの思想を受け入れよう)

といっても、関数型言語やそのプログラミング手法は、まだまだ一般的なものではありません。 また、(オブジェクト指向と同じく)言語ごとに「関数型」という言葉の意味が少しずつ違い、 どのように勉強すればいいのか分からない、どんなメリットがあるのかわからないという方も多いでしょう。

というわけで、Rのプログラマー関数型プログラミングを紹介しようという無謀な記事を書いてみます。 (最初に言っておきますが、私はデータ分析者ではないし、オブジェクト指向言語であるPythonRubyを使い慣れているWeb開発者です。)

といっても、私自身も関数型言語の経験が深いわけではなく、 例えば「Ladder of Functional Programming 〜関数型プログラミングのレベル分け〜」でいうとADVANCED BEGINNERのあたりのレベルです。 PythonRubyで関数/Procを用いたプログラミングは問題なくできるのですが、もっと抽象的な世界や「型」に関する知識がまだまだだと思っています。

つまり、「自分もまだまだだけど、今のコードから一歩進んで、よりよいプログラミングのために一緒に関数型プログラミングを学んでみませんか?」というお誘いです。とはいえ自分の経験不足のため、間違っている箇所や、「おれならもっと語れるぜ」という箇所もあると思うので、コメント等でツッコミいただけるとうれしいです。

Rは関数型言語なのか

結論から書くと、関数型言語の影響は強いものの、関数型言語ではないと私は思っています。

Rが関数型言語なのかについては、既にR Advent Calendar 2014R Advent Calendar 2014: Rでfunctional programming!?という記事に、話が出ていますが、

qiita.com

古典的な関数を自由に扱えるという点は満しそうです。下記のごとく無名関数はfunctionというclassを持つ一級オブジェクトで、関数定義は無名関数本体を代入しているだけです。(無名)関数を引数として使う高階関数(higher-order function)は、下記の例がRっぽいかは別としてlapplyやsapplyなどで一般的です。

(中略)

ということで、R言語関数型言語かどうかは定義によるという微妙な結論なようです。

私も全く同じ意見です。ただ、「無名関数やクロージャが使えるから関数型言語だ」と言ってしまうと、JavaScriptPythonなども関数型言語に入ってしまうような気がして個人的には違和感があります。

また、末尾再帰最適化やパターンマッチなど(関数型として)一般的な機能がRには存在しないため、 どうしても他の関数型言語と比べると見劣りしてしまう面もあります。 (じゃあ何言語なんだよ!と思うのですが、本当になんなんでしょう。RはRかな…)

といっても、関数型言語の定義を深く考えてもしょうがなく、 「データ分析者/Rのプログラマーとして、学んだ後にどのようなメリットがあるか」ということを考えたほうがいい気がします。

そして、実際に関数型プログラミングを学ぶメリットはあると思っています。

qiita.com

プログラミングはデータの変換をするものだ

関数型言語の一種と言われるElixirの入門書に「(関数型)プログラミングはデータの変換をするものだ」という説明がありました。

これは、dplyrtidyrでパイプ演算子(%>%)を用いたプログラミングに慣れている方には受け入れやすい説明だと思います。 これらのパッケージを使い慣れてくると、「データを変換する関数を作り」、「それをパイプ演算子でつなげる」という手順でRのプログラミングが行え、 それぞれの関数の役割(〇〇のデータを××に変換する)がはっきりした美しいプログラムが書けるようになっているはずです。

qiita.com

というのも、ElixirはR(+tidyverse)と同じくパイプ(ライン)演算子(|>)を導入している言語で、 最近のRが目指しているものに似たプログラミングスタイルを取る言語だからです。 さらに言うと、パイプライン演算子自体、F#という関数型言語の機能が発祥らしく、そちらではもっと昔からパイプを使ったプログラミングが行われていたようです。

(また、Elixirは大規模分散処理や対障害性のあるアプリケーション等、統計解析ツールであるRとは全く違う分野で使われている言語であることも面白いです。)

関数型について学ぶと、抽象度の高い概念が出てきて面食らうと思いますが、 基本的には「データの変換をより楽に、関数をもっと汎用的に使うために考えられた機能」だと考えると勉強しやすいと思います。

プログラミングElixir

プログラミングElixir

パイプ演算子を使えば、Rで必要な関数型言語の知識は(そこまで)深くない

[増補改訂]関数プログラミング実践入門』にある関数型言語の説明を引用ます。 この本は実質的にHaskellの入門書みたいな感じなので、若干そちらの思想に寄っちゃってます。

一方、関数プログラミングでは、「プログラムとは『関数』である」という見方をします。 そして、大きなプログラムは小さなプログラムの組み合わせから成ります。 大きなプログラムは大きな関数、小さなプログラムは小さな関数であるとすると、プログラムの組み合わせとは関数合成(function composion)ということになります。

(詳しく説明すると長くなるので端折りますが)Haskell等の多くの関数型言語では、あらかじめ関数合成した関数を作り、合成した関数を変数に適用します。 一方で、RやElixirでは、「パイプ演算子で変数を関数に渡し、その戻り値を次の関数に渡す」というコードになります。

abrahamcow.hatenablog.com

例えば、Elixirの入門書の最初のコード例で出ている例なのですが、 なんとなく|>がパイプ演算子Task.asyncで並行処理にmapしていることがわかると思います。

(Rのpurrrでのリストの操作に四苦八苦している方も多いですが、実はElixirでは10ページ目に出てくるような内容です。)

defmodule Parallel do
  def map(collection, function) do
    collection
    |> Enum.map(&Task.async(fn -> function.(&1) end))
    |> Enum.map(&Task.await/1)
  end
end

このプログラムでは、Parallel というモジュールに pmap という関数を定義しています。 pmap は、与えられたコレクションに対して map(Ruby での Enumerable#map と同じようなものと考えて下さい)を行なうのですが、 各要素の処理を、要素数の分だけプロセスを生成し、各プロセスで並行に実行する、というものです。 ちょっと見ても、よくわからないような気がしますが、大丈夫、本書を読めば、わかるようになります。

関数合成では「関数を返す関数」というちょっと難しい概念が必要なのですが、 パイプ演算子のある言語では単に「戻り値を次の関数に渡す」というデータの変換でプログラムを記述・理解することができます。

実際、関数合成や引数の部分適用を使うようなプログラミングスタイルを、Hadleyは『R言語徹底解説』の12章で紹介しているものの「Rでは自然ではない」と言っています。

この種のプログラミングはタシットプログラミング(tacit programming)やポイントフリープログラミング(point-free programming)と呼ばれる. (中略)このスタイルはHaskellでは一般的であり,ForceやFactorのようなスタック志向のプログラミング言語において典型的である. これはRにおいては自然でもエレガントでもないスタイルになってしまうが,試してみるのも悪くないだろう.

この本では特にパイプを使ったプログラミングスタイルには触れられていませんが、 おそらく、『R言語徹底解説』の原著が2014年で、パイプ演算子magrittrライブラリも2013年末から開発が始まったようなので、執筆より後に広く使うようになったのでしょう。

ちょっと話がズレてきちゃいましたね。

何を学べばいいのか → とりあえず高階関数を使いこなせるようになろう

最初にも触れた『Ladder of Functional Programming 〜関数型プログラミングのレベル分け〜』でいうと、 「NOVICEはマスターして、ADVANCED BEGINNERの一部は理解している」状態くらいあれば、Rのプログラマーとして困ることは無くなると思います。

(というか、むしろRのプログラマーが普段やっているような、統計解析のほうが難しい理論を使っていると思います!)

qiita.com

特に、型に関する項目は必ずしも必要なく、ひとまずは高階関数汎関数)に関する項目があれば充分です。 つまり、PythonRubyなどの、一般的なスクリプト言語で必要なレベルと大差ありません。

  • 二階関数(map, filter, fold)をイミュータブルなデータ構造に対して使える
    • Rの組み込み関数ではMap, Filter, Reduce、purrrではmap, keep, reduce
  • 二階関数にラムダを渡すことができる
  • 関数を引数にしたり関数を返したりする関数を書ける
  • 非純粋なコードから純粋なコードを見つけ出し切り分けることができる

Rに必要な関数型プログラミングをどうやって勉強するかですが、おそらく『R言語徹底解説』の汎関数(高階関数)の章の内容を学ぶのがベストです。 ただし、R独自の仕様のために、必要以上に難しく見えて行き詰まるかもしれません。

R言語徹底解説

R言語徹底解説

例えばたくさんのapply系の関数が存在し、リストとベクトルを返すものや、オプションが付いているものがあったりすることに面食らいます。 例えば、tidyverse関数型プログラミングを支援するpurrrパッケージがあるのですが、その中で特に重要な関数はmap, keep, reduceの3つだけで、Rの場合は戻り値の型(リスト、ベクトル、データフレーム…)によっていろいろなバリエーションがあるために難しく見えるだけです。

その場合、一般的な関数型言語をある程度勉強して、その後にRに戻ってくるといいでしょう。

Rを使いこなすために(≒tidyverseのライブラリを使うために)必要なレベルはそこまで高くないため、一般的に関数型言語といわれているものを1つ触れば充分だと思います。 本屋に行って肌に合いそうな本を探すのがいいと思います。

…と言って投げっぱなすのもアレなので、私が最近読んでいる本のリンクを貼っておきます。 『関数プログラミング実践入門』というタイトルですが、実質Haskellの入門書です。

Rに近いところでいうと、Rの影響元でもあるLisp (Scheme)がいいかもしれません。

postd.cc

さいごに

関数型プログラミングといいつつ、ほとんどパイプ演算子高階関数の話しかしていない記事になってしまいました。

とりあえず、私がいいたいことは次の4つです。

  • パイプ演算子を使うと、「データの変換」としてRのプログラムを記述できる
  • パイプ演算子R言語独自の特殊な機能ではない(ElixirやF#では昔から使われている)ので心配せずにガンガン使おう
  • Rのプログラマーに必要な関数型言語の知識のレベルは、ひとまずそんなに高くないし、高階関数をきちんと理解すればOK
  • purrr最高!一番好きなRのパッケージです

また、ここまで書いて気づいたのですが、ここまでRのコード例が一行も出ていません。 実際にパイプ演算子を使ったRの分析コード例が見たい場合は、こちらの記事を読みましょう!(投げっぱなし)

uribo.hatenablog.com

明日のR Advent Calendarは@yutannihilationさんのtidyverseについての記事です。 purrrの記事いつも読ませていただいてます。 楽しみにしています。