2021/08/11

reduceとsort、カリー化と関数の部分適用、クロージャー

@ 酒井悠宇

JavaScript

りあくとで学んだ以下の項目を復習します。

  • reduceとsort
  • カリー化と関数の部分適用
  • クロージャー


それではれっつごー

reduceとsortについて

この二つはどちらも配列の反復処理を行うメソッドで、他と違うところはメソッドの引数として渡す関数に二つの引数が存在するものだということ。
コードで見るとこんな感じ。

const arr = [1, 2, 3, 4, 5];

console.log(arr.reduce((n, m) => n + m)); //15
console.log(arr.sort((n, m) => n > m ? -1 : 1)); //[5, 4, 3, 2, 1]


reduceはこんな感じ。

  • 第二引数のmにはarr各要素の値が順番に入ってきて、第一引数のnには前回の関数の実行結果が返ってくる。
  • 最終的にreduce()が返すのは最後に実行された値だけ。


const arr = [1, 2, 3, 4, 5];

console.log(arr.reduce((n, m) => n + m)); //15


なので、最初から追って見ていくとこんな感じになる。

  1. m = 1、前回の実行結果がないので、結果は 1 がそのまま返る
  2. m = 2、前回の実行結果により、n = 1、結果は 1 + 2 = 3 が返る
  3. m = 2、前回の実行結果により、n = 3、結果は 3 + 3 = 6 が返る
  4. m = 2、前回の実行結果により、n = 6、結果は 6 + 4 = 10 が返る
  5. m = 2、前回の実行結果により、n = 10、結果は 10 + 5 = 15 が返る


色々やってるけど一言でまとめると配列の数値全てを足した値が出力されるってこと。

sortはこんな感じ。

  • 配列をアルファベット順に並べ替えるメソッド。sortの実行後は新しい配列が返されるのではなく、対象の配列が書き換えられる。
  • 引数には配列内の要素が順番に入る。第一引数には一番目の要素が入って、第二引数には二番目の要素が入る。


文字列を並べ替えたい場合、以下のようにsortするとアルファベット順に並べ替えられる。

let str = ['sa', 'mu', 'ra', 'i'];
str.sort();
 
console.log(str); // ["i", "mu", "ra", "sa"]


※ 数値を昇順に並べ替えたい場合、以下のようにsortしても昇順には並ばない。
なぜなら、数値の配列に対してsortメソッドを使用すると、数値が文字コードに変換され、文字コード順に並ぶから。
文字コードが何なのかはよくわからん。
とにかくこのやり方だと思い通りにならない。

lef num = [5, 3, 10, 6, 55];
num.sort();

console.log(str); // [10, 3, 5, 55, 6]


なので、数値を並べ替える場合は「比較関数」というものをつかう。
これは、2つの値を比較しながら、1つづつ順番を入れ替えていくという手法。
比較関数とは文字通り比較を行う関数のこと。

//比較関数のサンプル
function compareFunc(a, b) {
    return a < b;
}

これをsortの引数に当てはめることで、数値の並べ替えを実現する。

数値を昇順(1, 2, 3...)に並べ替えたい場合は以下の通り。

function compareFunc(a, b) {
  return a - b;
}
 
var num = [5, 3, 10, 6, 55];
num.sort(compareFunc);
 
console.log(num); //[3, 5, 6, 10, 55]


順を追って処理を見ていくと多分こんな感じ。

  1. 5 - 3 を行う。返ってくるのが整数なので、5は3より後
  2. 5 - 10 を行う。返ってくるのが負数なので、5は10より前
  3. 5 - 6 を行う。返ってくるのが負数なので、5は6より前
  4. 5 - 55 を行う。返ってくるのが負数なので、5は55より前
  5. 5が配列内の前から二番目に配置される
  6. 1 ~ 5みたいなことを他の数値でも行う
  7. 昇順の並べ替え完了


数値を降順(...10, 9, 8...)に並べ替えたい場合は以下の通り

function compareFunc(a, b) {
  return b - a;
}
 
var num = [5, 3, 10, 6, 55];
num.sort(compareFunc);
 
