3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Ramdaをキャッチアップした

Last updated at Posted at 2021-05-23

前書き

Ramdaを勉強している中、いろいろ関数側プログラミングと他の種別のプログラミングスタイルを実例で比較してみたいと思います。

関数型プログラミングの特徴

特徴に関する資料がたくさんあるので、ここは簡単に自分の理解をまとめます。

第一級関数

wiki

一言で言いますと、
開発者が定義した関数は別で定義した引数、String型とか、Boolean型、Integer型とかは特に相違がないとのことです。
つまり、

  1. どこでも定義できる
  2. 関数のパラメータとして使える
  3. 関数の帰り値として使える

のが特徴です。

Javaではもともと開発者が定義したメソッドはクラス以下にしか定義できず、
また関数のパラメータと帰り値として使えなかっただが、
Java8のLambdaの導入により条件付きに実現可能となりました。

高階関数

wiki

では、第一級関数をサポートしている関数はなんと言いますかと言うと、高階関数です。

  • 関数を引数に取る
  • 関数を返す

上記の第一級関数の説明とほぼ同じように、
上記の条件をどちらを満たす場合な関数が高階関数と呼ばれます。

// 関数が引数
const arr = [1, 2, 3];
arr.map(x => x * 2);
// output: [2, 4, 6]
// 関数が帰り値
const add = a => b => a + b;
const add10 = add(10);
add10(32);
// output: 42

Point-Free

一般的に定義されている関数は何かしらのパラメータを持って、そのパラメータを用いて何かしらの操作をすると思います。

// Point-Freeではないバージョン:(a + b) ^ 2
const add = (a, b) => a + b;
const square = x => x * x;
const square_of_sum = (a, b) => square(add(a, b));
// Point-Freeバージョン: (a + b) ^ 2
import * as R from 'ramda'
const add = (a, b) => a + b;
const square = x => x * x;
const square_of_sum = R.pipe(add, square);

違いが、square_of_sumのところ、PointFreeのほうは引数を指定していないことです。
いろいろPointFreeについての議論がありますが、
私の考えでは、PointFreeのメリットはpipeやcomposeで関数を連結するときに、
インプットデータをいったん置いといて、各関数の動きだけをフォーカスすることです。

もちろん、インプットがないと、何を操作しているかがパットピンとこないと思いますが、
これはPointFreeのメリットでもありますし、デメリットでもあります。

副作用がない

関数型プログラミングで定義した関数は、処理が必要なデータが全部インプット中に含まれます。
グローバルの変数を含まれていないため、
インプットデータが変わらない限り、関数の帰り値は変わらないです。

副作用がないことはいろんなメリットがありますが、
私にとって一番のメリットはUnitテストが書きやすいことです。

何のモックも必要なく、どんな関数が簡単にテストできるのが楽ですね。

引数の順位

もしPointFreeではない関数を作成する場合に、
実際のインプットデータがなるべく最後の引数にするのが一般的です。

なぜかと言いますと、
私が関数型プログラミングで実装している経験の中に、
基本的に関数を組み合わせて新しく、もっと複雑な関数を作成することが多いです。

全てのロジックを完成し、最終的にデータをやむを得ず処理する段階に入ると、
はじめて関数のパラメータにインプットデータを入れます。
RamdaのAPIを見てもらえるとわかると思います、複数個のパラメータが存在する場合に、
大半の場合に関数をまず引数として渡して、何をしたいかを決める。
で最終的に実際に処理したいデータを最後の引数として渡します。

Immutable

これも副作用の一部ですが、
副作用に影響されない一方、副作用を与えないことも大事です。

関数型プログラミングで作った関数では、
受け取った引数を変更することがないので、
常に新しいコピーを返しています。
なので、パラメータとして使われた変数がメソッドの実行により影響されないわけです。

これでデバッグとか、不具合修正するときに、
データのステータスの変更を追跡しなくてもすぐ問題を特定できるでしょう。

話が長くなりましたのでいったん本題に戻ります。

今回は、配列をインプットとしてインデックスを返すという簡単な処理から、
各書き方を見比べてみたいと思います。

for-loop

