関数型プログラミングメモ

Posted: February 01, 2022

参考書籍

りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】 - くるみ割り書房 ft. React - BOOTH
りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】 ☝🏻 2021年 9月アップデート。React 17.0 と TypeScript 4.4 に対応。フロントエンド初中級者や前の版をお持ちの方にもオススメです。くわしくは下の目次をチェック! ―――― シリーズ累計 1.8 万部を突破、BOOTH 技術書カテゴリで売上 1 位を誇る『りあクト! TypeScriptで始めるつらくないReact開発』の最新 3.1 版。今版は三部構成となっており、本書はその第一部「言語・環境編」です。 第一部は Create React App の使い方に始まり、JavaScript、関数型プログラミング、TypeScript までをカバー。今版では「フロントエンドの開発になぜ Node.js が必要なの?」「webpack は何をしてくれてるの?」「import はどうやってファイルを読み込んでるの?」のような、学習者が抱く根本的な疑問にまで分け入ってくわしく説明していきます。 前版は言語についての解説はフロントエンド開発に必要な最低限の内容でしたが、本書ではそれに加えて JavaScript の一見不可解な仕様をその歴史から読み解いたり、TypeScript がどのように型を解決するかや、型定義をフレキシブルに行う方法など、中級以上にステップアップするための、より掘り下げた内容が盛り込まれています。 なお第二部では React のコンポーネントや Hooks、第三部では React の副作用処理を扱っています。続けてご覧ください。 ▫️ https://oukayuka.booth.pm/items/2368019 (Ⅱ. React 基礎編) ▫️ https://oukayuka.booth.pm/items/2367992 (Ⅲ.

関数型プログラミングの良い点

// Procedural programming
{
  const octuples = [];
  for (let n = 1; n < 101; n += 1) {
    if (n % 8 === 0) {
      octuples.push(n);
    }
  }

  console.log(octuples);
}

// Functional programming
{
  const range = (start, end) => [...new Array(end - start).keys()].map((n) => n + start);
  console.log(range(1, 101).filter((n) => n % 8 === 0));
}

不変性(immutability)→安全性

関数型は1つも変数の書き換えが発生しない。手続き型の方はoctuplesやnが何回も書き換えられている(バグが入り込む可能性があがる)。

関数型はそもそも代入を控える傾向にある。

文(statement)と式(expression)→シンプル

手続き型の方は、forやifを用いて値を返さないサブルーチンの中で処理が実行される。

関数型はすべてが値を返す式の組み合わせで、左辺から右辺に評価されていき最終的な値に到達するのでシンプル。

解へのアプローチ→意図がはっきりし可読性が高い

手続き型の方は、ボトムアップで積み上げていって最終的な解を出す。

関数型は、最初から完成形を見据えた上で大雑把なところから絞り込んでいく。

コレクションの反復処理

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

//map() 対象の配列の要素一つ一つを任意に加工した新しい配列を返す
console.log(arr.map((n) => n * 2)); // [2, 4, 6, 8, 10, 12, 14, 16, 18]

//filter() 与えた条件に適合する要素だけを抽出した新しい配列を返す
console.log(arr.filter((n) => n % 3 === 0)); // [3, 6, 9]

//find() 与えた条件に適合した最初の要素を返す。なかった場合はundefinedを返す
console.log(arr.find((n) => n > 4)); //5

//findIndex() 与えた条件に適合した最初の要素のインデックスを返す。なかった場合は-1を返す
console.log(arr.findIndex((n) => n > 4)); //4

//every() 「与えた条件をすべての要素が満たすか」を真偽値で返す
console.log(arr.every((n) => n !== 0)); //true

//some() 「与えた条件を満たす要素が一つでもあるか」を真偽値で返す
console.log(arr.some((n) => n >= 10)); //false
const arr = [1, 2, 3, 4, 5];

//reduce() 第一引数には前回の関数の実行結果、第二引数にはarrの各要素が順番に入る
//最後に実行された結果を返す
console.log(arr.reduce((n, m) => n + m)); //15

//sort() 第一引数は要素のいずれか、第二引数は第一引数の次の要素
//第一引数と第二引数を入れ替える場合は1を返し、入替えない場合は-1を返す。
console.log(arr.sort((n, m) => n > m ? -1 : 1)); //[5, 4, 3, 2, 1]

