LoginSignup
9
12

More than 3 years have passed since last update.

JavaScript:カリー化してくれる関数

Last updated at Posted at 2018-03-24

カリー化とは?的なことはすでに諸先達が書かれているので、そちらをご覧ください。

関数をカリー化したいとき

いっぺんにたくさんということでなければ、その都度、一個ずつカリー化でいいと思います。
何をしてるか自分でわかるので安心です。

// 例1。引数が二つの非カリー化関数をカリー化する。
const uncurriedF = (x, y) => (何か返り値);//があるとして
const curriedF = x => y => uncurriedF(x, y)

// 例2。配列のメソッドmapをカリー化関数にする。
const curriedMap = f => xs => xs.map( f )

汎用の関数を作ってラップする

const curry2 = f => x => y => f(x, y)
const curry3 = f => x => y => z =>f(x, y, z)

たいがい2~3引数の場合が多いので、こんなので十分実用的かと思います。

でも、引数の数が可変だったら便利だし、かっこいいですよね。

引数の数可変バージョン

いろんな方が書かれていますが、この方の記事で初めて意味がわかりました。
javascriptで簡単なカリー化関数を作った(ES6版)より、ただの関数版を引用。

function curry(fn) {
  return function partial(...args) {
    return (args.length >= fn.length)
      ? fn.apply(null, args)
      : function(...ret_args) {
          return partial.apply(null, [...args, ...ret_args]);
        }
  }
};

わかりやすい!
自分で理解するためにさらに簡単になるか変形してみました。

const curry = f =>{ 
  //関数partの定義。
  const part = (...xs) => 
    (xs.length < f.length) ? 
      y => part(...xs, y) 
    : f(...xs)
  // partを呼び、返す。
  return part
}

※ 複数の引数を取らないようにしてますが( y のくだり)、本筋は一緒かと思います。

  • curry は関数fを引数にとって、関数partを返す。
  • part は複数の引数(配列xsになる)をとって、
  • もし xsの要素数がfが必要とする引数の数に足りないなら、yを引数に、展開したxsの後にyを付けた引数でpartを返す関数を返す。
  • そうでないなら、展開したxsを引数にfを返す。

ちょっとわかりにくいですが、再帰になってます。

ループを追って並べてみるとわかります。
関数fが必要とする引数の個数をnとして、一番目の引数(...xs)に単一の引数y1を入れることにします。

curry(f) 
  //=> part
curry(f)(y1) 
  //=> part(y1) 
  //=> part(y1)(y)=>part(y1, y)
curry(f)(y1)(y2) 
  //=> (part(y1)(y)=>part(y1, y))(y2) 
  //=> part(y1,y2) 
  //=> part(y1,y2)(y)=>part(y1,y2,y)
.
.
.
curry(f)(y1)(y2)....(yn) 
  //=> ( part(y1,y2,....,y(n-1))(y)=>part(y1,y2,....,y(n-1),y) )(yn)
  //=> part(y1,y2,....,y(n-1),yn)
  //=> f(y1,y2,....,y(n-1),yn)

改良バージョン

このままでも使えますが、より使いやすいように改造してみます。

  • 関数 f が必要とする引数の数が0個の時に、即時実行 f() しないで関数 f を返すようにする
  • 複数個の実引数を最初の括弧に入れると(...xs)で複数個評価されてしまうので、最初のpart(...xs)は実引数無しとし、次の y から実引数が入るようにすることで、複数個入れても括弧あたり最初の一個だけ評価するようにする。
const curry = f =>{ 
  // f が必要とする引数が無ければ f を返す。
  if(f.length === 0) return f   
  //関数partの定義。
  const part = (...xs) => 
    (xs.length < f.length) ? 
      y => part(...xs, y) 
    : f(...xs);
  // 引数無しでpartを呼び、返す。
  return part()
}

追記(2018.10.17)

ながめていたら、{ }、returnがなくせるなと気がついた。

const curryR = f => 
  f.length==0 ? f
  : (p => p(p)() )(
     p => (...xs) => 
      (xs.length < f.length) ? 
        y =>  p(p)(...xs, y)
      : f(...xs)
    )

かえってわかりにくいかも。
機能ごとにバラしてみます。

const recurse = f =>  f(f)  //再帰するしくみ。
const currySeed = p => f => (...xs) =>   //再帰の元になる関数。
      (xs.length < f.length) ? 
        y =>  p(p)(f)(...xs, y)
      : f(...xs)
const curryR = f => 
      f.length===0 ? 
        f
      : recurse(currySeed)( f )()  //関数fのカリー化。実引数なしでスタートすることで、最初の引数を一個に限定している。
//使用例:
const mul2 = (x, y) => x * y;
const mul2C = curryR( mul2 );

mul2C(2);  //=> [Function]
mul2C(2)(3)  //=> 6
mul2C(2, 3)  //=> [Function] 
mul2C(2, 3)(3)  //=> 6 ちゃんと最初の引数を一個に限定している。

こっちの方がまだわかりやすいかも。
でもまあ、無名再帰っぽいことがしたいわけではないのでもっと簡単にします。

//カリー化の元になる再帰関数。
//引数が足りないなら引数を後に加えます  curryS = f =>(...xs)=> y => curryS(f)(...xs, y)
//足りていれば引数をそのまま適用       curryS = f =>(...xs)=> f(...xs)
const curryS = f => (...xs) =>
  (xs.length < f.length) ? 
    y =>  curryS(f)(...xs, y)
  : f(...xs)
//fをカリー化する関数。
//fに引数がないならfを返す  curry = f => f
//fに引数があるなら最初の実引数なしでcurryS(f)に適用  curry = f => curryS( f )()
const curry = f =>
  f.length===0 ? 
    f
  : curryS( f )()

これが一番わかりやすいかな。

改悪?バージョン

改悪、は言い過ぎですね。ちょっと変えるだけで、別用途、別概念っていうか...
y...ysに変えて引用元と同じくしてみました。
どの括弧にいくつ実引数を入れても、fが必要とする数だけ全部評価されます。

const bindPartial = f =>{ 
  if(f.length === 0)return f
  const part = (...xs) => 
    (xs.length < f.length) ? 
      (...ys) =>  part(...xs, ...ys) 
    : f(...xs)
  return part()
}

カレー化もできるし、しなくても自由なやりかたで部分適用できる。

便利! と思う方はこちらで。
間違いを起こす元凶? と感じる方は改良バージョンで。

注意点

  • 関数 f に可変長引数があるとうまくいかない。
9
12
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
9
12