const indexesFrom = xs => {
  const indexes = [];
  for (let i = 0; i < xs.length; i++) {
    indexes.push(i);
  }
  return indexes;
}

Array.prototype.forEach

for-loopがちょっとダサいので、ちょっと変更してみると下記になります。

const indexesFrom = xs => {
  const indexes = [];
  xs.forEach((_, i) => indexes.push(i));
  return indexes;
}

Array.prototype.map

forEachより、mapで一発でできるので下記になります。

const indexesFrom = xs => xs.map((_, i) => i);

destructuring

ちょっとトリッキーですが、これでもできますよ。
(チームメンバー提供。Shout out to Vir

const indexesFrom = xs => [...Array(xs.length).keys()];

Ramda.range Non-Point-Free

Ramdaではrangeという関数を提供しています。
rangeはstartendの二つの引数を受け取って、
[start, ..., end-1]のリストを返します。

const indexesFrom = xs => range(0, R.length(xs));
// or
const indexesFrom = xs => range(0, xs.length);

Curryについて少し

rangeが二つの引数を受け取るなのに、なぜ片方を渡せるのかを軽く説明します。

Ramdaは全部カリー化しているので、
つまり:引数を一気に全部渡さなくても大丈夫です。
途中で一部の引数のみを指定する場合に、関数の帰り値をまた関数になるというすごい書き方です。

// 自分でカリーを実現する、カリーの良くない例
const add = a => b => a + b;
add(1)(2); // output: 3
add(1, 2); // output: function b => 1 + b; 引数2が無視される

こういう風に関数を実行して、関数を返すことができますが、
引数を一個ずつ渡すことしかできません。

const add = R.curry((a, b) => a + b);
add(1)(2); // 3
add(1, 2); // 3
const inc = add(1);
inc(2); // 3
const dec = add(-1);
dec(2); // 1

上記の例で、カリーの面白いところを感じたでしょうかね?
Ramdaは任意個数の関数をカリー化することができます。
そうすることで、関数実行時にも任意個数のパラメータを自由に渡すことができるようになりました。

Ramda.pipe

上記の例はまだPointFreeではないので、これからPointFreeの関数について紹介します。

pipeはLinuxコマンドと似ているようなイメージだと思ってもらえるといいと思います。

// まず貰ったデータのLengthを取得して、Lengthを取得後の帰り値を次の関数のインプットとして利用します。
const indexesFrom = R.pipe(R.length, R.range(0))

Ramda.useWith

ここからはRamda特有の関数の紹介になります。

useWithは二つ以上の引数があります、
一つ目は最終的に実行する関数です、
二つ目は一つ目の関数の引数をどうやって生成するかの関数になります。

下記の例で言いますと、
range(0)は第一引数で、range(0)を実行するためもう一つの引数が必要になります。
その引数の値は第二引数で定義したメソッド配列から計算されます。

計算のフローとしては、
まず貰った配列のlengthを取得して、
その値をrange(0)の引数として渡し、最終結果を計算します。

useWithで返した関数の引数の個数は第二引数の配列の長さで決めます。

const indexesFrom = R.useWith(range(0), [R.length]);
indexesFrom([1, 2, 3]); // output: [0, 1, 2]
const indexesFrom = R.useWith(range, [R.always(0), R.length]);
indexesFrom(0)([1, 2, 3]); // output: [0, 1, 2]

Ramda.converge

これはuseWithと似ていますが、
違うところはconvergeは一つの引数をしか受け取れないことです。

const indexesFrom = R.converge(range(0), [R.length]);
// Or
const indexesFrom = R.converge(range, [always(0), R.length]);
// 二つともの実行方法
indexesFrom([1, 2, 3]);

最後

同じことを実現するのにこんなにいっぱいの方法があるなんて、
結構面白いですね。

関数型プログラミングとか、Ramdaとかは最初の段階で結構わかりづらい部分があると思いますが、
でも勉強すればするほど関数型プログラミングの面白味を感じられると思いますので是非お試しください。

また、関数型プログラミングでは、基本的に関数と接することが多いので、
本当に簡単ないくつかの関数から、
どんどん組み合わせて複雑な関数を作っていくことも一つの醍醐味のではないかと思います。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?