Ramda.js を通してカリー化に触れてみる


こんにちは、フロントエンドエンジニアの辻です。
前回の記事では JavaScript の関数型ライブラリ「Ramda.js」を紹介しました。
今回は Ramda.js を通してカリー化に触れてみます!

カリー化とは?

前回の記事でサラリと触れましたが、改めてカリー化について書きます。
Ramda.js 固有の考え方ではなく、関数型プログラミングに登場する考え方です。

カリー化を Wikipedia で調べてみると、以下のような説明が見つかります。

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

カリー化 Wikipedia より引用

…なかなかに難しいですね。
JavaScript を例にとって、見ていこうと思います。

例えば、以下のような引数を4つとる関数 calc があります。
calc 関数の実行内容は単純で、以下の計算をしているだけです。

  1. 第4引数に第1引数を加算する
  2. 1.の計算結果を第2引数で乗算する
  3. 2.の計算結果を第3引数で除算する
  4. 3.の結果を返す
const calc = (num1, num2, num3, target) => {
  return (((target + num1) * num2) / num3)
}

calc(10, 20, 30, 40) // 33.333…

では、calc 関数をカリー化を意識して書き直してみます。

const calc = (num1) => {
  return (num2) => {
    return (num3) => {
      return (target) => {
        return (((target + num1) * num2) / num3)
      }
    }
  }
}
calc(10)(20)(30)(40) // 33.333…

カリー化する前後で、calc 関数の実行方法が変わりました。
カリー化前は4つの引数全てを入れて実行していましたが、カリー化後は引数を1つずつ入れて関数を実行しています。

カリー化のメリット

さて、実行方法が変わったのは分かりましたが、実際のところ、この変化の何が嬉しいのでしょうか?
先程のカリー化後の calc 関数を例にとってみます。

まず calc 関数に、加算する数 10、乗算する数 20、除算する数 30 の3つの引数を設定します。その返り値を calc2 としましょう。
calc2 は「引数に対して、10を加算して、20で乗算して、30で除算するただの関数」ですので、任意の数値を引数にとって実行できます。

calc2(40) とすれば (((40 + 10) * 20) / 30) = 33.333…
calc2(50) とすれば (((50 + 10) * 20) / 30) = 40
…と計算できます。

const calc = (num1) => {
  return (num2) => {
    return (num3) => {
      return (target) => {
        return (((target + num1) * num2) / num3)
      }
    }
  }
}

const calc2 = calc(10)(20)(30) // 加算する数 10, 乗算する数 20, 除算する数 30,
calc2(40) // 33.333…
calc2(50) // 40
calc2(60) // 46.666…
calc2(70) // 53.333…

このようにカリー化された関数には「引数を使って関数の実行内容を柔軟に変更できる」・「関数の実行を保留し、任意のタイミングで実行できる」という特徴があります。

Ramda.js でカリー化に触れてみる

ここからが本題です。
Ramda.js を通してカリー化に触れてみます。

Ramda.js 公式サイトの whats-different に記載の通り、実は Ramda.js の関数は自動的にカリー化されるよう設計されています。
ですので、Ramda.js を導入した時点から特に何の設定もなしにカリー化の恩恵を受けられます。

前回の記事で登場した omit() を例にとってみます。
以下のように、R.omit() に引数を 1 つだけ指定すると「引数のオブジェクトからプロパティ a を取り除く関数」(omitA 関数)が返却されます。
omitA 関数はただの関数ですので、他のオブジェクトを引数にとり、任意のタイミングで実行できます。

import * as R from "ramda"

const omitA = R.omit(["a"]) // オブジェクトからプロパティ a を取り除く関数
omitA({ a: "a", b: "b", c: "c" }) // { b: "b", c: "c" }
omitA({ a: "a", d: "d", e: "e" }) // { d: "d", e: "e" }
omitA({})                         // {}

もう一つの例として equals() も使ってみます。
以下のように、R.equals() に引数を一つだけ指定すると、「引数が { a: "a", b: "b", c: "c" } と同一かを判定する関数」(equalsObj 関数)が返却されます。
omitA 関数と同じく、equalsObj 関数も任意のタイミングで実行できます。

import * as R from "ramda"

const equalsObj = R.equals({ a: "a", b: "b", c: "c" }) // { a: "a", b: "b", c: "c" } と同一かを判定する関数
equalsObj({ a: "a", b: "b", c: "c" }) // true
equalsObj({ a: "a", b: "b", d: "d" }) // false

このように、Ramda.js の関数はカリー化のおかげで「はじめに引数を設定して関数の実行内容を決定する → 任意のタイミングで関数を実行する」事が可能です。

おわりに

今回は Ramda.js を通して、カリー化に触れてみました。
カリー化された関数を使えば、引数の設定と関数の実行を切り離せて、柔軟なコードを書けます。

また機会があれば、Ramda.js の機能や特徴に触れていきたいと思います。

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Tsuji Atsuhiro

フロントエンドエンジニア。 DTP・Webデザイナーを経験した後、フロントエンドエンジニアに転向。HTML/CSS/JavaScriptを中心にWeb開発を担当してきました。 UI・UXに興味があり、デザイン・コーディング両面から考えられるデザインエンジニアを目指しています。 普段はマラソンやボクシングなどで体を動かしてます。