console.log(num); //[55, 10, 6, 5, 3]


順を追って処理を見ていくとこんな感じ。

  1. 3 - 5 を行う。返ってくるのが負数なので、5は3より前
  2. 10 - 5 を行う。返ってくるのが整数なので、5は10より後
  3. 6 - 5 を行う。返ってくるのが整数なので、5は6より後
  4. 55 - 5 を行う。返ってくるのが整数なので、5は55より後
  5. 5が配列内の前から四番目に配置される
  6. 1 ~ 5みたいなことを他の数値でも行う
  7. 降順の並べ替え完了


計算の部分は多分こんな感じて行われてると思われる。
注意点としては、新しい配列が返されるのではなく、対象の配列が書き換えられるてところ。

サンプルコードはわかりやすくするためにfunction関数で書いてあるけど、アロー関数でこのように省略して書くこともできる。

const arr = [1, 2, 3, 4, 5];

arr.sort((n, m) => n - m);  //[1, 2, 3, 4, 5]
arr.sort((n, m) => m - n); //[5, 4, 3, 2, 1]


挙動から見るに、比較関数の返り値がtruthyな値だと後ろに並べ替えられて、falsyな値だと前に並べ替えられるっぽい。
ってことは配列の要素数とtrue/falseさえわかれば並べ替えられる。
だから最初に出した例のように書くこともできるってことか。なるほど。

console.log(arr.sort((n, m) => n > m ? -1 : 1)); //[5, 4, 3, 2, 1]


カリー化と関数の部分適用


カリー化:「複数の引数を取る関数を、より少ない引数を取る関数に分割して入れ子にすること」


カリー化する前のコード

const multiply = (n, m) => n * m
console.log(multiply(2, 4));

multiplyは引数に受け取った値の積を返すだけの関数。

カリー化した後のコード

const withMulthple = (n) => {
  return (m) => n * m
}

console.log(withMulthple(2)(4));

withMulthpleはnを引数に受け取った上で、「mを引数に受け取りnとの積を返す関数」を返す関数。

アロー関数でカリー化したコード

const withMulthple = (n) => (m) => n * m ;
console.log(withMulthple(2)(4));


このように複数の引数をとる関数を、より少ない引数を取る関数に分割して入れ子にすることをカリー化という。

カリー化された関数の部分適用


const withMultiple = (n) => (m) => n * m;
console.log(withMultiple(3)(5));

const triple = withMultiple(3);
console.log(triple(5)); // 15


カリー化された関数の一つ目の引数に3を渡してできた関数にtripleという名前をつけている。
このようにすれば、どんな数を渡しても常に3倍される関数を作ることができる。
このようにカリー化された関数の一部の引数を固定して新しい関数を作ることを、「関数の部分適用」という。

クロージャー


Closure。日本語に訳すと「閉鎖」という意味。
プログラミングでは「関数閉包」つまり、関数を関数で閉じて包むことを意味する。

クロージャーのメリットを理解するためにまずは閉じていない状態を考える。

let count = 0;

const increment = () => {
  return count += 1;
};

$ increment();
▶︎ 1

$ increment();
▶︎ 2

$ count
▶︎ 2


一つづつカウントアップしていく単純なカウンターだが、グローバル変数countはどこからでも参照ができて、任意に書き換えができてしまう。なので、incrementは参照透過的ではなく、挙動が予測不可能な関数だと言える。(参照透過性のない関数)

参照透過性とは、その式をその式の値に置き換えても、プログラムの観測可能な振る舞いが変わらないことを指します。 別の言い方をすると、参照透過性のある関数は、同じ入力に対して、同じ作用と同じ出力を持ちます


これを丸ごと関数の中に入れてみる。

const counter = () => {
  let count = 0;

  const increment = () => {
    return count += 1;
  };
};


安全にはなったが、カウンターの機能が完全に関数の中に閉じ込められていて使えない。
つまりcounterを実行しても毎回初期化されカウントアップされない。(参照透過性のある関数)


機能だけを外から使えるように出してみる。

