関数合成
途中の結果に、毎回名前をつけている
記事のタイトルから URL 用の文字列を作るとします。前後の空白を落として、小文字にして、残った空白をハイフンに変える。素直に書くと、途中の結果に名前をつけ続けることになります。
JavaScript
const trimmed = trim(title);
const lowered = toLower(trimmed);
const slug = hyphenate(lowered);trimmed にも lowered にも、それ自体の意味はありません。次の関数に渡すためだけの名前です。かといって入れ子にすると、今度は読む向きが逆になります。
JavaScript
hyphenate(toLower(trim(title)));実際に動く順番は内側の trim からですが、目に入るのは外側の hyphenate からです。関数が 3 つならまだしも、5 つ 6 つと増えると、括弧の対応を数えながら内側へ潜ることになります。
小さい関数を並べて 1 本にする
やりたいのは「この 3 つを、この順に通す」だけです。関数を受け取って関数を返す部品を用意すると、並べた通りの 1 本の関数が手に入ります。
JavaScript
const chain = (...steps) => (value) =>
steps.reduce((acc, step) => step(acc), value);
const slugify = chain(trim, toLower, hyphenate);
slugify(" Hello World "); // "hello-world"chain(...) が返すのは値ではなく関数です。ここで slugify という新しい関数が 1 つ増えたので、別の場所でそのまま呼べますし、map にそのまま渡すこともできます。部品の trim や toLower は単体でも使えるまま残っているので、組み合わせを変えれば別の変換もすぐ作れます。
つなぐときの条件は 1 つだけです。前の関数が返すものを、次の関数がそのまま受け取れること。文字列を返す関数の後ろに、配列を受け取る関数を置くとつながりません。部品を作るときに入口と出口を揃えておくと、後から自由に並べ替えられます。
並べる向きで結果が変わる
順番は飾りではありません。" Hello World " に対して、空白をハイフンに変えてから前後を落とすと --hello-world-- になり、前後を落としてから変えると hello-world になります。同じ 2 つの関数でも、向きが違えば別の結果です。
ここでつまずきやすいのが、向きの流儀が 2 つあることです。数学の記号 f ∘ g は「先に g、後で f」と読み、右から左に進みます。ライブラリの compose はこの記法に合わせてあり、pipe や flow は逆に、左から右へ書いた順に進みます。名前を見たら、どちらの向きかを先に確かめてください。自分で組み立てるときも、どちらの向きで受け取る部品なのかを決めてから中身を書きます。
覚え方 ...
composeは右から左、pipeは左から右。迷ったら 2 つの関数で試すと一発で分かる。
Python でも結果を次の関数へ渡す
文字列から URL 用の文字列を作るなら、例えば次の 2 段階になります。
Python
def trim(text):
return text.strip()
def lowercase(text):
return text.lower()
cleaned = trim(" Hello World ")
print(lowercase(cleaned)) # hello worldstrip は前後の空白を落とし、lower は小文字にする文字列のメソッドです。最初の結果を次へ渡す順序が大切です。この例はすぐ値を計算しますが、合成ではその順序を覚えた関数を作り、後から入力を渡します。
Python では普通の def を別の関数の内側にも書けます。内側の関数は外側で受け取った関数を覚えて使えます。この性質をクロージャと呼びます。関数名を括弧なしで返せば、まだ呼び出さずに関数そのものを返せます。受け取った側で括弧と入力を付けると、その時点で計算が始まります。
演習では足し算と倍にする部品、内側の def の置き場所を用意しました。結果を次へ渡す処理、関数を返す処理、組み立てと適用は自分で書きます。
要件
addOneとdoubleは変更しない- 内側の関数で、先の関数の結果を次の関数へ渡す
composeは関数を返し、addOneThenDoubleは合成した関数を呼んで値を返す