//sort()は破壊的メソットなので注意が必要
//sliceを間に挟むことでコピーをsortすることになり、元の配列は変更されない
const lst = [5, 7, 1, 3];
console.log(lst.slice().sort((n, m) => n < m ? -1 : 1)); //[1, 3, 5, 7]

sortは参考書籍だけだとイマイチイメージわからなかったので、以下のURLも参照

JavaScriptのsort関数の使い方を絶対わかるまで解説する
JavaScriptで数字を並び替えしたい時があってsort関数を使う機会があったのですが、どういう原理で並び替えをしているのかイマイチよくわからなかったので調べまくりました。 一通り理解できたので僕のように「何でそうなるの?」という人の助けに少しでもなればいいかなと思って記事を書きます。役に立たなかったらごめん。 また、 この記事では数値の並び替えで使われるコールバック関数付きのsort関数に絞って解説します(文字列のソートは比較関数を必要とせず sort() だけでソートできるので説明を割愛します)。 sort関数が並び替えをする関数ってことはみんな知ってると思うので、「何で並び替えができるの?」「どういう原理で並び替えをしているの?」といったことを見ていきます。ぜひ時間を書けてじっくり読むことをおすすめします。 まずは以下のサンプルを見てください。このサンプルは配列の中の数字を小さい順に並び替えます。 var numbers = [2, 5, 100, 4]; numbers.sort(function (a, b) { if (a b) { return 1; } else { return 0; } }); console.log(numbers); // 結果:[2, 4, 5, 100] こんな感じのコードはいろんな記事で紹介されていますが、僕は最初これを見た時にわからないことがいくつもありました。 まずはこのあたりを見ていきます。 例えば配列 numbersにある2を a、次の要素である5を b に入れてみます。 2と5を比較すると2 4となり、 a > bの条件を満たすので
const arr = [1, 2, 3, 4, 5];

//includes() 「指定した値の要素が一つでも膨れているか」を真偽値で返す
console.log(arr.includes(5)); //true
console.log(arr.includes(8)); //false

オブジェクトの反復処理

const user = {
  id: 3,
  name: 'Bobby Kumanov',
  username: 'bobby',
  email: 'bobby@maple.town',
};

//プロパティのキーリストを配列で取得
console.log(Object.keys(user));
// [ 'id', 'name', 'username', 'email' ]

//プロパティ値のリストを配列で取得
console.log(Object.values(user));
// [ 3, 'Bobby Bear', 'bobby', 'bobby@maple.town' ]

//キーと値が対になった2次元配列で取得
console.log(Object.entries(user));
// [
//   [ 'id', 3 ],
//   [ 'name', 'Bobby Kumanov' ],
//   [ 'username', 'bobby' ],
//   [ 'email', 'bobby@maple.town' ]
// ]

JavaScriptで関数型プログラミング

関数型プログラミングで行われること

  1. 名前のないその場限りの関数(無名関数)を定義できる
  2. 変数に関数を代入できる
  3. 関数の引数として関数を渡したり、戻り値として関数を返すことができる(高階関数)
  4. 関数に特定の引数を固定した新しい関数を作ることができる(部分適用)
  5. 複数の高階関数を合成してひとつの関数にできる(関数合成)

高階関数

引数として関数をとったり、戻り値として関数を返したりする関数。「コールバック」はこの引数として渡される関数のことをいう。

// 戻り値として関数を返すサンプル
const greeter = (target) => {
  const sayHello = () => {
    console.log(`Hi, ${target}!`);
  };
  
  // 実行結果(sayHello())でなく、関数そのもの(sayHello)を返す。
  // 実行結果を返すとundefinedになってしまう
  return sayHello; 
};

const greet = greeter('Step Jun');
greet() //Hi, Step Jun!

//余計な表記をなくすと以下のようにスッキリする
const greeter = (target) => () => console.log(`Hi, ${target}!`);

カリー化

複数の引数を取る関数を、より少ない引数を取る関数に分割し入れ子にする。

// Pre-curried
const multiply = (n, m) => n * m;
console.log(multiply(2, 4)); // 8