const counter = () => {
  let count = 0;

  const increment = () => {
    return (count += 1);
  };

  return increment;
};


安全性を保ったままカウントアップの機能も使えるようになった。


言葉で説明すると、
最初に例としてあげたこれ(グローバル変数に依存してるけどカウントアップされてたやつ)を、

let count = 0;

const increment = () => {
  return count += 1;
};

$ increment();
▶︎ 1

$ increment();
▶︎ 2

$ count
▶︎ 2


動作した環境ごと関数の中に閉じ込めている。
という説明になる。

const counter = () => {
  let count = 0;

  const increment = () => {
    return (count += 1);
  };

  return increment;
};

でも正直なんでこのcountの状態が保持されたままになるのかがわからない。

この例ではcountが毎回初期化されるけど、

const counter = () => {
  let count = 0;

  const increment = () => {
    return count += 1;
  };

  increment();
};


こっちの例ではcountが初期化されない

const counter = () => {
  let count = 0;

  const increment = () => {
    return (count += 1);
  };

  return increment;
};


前者はcounter内でincrement関数を実行してる。
後者はincrementの参照をcounter実行時に返り値として返してる。

この違いがcountが初期化されるかされないかの違いを生んでる。

りあくとでなぜこうなるのかの説明があったのでまとめてみる。

javascriptのメモリのライフサイクル


言語の種類に関わらず、一般的にメモリのライフサイクルというの以下のようになっている。

  1. 必要なメモリを割り当てる
  2. 割り当てられたメモリを使用する
  3. 必要がなくなったら割り当てたメモリを解放する


javascriptのような高水準言語は3を手動で行う必要はなく、不要になったメモリ領域を自動的に判別し解放する機能が装備されている。(ガベージコレクタ)例えると死神のようなもの。

この死神がいることによって、この世で死者が溢れかえって生者の居場所を圧迫してしまうということが起こらずに済んでいる。

現在確保されているメモリ領域が、この世にいるべきではない死者かどうかを判断しているのがこのガレージコレクタだが、その判断基準は「他の生者から必要とされているかどうか」ということ。つまり、そのメモリ領域への参照があるかどうかということ。

こちらの例(countが毎回初期化される方)は処理がこの関数内で完結しているので、処理が終われば確保されていたメモリ領域は死者であると判断され、ガベージコレクタによってメモリ解放(初期化)される。
だからカウントアップ機能が機能しない。

const counter = () => {
  let count = 0;

  const increment = () => {
    return count += 1;
  };

  increment();
};


こちらの例(カウントアップが正常に行われる方)は、関数incrementへの参照が、counterの実行元に返されるので、つまり「他の生者から必要とされている関数である」と判断される。なので、関数の実行によって確保されたメモリ領域は処理が終了してもメモリ解放(初期化)されない。だからcountが状態を保ち続ける変数となり、正常にカウントアップを行うことができる。

const counter = () => {
  let count = 0;

  const increment = () => {
    return (count += 1);
  };

  return increment;
};


なるほど!めちゃしっくりきた!

ちなみにクロージャーは必ずしも内側の関数を返す必要はないみたいです。

単に外のスコープの自由変数を参照する関数をさらに関数で包み込んだものをクロージャーと呼ぶ。
包み込んでいる外側の関数はエンクロージャーと呼ぶ


まとめ

  • reduceとsortはメソッドの引数として渡す関数に二つの引数が存在するメソッド
  • reduceを使うと結果的に配列の数値全てを足した値が出力される
  • sortは配列をアルファベット順に並べ替えるメソッド。数値に対して使用する時は比較関数を引数に渡す。
  • 複数の引数を取る関数をより少ない引数を取る関数に分割して入れ子にすることをカリー化という。
  • カリー化された関数の一部の引数を固定して新しい関数を作ることを、関数の部分適用という。
  • クロージャーとは関数を関数で閉じて包むことを意味する。
  • 関数内関数への参照をエンクロージャーによって関数の外に渡すと、その関数は状態を持つクロージャーになる。
© 2021 powerd by UnReact