1. 概要
今までVB
などのOOP
にばかり慣れ親しんできていた
関数型プログラミングという言葉は知っているが、その実態を全く理解できていない
JavaScriptでは関数型プログラミングを意識することでいいことがあるようなので、学んだことをまとめる
補足
本記事はりあクト!という非常に良質な技術同人誌を読んでみて、理解が浅い点等を外部のソースを元に試しつつ補足するような構成となっております。
リンクの書籍を一読して記事を読むとさらに理解が深まるのではないかと思っております!
雑誌に出てくる新人さんの理解力が良すぎるので都度外部のソースを調べながら記事にしました。
2. 関数型プログラミングとは
参照透過的1な関数を組みあわせて解決すべき問題を対処する宣言型のプログラミングスタイルのこと
3. プログラミングパラダイムからみる関数型プログラミング
3.1. プログラミングのパラダイムの大分類
3.1.1. 命令型プログラミング
最終的な出力を得るために状態を変化させる連続した文によって記述されるプログラミングスタイルのこと
命令方に分類される代表的なパラダイム
- 手続き型プログラミング(C言語等)
- オブジェクト志向プログラミング(Ruby等)
3.1.2. 宣言型プログラミング
出力を得る方法ではなく、出力の性質・あるべき状態を文字通り宣言することでプログラムを構成する
「どうやって得るか」ではなく、「何を得たいか」というアプローチ
- 宣言型プログラミングの代表例はは
SQL
この宣言型プログラミングというのは、意味的に二つ存在する
- 一つは上記の内容
- 二つ目は、関数型プログラミング、論理プログラミング、制約プログラミング、の総称の意味
つまり、関数型プログラミングは宣言型プログラミングのうちの1つである
4. 手続き型と比較した関数型プログラミングの利点
4.1. オブジェクトが基本的にイミュータブル2なので、プログラムの予測可能性が高い
手続き形プログラミングでは、変数がミューダブル(可変)である場合が多く、繰り返しの処理などではその間のステップで予期せぬバグが混入する確率が高くなる
不変性を守ることでプログラムから副作用3が排除される
関数型プログラミングでは、副作用をなるべく排除するために
変数への代入も極力行わないようにする
代入しなければ壊れるものもないため
メソッドチェーンで式を繋ぎ、関数自体もその場限りの無名関数を使用する
4.2. 手続き形では「文」を使用する一方で関数型では「式」を組みあわせてプログラムを構成する
手続き形ではfor分の中でif文などで条件分岐したりする
関数型プログラミングでは、すべてが値を返す式の組み合わせであり
それが左辺から右辺へ評価されていき最終的な値へ到達する形になっている
それぞれの式が独立しており、相互に干渉がないので1度に考えることが少なくて済む
メソッドチェーンで式をつないだり、関数自体に無名関数を使用する理由
4.3. プログラミングのアプローチ
手続き形はボトムアップ的に積み上げていったものが最終的な成果物となる
関数型では最初から大雑把なものを作り上げてそこから絞り込んでいく
メソッドチェーンの例でいうと、リソースに対して
<resource>.filter(something)...
などとしていくことで、左に大雑把なもの(=リソース)が存在し
メソッドチェーンでリソースに対してフィルタリングや値の加工などの特定の処理をしていくことで目的のデータに仕上げていくイメージ
5. JavaScriptでのコレクションの配列処理
5.1. map
渡した配列を処理して新しい配列を返す
下記例では、配列arr
に対してmap
を使用して、要素に対して*3
した結果をmap1
に格納している
const arr = [1, 2, 3, 4];
const map1 = arr.map(x => x * 3);
console.log(map1); //[3, 6, 9, 12]
5.2. filter
渡した配列の中で条件に当てはまる値を抽出する
下記例では、配列arr
に対してfilter
を使用して、要素の中で3
で割り切れる要素をfilter1
に格納している
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const filter1 = arr.filter(x => x % 3 == 0);
console.log(filter1); //[3, 6, 9]
5.3. find
渡した配列で条件に適合した最初の値を返す
下記例では、配列arr
に対して、要素の並びで最初に5
で割り切れる要素をfind1
に格納している
find2
では、最初の値というのが分かりやすいように、arr
を降順にした並びに対して最初に5
で割り切れる要素を格納している
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const find1 = arr.find(x => x % 5 == 0);
const find2 = arr.sort((a,b) => (a > b ? -1 : 1)).find(x => x % 5 == 0);
console.log(find1); //5
console.log(find2); //10
5.4. findindex
渡した配列で条件に適合した最初のインデックスを返す
下記例では上記のfind
のインデックスを返すのと同じ
findIndex
とsort
の中身を関数化した
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const div5 = (x) => x % 5 == 0;
const descsort = (x,y) => x > y ? -1 : 1;
const find1 = arr.findIndex(div5);
const find2 = arr.sort(descsort).findIndex(div5);
console.log(find1); //4
console.log(find2); //0
5.5. every
渡した配列が条件を全て満たすかを真偽値で返す
const arr = [3, 6, 9];
const arr2 = [12, 15, 20];
const div3 = (x) => x % 3 == 0;
const every1 = arr.every(div3);
const every2 = arr2.every(div3);
console.log(every1); //true
console.log(every2); //false
5.6. some
渡した配列に条件を満たす値が1つでもあるかを真偽値で返す
const arr = [10, 20, 100]
const arr2 = [1, 3, 6, 101]
const over100 = (x) => x > 100 ;
const some1 = arr.some(over100);
const some2 = arr2.some(over100);
console.log(some1); //false
console.log(some2); //true
5.7. reduce
レシーバーの配列に対して、引数の関数を実行し、最終的に一つの値を返す
引数を以下のように取り、単一の処理を実行する
- 第一引数に実行値が蓄積される値
- 第二引数に現在処理される配列の要素
const arr = [1, 2, 3, 4, 5];
const total = (x, y) => x + y;
const reduce1 = arr.reduce(total);
console.log(reduce1); //15
5.8. sort
レシーバーの配列に対して、引数の関数を実行し、配列を並び替える
sort
は破壊的メソッドなので、レシーバーに対して実行すると、変数に代入しても元の配列も並び変えてしまう
const arr = [1, 2, 3, 4, 5];
const descsort = (x,y) => x > y ? -1: 1;
const arr1 = arr.sort(descsort);
console.log(arr1); //[5, 4, 3, 2, 1]
console.log(arr); //[5, 4, 3, 2, 1]
上記の例では、元の配列まで並び替えられてしまっている
slice()
を間に入れることで元の配列を並び替えずに実行できる
slice()
は、元は引数にstart
とend
を指定することで、レシーバーの配列から指定のインデックス間の要素をコピーすることができる関数
は引数なしであれば元の配列全体をコピーすることになるので、コピーした配列に対して破壊的メソッドを実行しても元の配列は変更されない
const arr = [1, 2, 3, 4, 5];
const descsort = (x,y) => x > y ? -1: 1;
const arr1 = arr.slice().sort(descsort);
console.log(arr1); //[5, 4, 3, 2, 1]
console.log(arr); //[1, 2, 3, 4, 5]
5.9. includes()メソッド
指定した値の要素が一つでも配列に含まれているかを真偽値で返すArrayのプロトタイプメソッド
const arr = [1, 2, 3, 4, 5]
const includes1 = arr.includes(5);
const includes2 = arr.includes(6);
console.log(includes1); //true
console.log(includes2); //false
5.x. 組み込みオブジェクトObject
の反復処理について
5.x.1. オブジェクトのキー名一覧を取得する
Object.keys(<objectName>)
で、<objectName>
で指定したObject
オブジェクトのキーの一覧を取得する
const obj = { name: `user`, age: 20 };
console.log(Object.keys(obj)); //["name", "age"]
5.x.2. オブジェクトの値一覧を取得する
Object.values(<objectName>)
で、<objectName>
で指定したObject
オブジェクトの値の一覧を取得する
const obj = { name: `user`, age: 20 };
console.log(Object.values(obj)); //["user", 20]
5.x.3. オブジェクトのキーと値の一覧を取得する
Object.entries(<objectName>)
で、<objectName>
で指定したObject
オブジェクトのキーと値の一覧を取得する
const obj = { name: 'user', age: 20};
for (const [key, value] of Object.entries(obj)) {
console.log(`${key} is ${value}`);
}
//"name is user"
//"age is 20"
6. 関数のカリー化
6.1. カリー化とは
複数の引数を取る関数を、
引数をもとの関数の最初の引数で
戻り値が最初の関数の引数としての残りの引数をとり
それを使って結果を返す関数である高階関数化する こと
※カリー化の定義について
上記のカリー化の定義は、必ずしもすべてのコンテキストにおいて成り立つものではないとのこと
関数が「カリー化」されていない言語でカリー化について定義するのであれば上記の定義が成り立つが
Haskellなどのすべての関数がカリー化されているような言語でのカリー化の定義はまた別とのこと
以下詳細
6.2. カリー化のサンプル
x
とy
を引数にとって加算する「二引数の関数」をplus
という「一引数を取る関数のチェイン」に直した例
const plus =
function (x) {
return function (y) {
return x + y;
}
}
console.log(plus(2)(3)); //5
6.3. カリー化された関数の部分的用
カリー化された関数の一部の引数を固定して新しい関数を作ること
カリー化された関数は、変数に代入する時に引数を省略すると、残りの引数を残した戻り値の関数を変数に代入することが出来る
下記の例ではplus
という関数がまず存在する
plus
は、y
を引数に取った関数をx
を引数に取った関数でカリー化してある関数。中身は加算しているだけ
これを部分適用するために、plus
の第一引数に5
を代入した新しい関数add5
を定義している
以降はこの部分適用された関数を使用することができる
const plus = x => y => x + y;
const add5 = plus(5);
console.log(plus(2)(3)); //5
console.log(add5(3)); //8
7. クロージャ(Closure)について
7.1. クロージャとは
関数と『その関数が作られた環境』という 2 つのものが一体となった特殊なオブジェクトのこと
クロージャは内側の関数から外側の関数スコープへのアクセスを提供します
以下でステップで理解する
7.1.1. 関数が入れ子になっている状態を考える
関数が入れ子になっているとき、内側の関数は外側の関数の変数を参照することができる
下記の例では、sample
という関数の中で、getmessage
という関数があり、これはsample
内の変数aaa
を返す
sample
内で最後にgetmessage()
を表示するようにしており、これは入れ子になっている関数の中で取得しているaaa
が表示されるので
入れ子になっている関数では、内側の関数から外側の関数の変数にアクセスできる
const sample = () => {
const aaa = 'msg';
const getmessage = () => aaa;
console.log(getmessage());
}
sample(); //"msg"
7.1.2. クロージャらしいコード
関数が入れ子になっている状態を考えたとき、最下部の関数が最上部の関数にアクセスできるとき
関数を変数に定義すると、その環境が変数に保存される
上記の特徴をレキシカル環境という
JavaScriptの関数オブジェクトは、関数が定義された時のコンテキストを保存している
逆に、クロージャでない関数(無名関数)は、実行された後はその関数へのポインタがないため、特定のタイミングでがGCに処理されることになる
function sample() {
let msg = 'aaa';
return {
getMessage: function() {
return msg;
}
,
changeMessage: function(varr) {
msg = varr;
}
};
}
let sample1 = sample();
let sample2 = sample();
console.log(sample1.getMessage()); //aaa
sample1.changeMessage(`ohayo`);
console.log(sample1.getMessage()); //ohayo
sample2.changeMessage(`good morning`);
console.log(sample2.getMessage()); //good morning
8. 非同期処理について
まずはPromise
やasync/await
について理解する前に非同期処理について復習
8.1. 非同期処理とは
あるタスクを実行している状態の時、そのタスクの処理を中断することなく別のタスクを実行できる処理方式のこと
同期処理はあるタスクの実行が別の処理の実行を待って処理を行う(同期をとっている)処理方式のこと
詳しくは以下
JSには非同期処理の処理方式として、コールバック
、Promise
、async/await
がある
8.2. コールバックとは
JavaScriptで非同期処理を制御するための仕組みの1つ
単純に非同期処理をネストして記述することで、上部の処理が終了しないとネストされた処理が実行されないようにすることができる
以下例では、ネストしない場合とネストした場合の処理結果を比較したもの
ネストしていないと、非同期処理がそれぞれで実行されるので、setTimeout
の時間が短い順に出力を返すようになる
const a = () => console.log('a');
const b = () => console.log('b');
const c = () => console.log('c') ;
window.setTimeout(() => {
a();
}, 300);
window.setTimeout(() => {
b();
}, 400);
window.setTimeout(() => {
c();
}, 200);
// c
// a
// b
const a = () => console.log('a');
const b = () => console.log('b');
const c = () => console.log('c') ;
window.setTimeout(() => {
a();
window.setTimeout(() => {
b();
window.setTimeout(() => {
c();
}, 400);
}, 300);
}, 200);
// a
// b
// c
ただし、これらの記述は冗長になりやすい(コールバック地獄という)
且つ、複数の非同期処理を並列実行して双方の完了を待って処理をするなどを実装するのは難しいなどの課題があった
8.3. Promise とは
上記のコールバック地獄を解決することができる機能
実際はECMAScript6(ES2015)で標準化された組み込みのクラスのこと
8.3.1. Promiseオブジェクト
Promise
を使用した非同期処理の関数では、Promise
オブジェクトというオブジェクトを返り値にもつ
8.3.2. Promiseオブジェクトのステートについて
Promise
オブジェクトは3つの内部状態を持つ
状態 | 意味 |
---|---|
pending(保留) | 処理が未完了 |
fulfilled(成功) | 処理が完了し、Promiseが値を1つ持っている |
rejected(拒否) | 処理が失敗した、またはエラーを返した |
また、
初期状態はpending
であり
#####これから処理の実行結果によってfulfilled
あるいはrejected
に遷移し
#####それ以降は状態変化はないので返す値が変わることもない
8.3.3. Promiseオブジェクトのコンストラクター
Promise
オブジェクトのコンストラクターは2つの関数(resolve
,rejected
)を引数に取る
- 1番目の関数:
resolve
resolve
に引数を渡して実行するとPromise
オブジェクトのステートがfulfilled
となり、Promise
オブジェクトが持つ値は引数の値になる
- 2番目の関数:
rejected
rejected
に引数を渡して実行するとPromise
オブジェクトのステートがrejected
になり、Promise
オブジェクトが持つ値は引数の値になる
また、関数がエラーを返したときもPromise
オブジェクトのステートはrejected
になる
const samplepromise = (t) => {
return new Promise((resolve, rejected) => {
() => {
if(x == y){
resolve
[wip]調べてまとめる
node-fetchについて
- モダンブラウザに実装されているネットワークリクエスト操作のためのFetchAPI をほぼ同じインターフェースでNode.jsから使えるようにしたライブラリ
FetchAPI - ネットワーク越しの通信を含むリソース取得のためのインターフェースが定義されている
- 第一引数にリソースのパスを指定する
- 成功か失敗によらずリクエストに対するResponseに解決できるPromiseオブジェクトを返す
async/await
Promiseで非同期処理をこれまでのコールバックインターフェースを使用した記述よりは直感的に記述できるようになり、深い階層にならずに済んでいる
それでも1階層でもコールバックをさせないためにPromiseの記述のシンタックスシュガーが用意されている
関数宣言時に async キーワードを付与するとその関数は『非同期関数』となって、
返される値が暗黙の内に Promise.resolve()によってラップされたものになる
そして非同期関数の中では他の非同期関数を、await 演算子をつけて呼びだすことができる
await 式によって非同期関数を実行すると、その Promise が resolve されるか reject されるまで文字通り待ってもらえるようになる
→非同期処理が終了してPromiseオブジェクトの内部状態がfulfilledかrejectedとなってresolveまたはrejectを返すまで待つということ
そして resolve されたら await 式は、そのラップしていた Promise から値を抽出して返してくれる
モジュールと型定義
typescriptのインポート/エクスポート
TypeScriptはモジュールについては言語レベルでES Modulesの構文を標準採用している
importとexportの書きかたはJSと変わらない
インポートに指定するパスでの拡張子の扱いが違う
Type Scriptでは拡張子をつけるとエラーになる
TypeScriptから読み込めるように JSのモジュールを作る
TypeScriptの環境設定について
TypeScriptのコンパイルオプション
tsconfig.jsonのカスタマイズ
target
コンパイル先のJavaScritのバージョンを指定するもの
。。。
現在ではtsconfig.jsonmファイルに手を加える必要はほぼ無くなった
以下二点だけカスタマイズしたい
“baseUrl”: “src”,
“downlevelIteration”: true,
型エイリアス
baseUrl
モジュールのインポートのパス指定に絶対パスを使えるようにしつつ、その起点となるディレクトリを指定するオプション
downlevelIteration
コンパイルターゲットがES5以前に設定されている場合でも、ES2015から導入された各種イテレータ周りのん便利な記述をES5以下でも実行できるように上手いこと書き下してくれるオプション