// Curried
const withMultiple = (n) => {
  return (m) => n * m;
};
console.log(withMultiple(2)(4)); // 8

// Curried with double arrow
const withMultiple = (n) => (m) => n * m;
console.log(withMultiple(2)(4)); // 8

関数の部分適用

カリー化された関数の引数の一部を渡して新しい関数を作ることを関数の部分適用という

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

const triple = withMultiple(3); //引数を一部だけ渡して3倍する関数を作る
console.log(triple(5)); //15

クロージャ

「関数閉包」と訳され、関数を関数で閉じて包む

//閉じられてない
let count = 0;
const increment = () => {
  return count += 1;
};

//上記ではcountがグローバル変数であり参照透過的でない。以下のように変更する
const counter = () => {
  let count = 0;
  const increment = () => {
    return count += 1;
  };
  return increment;
};
const increment = counter();

クロージャとは、関数と『その関数が作られた環境(コンテキスト)』という 2 つのものが一体となった特殊なオブジェクトのことを指す

よくわからなかったので以下リンクで補完

JavaScriptのクロージャとは?
まずはMDNによるクロージャーの定義はこちら。 クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせで>す。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript >では、関数が作成されるたびにクロージャが作成されます。 参照: https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures これを読んでも理解できなかったので、詳しく調べてみました。 クロージャーを理解する上で、まずダイナミックスコープとレキシカルスコープの違いを理解する事が大事そうだったので説明します。 JavaScriptは親の関数の変数を子の関数内で使える。 下のコードでいうと、親の関数が(ichiro), 子の関数が(jiro)であり、parentが親の関数の変数であり、この変数を子の関数のjiro内で使っている。 const ichiro_function = () => { const parent = "ichiro"; // 親の関数の変数 const jiro_function = () => { const jiro = "jiro"; //子の関数の変数 console.log(jiro); console.log(parent); //子の関数内で親の関数の変数を使用 }; return jiro_function(); }; ichiro_function(); //実行結果 //jiro //ichiro 反対にJavaScriptは子の関数の変数を親の関数内で使うとnot definedになります。 const ichiro_function = () => { const

JavaScriptでの非同期処理

Promiseオブジェクトを使うことで、任意の非同期処理の完了を待って次の処理を行うということが可能となる

const isSucceeded = true;

const promise = new Promise((resolve, reject) => {
  if (isSucceeded) {
    resolve('Success');
  }else {
    reject(new Error('Failure'));
  }
});

promise.then((value) => { // コールバックの結果(resolv or reject)がvalueになる
  console.log('1.', value);
  return 'Success again';
})
.then((value) => { // 一つ前のthenのreturnがvalueになる
  console.log('2.', value);
})
.catch((error) => {
  console.error('3.', error);
})
.finally(() => { // 処理が成功でも失敗でもfinallyの関数は必ず最後に実行される
  console.log('4.', 'Completed');
});

Promiseをハンドリングする

import fetch from 'node-fetch';

const getUser = (userId) => 
  fetch((`https://jsonplaceholder.typicode.com/users/${userId}`).then(
    (response) => {
      if (!response.ok) {
        throw new Error(`${response.status} Error`);
      } else {
        return response.json();
      }
    },
  );
  
getUser(2)
  .then((user) => {
    console.log(user);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log('-- Completed --');
  });

上記をシンタックスシュガー(async, await)を用いて書くと以下のようになる

コールバックがなくなるのでシンプルになる

import fetch from 'node-fetch';

const getUser = async (userId) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`,
  );
  
  if (!response.ok) {
    throw new Error(`${response.status} Error`);
  }
  
  return response.json();
};

const main = async() => {
  try {
    const user = await getUser(2);
    console.log(user);
  } catch(error) {
    console.error(error);
  } finally {
    console.log('-- Completed --');
  }
};

関数宣言時にasyncをつけると「非同期関数」となり、返される値はPromise.resolve()にラップされたものになる

非同期関数の中では、他の非同期関数をawaitをつけて呼び出すことができる。awaitをつけて非同期関数を実行するとPromiseから返りがあるまで待つ。awaitが使えるのは非同期関数の中だけ