164
177

More than 5 years have passed since last update.

忙しい人のためのjavascriptでざっくり理解する関数型

Last updated at Posted at 2019-08-24

対象読者

本記事はタイトルの通り、javascriptという身近な言語で関数型プログラミングのエッセンスを概要レベルで学びたい人向けの内容です。本気で「関数型」を追求している方にとっては物足りたいどころかツッコミどころも多いかと思いますが何卒ご容赦ください。

しかし一方で、「関数型って最近聞くけどどういうものなんだろう?」と思っている方々にとっては、『明日から使える』レベルで実践的な内容になるように心がけています。

そもそも関数型って?

2019/08/25 コメントで頂いたご指摘を元に修正しました。どういう形で修正させて頂くか迷ったのですが、ここでは「ざっくり理解する」に主眼を置いた表現を目指すことにいたしました。キチンとした定義を勉強したい方はコメント欄をご参照ください。

関数型言語や関数型プログラミングの定義を厳密に理解しようとするとやや難しい話になるので、ここではざっくりと、関数(function)という型が最初から用意されている言語およびそれを生かしたプログラミングをすることと理解してください。

javascriptでいえば、

function add(n, m){
   return n + m;
}

console.log(typeof add); //-> 'function'

var sub = function(n, m){
   return n - m;
}

console.log(typeof sub); //-> 'function'

という風に、上記のaddやsubはfunction型の変数として、stringnumberなど他の型の変数と同様に扱うことができます。

つまり、特別なライブラリやフレームワークなどを導入しなくても、言語そのものが関数(function)型を用意している言語を(広い意味での)関数型言語というわけです。この定義に依れば、javascriptだけではなく例えばPHPのような身近な言語も関数型言語であるといえるわけです。

関数型というのは何もhaskellやF#を書いている人たちだけのものでなく、我々プログラマ全体にすでに浸透している概念だということをまずは理解してください。

javascriptにおける関数の宣言

今後の説明をわかりやすくするため、ここでjavascriptにおける関数の宣言、言い換えれば、function型の変数(や定数)を作る方法を整理させてください。同じ関数を3つの方法で宣言してみようと思います。

通常の関数宣言

function add(n, m){
   return n + m;
}

console.log(typeof add); //-> 'function'

おそらく多くの方にとって見慣れた形かと思います。

ただし、関数型を理解する上で抑えて欲しい点は、上記のコードは実質、addという名前のfunction型の変数を宣言しているのと同じだという点です。

function say(){
   return 'hello!';
}

//say()は関数の実行結果を表す
console.log(say()); //-> 'hello!'
//なので型はstring
console.log(typeof say()); //-> 'string'

//一方、sayはfunction型の変数そのものを指す
console.log(say);//-> [Function: say]
//なので、型はfunction
console.log(typeof say);//-> 'function' 

というように、say()のようにカッコをつけて関数を呼び出すというおそらく多くの人にとって見慣れた使い方の他に、sayというfunction型の変数そのものを扱うことができるということをここではまずご理解ください。

匿名関数

実は、先ほどのadd関数の宣言は以下とほぼ同義です。

var add = function(n, m){
   return n + m;
}

console.log(typeof add); //-> 'function'

上記のコードはaddという変数を用意して、そこにnmを引数に取る匿名関数を代入しています。

なぜ関数をわざわざ匿名で宣言するのか理解に苦しむ方もいらっしゃるかと思いますが、その解説は後ほどいたしますので、ここでは「そういう宣言方法もあるんだ」程度にご理解いただければと思います。

(ES2015以降) アロー関数

javascriptの中でもES2015というバージョン以降では、上記2つに加えてさらにアロー関数という方法で関数を宣言することができます。

var add = (n, m) => {
   return n + m;
}

console.log(typeof add); //-> 'function'

匿名関数からfunctionという表記を無くして、代わりに()の後ろに=>という「アロー(arrow,矢)」を足したような形ですね。

さらに、今回のaddのように中の処理が1行の場合は、{}returnを省略することができます。

var add = (n, m) => n + m;

さらにさらに、引数が1つの場合は、引数を囲む()も省略できます。

var areaOfCircle = r => r * r * 3.14; //円の面積を計算する関数。rは半径。

ここまでくるとなかなか初心者泣かせというか、一見理解しにくい省略ですね。
ここでは、このアロー=>の形を見かけたら、「これは関数なんだ」と理解できることが大事だと思います。

関数型の性質

それでは話を戻して、そもそもfunctionという型の変数を作ることができて何が嬉しいのかについてお話ししたいと思います。ここでは関数型の性質を3つご紹介したいと思います。

1. 関数を変数に代入することができる

function型という見慣れない型であるとはいえ、変数は変数なので、当然その中身を他の変数に代入することができます。

function add(n, m){
   return n + m;
}

//当然、add関数はそのまま呼べる
console.log(add(1,1));//->2

//add関数をcalcという変数に代入しても
var calc = add;
//その名前で呼ぶことができる
console.log(calc(1,1));//->2

これだけだと「何が嬉しいの?」という感じかも知れませんが、もう少し応用してfunction型の配列まで作ると、例えば以下のようなことができます。

/**
* メッセージをログに出力する
*/
function logMessage(msg){
   console.log(msg);
}

/**
* メッセージを表示する
*/
function displayMessage(msg){
   $('#message').html('<span>' + msg + '</span>');
}

/**
* メッセージを保存する
*/
function storeMessage(msg){
   localStorage.setItem('message',msg);
}

/**
* メッセージを取り扱う関数の配列
*/
var messengers = [];

/**
* 以下、logEnabled,displayEnabled,storeEnabledは設定値か何かから取得したと仮定してください
*/
if(logEnabled) messengers.push(logMessage);// logEnabledがtrueであればlogMessage関数を有効にする
if(displayEnabled) messengers.push(displayMessage); //displayEnabledがtrueであればdisplayMessage関数を有効にする
if(storeEnabled) messengers.push(storeMessage); //storeEnabledがtrueであればstoreMessage関数を有効にする

/**
* 設定値に応じてメッセージを取り扱う
*/
function message(msg){
   /**
   * messengersという関数の配列から1つずつ関数を取り出して
   * 受け取ったmsgを渡して実行する
   */
   for(const messenger of messengers){
      messenger(msg);
   }
}
/**
* ちなみに、後半で解説する関数適用やアロー関数を組み合わせるとmessage関数は非常にシンプルに書けます
*/
//const message = msg => messengers.forEach(messenger => messenger(msg));

message('Hey!'); //->設定値応じて表示されたりログに出力されたり様々な挙動をする

2.関数を引数に取る関数を作ることができる

stringなど他の型と同様に、function型の値も関数の引数にすることができます。

/**
* beforeを実行してsleepTimeMsミリ秒後にafterを実行する
* @param {function} before sleep前に実行する関数
* @param {function} after sleep後に実行する関数
* @param {number} sleepTimeMs sleepする時間(ミリ秒)
*/
function sleep(before,after,sleepTimeMs){
    before();

    const timerId = setInterval(
       function(){
          after();
          clearInterval(timerId);
       },
       sleepTimeMs);
}

function beforeSleep(){
   console.log("寝たよ!!");
}

function afterSleep(){
   console.log("起きたよ!!");
}

sleep(beforeSleep, afterSleep, 5 * 1000); //->"寝たよ!!"と表示され5秒後に"起きたよ!!"と表示される

ところで、上記のafterSleepやbeforeSleepはsleep関数に引数として渡すためだけに存在する関数です。他から呼ばれる見込みもありません。こんな時、わざわざ上記のように名前付きで宣言するのはやや冗長な気がします。

こんな時こそ匿名関数の出番です。

//beforeSleepやafterSleepを宣言する代わりに匿名関数として引数に渡す
sleep(
   function(){
      console.log("寝たよ!!")
   },
   function(){
      console.log("起きたよ!!")
   },
   5 * 1000
   );

匿名関数の使い道は色々とあるのですが、まず最初のうちはある関数の引数としてしか使わない関数を宣言するために使うと覚えていただいて問題ないかと思います。

もう少し身近な例を出しましょう。

おそらくみなさんが大好きなjQuery日本語リファレンスさんより、click(fn)のページから利用例を抜粋します。

$("p").click(function () {
  $(this).slideUp();
});
$("p").hover(function () {
  $(this).addClass("hilite");
}, function () {
  $(this).removeClass("hilite");
});

clickメソッドの引数として、クリックされたらして欲しい挙動を匿名関数として渡していますね。
また、hoverメソッドは2つの関数を引数に取るようです。
第1引数としてカーソルが当たった時にして欲しい挙動を、第2引数としてカーソルが外れた時にして欲しい挙動を、それぞれ匿名関数として渡しています。

このように、関数を引数に渡すという設計はjQueryのような身近なライブラリでも当然のように使われおり、jQueryを使用しているということは(意識しているかどうかに関わらず)みなさんは既に関数型の恩恵を受けているということです。

3.関数を返す関数を作ることができる

ここまででわかる通り、function型もstringやnumber,booleanなど他の型と全く同じなので、当然、関数の戻り値として返すこともできます。

/**
* nを受け取り、nとmを足す関数を返す
* @param {number} n 足される数
* @returns {function} mを受け取りnとmを足して返す関数
*/
function add(n){
   return function(m){
      return n + m; //上位関数の引数であるnを下位関数でも使用できるのがポイント
   }
}

//n=5に固定する
const add5 = add(5);//add5はmを受け取って5+mを計算して返す関数
console.log(add5(5));//->'10'
console.log(add5(10));//->'15'

//このように普通の関数に近い形で呼ぶこともできる
console.log(add(1)(2)); //->'3'
//ただし以下はNG。add関数に第2引数は渡せない。
//console.log(add(1,2));

なお、このように関数を返す関数のことを専門的には高階関数と呼んだりします。

この高階関数のメリットを初心者の方が理解するのはちょっと難しいかと思いますので、ここでは詳しい説明を省略させてください。きちんと理解したい方はクロージャという概念を勉強されると幸せになれると思います。

ここでは、

  • 関数を返す関数を作ることができる
  • 引数のうち1つ(以上)を固定できると何か良いことがあるらしい

程度の理解で十分です。

明日から使える応用例:配列への関数適用

javascriptの配列(Array)には、関数適用するためのメソッドがいくつか用意されています。
配列関数適用するというのはどういうことかと言うと、

  • 配列の各要素に対して関数を実行する(forEach)
  • 配列の要素を関数を使って絞り込む(filter)
  • 配列の各要素に対して関数を適用して別の配列を作成する(map)
  • 配列の各要素に対して関数を適用して配列以外の何かを作り出す(reduce)

というように、配列に対して関数を使って操作をしたり変形したりすることを言います。

代表的な例を一つずつ見ていきましょう。

取り扱うデータ

このデータを例に解説していきます。

const data = [
 {code:"IP",nameJa:"ITパスポート試験",nameEn:"Information Technology Passport Examination",season:["随時"]},
 {code:"SG",nameJa:"情報セキュリティマネジメント試験",nameEn:"Information Security Management Examination",season:["",""]},
 {code:"FE",nameJa:"基本情報技術者試験",nameEn:"Fundamental Information Technology Engineer Examination",season:["",""]},
 {code:"AP",nameJa:"応用情報技術者試験",nameEn:"Applied Information Technology Engineer Examination",season:["",""]},
 {code:"ST",nameJa:"ITストラテジスト試験",nameEn:"Information Technology Strategist Examination",season:[""]},
 {code:"SA",nameJa:"システムアーキテクト試験",nameEn:"Systems Architect Examination",season:[""]},
 {code:"PM",nameJa:"プロジェクトマネージャ試験",nameEn:"Project Manager Examination",season:[""]},
 {code:"NW",nameJa:"ネットワークスペシャリスト試験",nameEn:"Network Specialist Examination",season:[""]},
 {code:"DB",nameJa:"データベーススペシャリスト試験",nameEn:"Database Specialist Examination",season:[""]},
 {code:"ES",nameJa:"エンベデッドシステムスペシャリスト試験",nameEn:"Embedded Systems Specialist Examination",season:[""]},
 {code:"SM",nameJa:"ITサービスマネージャ試験",nameEn:"Information Technology Service Manager Examination",season:[""]},
 {code:"AU",nameJa:"システム監査技術者試験",nameEn:"Systems Auditor Examination",season:[""]},
 {code:"SC",nameJa:"情報処理安全確保支援士試験",nameEn:"Registered Information Security Specialist Examination",season:["",""]}
];

forEach -配列の各要素に対して関数を実行する

data.forEach(function(row){
   console.log(row.nameJa); //各行のnameJaを出力する
});//=>
/**
ITパスポート試験
情報セキュリティマネジメント試験
基本情報技術者試験
応用情報技術者試験
ITストラテジスト試験
システムアーキテクト試験
プロジェクトマネージャ試験
ネットワークスペシャリスト試験
データベーススペシャリスト試験
エンベデッドシステムスペシャリスト試験
ITサービスマネージャ試験
システム監査技術者試験
情報処理安全確保支援士試験
*/

配列(上記例ではdata)の各要素(例ではrow)を受け取る関数を渡すとそれを各要素ごとに実行してくれるメソッドです。
ループを使って以下のように書けるのであまりメリットを感じないかも知れませんが、

for(const row of data){
   console.log(row.nameJa);
}

前に例示したmessage関数のようにアロー関数と組み合わせて記述をシンプルにしたいときなどに重宝します。

/**
* 設定値に応じてメッセージを取り扱う
*/
//function message(msg){
   /**
   * messengersという関数の配列から1つずつ関数を取り出して
   * 受け取ったmsgを渡して実行する
   */
//   for(const messenger of messengers){
//      messenger(msg);
//   }
//} =>長い。。。

/**
* こっちの方がシンプル
*/
const message = msg => messengers.forEach(messenger => messenger(msg));

filter -配列の要素を関数を使って絞り込む

const result = data.filter(function(row){
   return row.season.indexOf('') > -1; //開催時期に'春'が含まれているデータのみに絞り込む
});

//アロー関数で書くとこちら
//const result = data.filter(row => row.season.indexOf('春') > -1);

console.log(result);//=>
/**
 *  [ { code: 'SG',
    nameJa: '情報セキュリティマネジメント試験',
    nameEn: 'Information Security Management Examination',
    season: [ '春', '秋' ] },
  { code: 'FE',
    nameJa: '基本情報技術者試験',
    nameEn: 'Fundamental Information Technology Engineer Examination',
    season: [ '春', '秋' ] },
  { code: 'AP',
    nameJa: '応用情報技術者試験',
    nameEn: 'Applied Information Technology Engineer Examination',
    season: [ '春', '秋' ] },
  { code: 'PM',
    nameJa: 'プロジェクトマネージャ試験',
    nameEn: 'Project Manager Examination',
    season: [ '春' ] },
  { code: 'DB',
    nameJa: 'データベーススペシャリスト試験',
    nameEn: 'Database Specialist Examination',
    season: [ '春' ] },
  { code: 'ES',
    nameJa: 'エンベデッドシステムスペシャリスト試験',
    nameEn: 'Embedded Systems Specialist Examination',
    season: [ '春' ] },
  { code: 'AU',
    nameJa: 'システム監査技術者試験',
    nameEn: 'Systems Auditor Examination',
    season: [ '春' ] },
  { code: 'SC',
    nameJa: '情報処理安全確保支援士試験',
    nameEn: 'Registered Information Security Specialist Examination',
    season: [ '春', '秋' ] } ]
*/

配列(上記例ではdata)の各要素(例ではrow)を受け取ってboolean(残したい要素ならtrue)を返す関数を渡すと関数の戻り値にしたがって要素を絞り込んでくれる(関数がfalseを返した要素を除外してくれる)メソッドです。

これをループで書くと以下のように冗長になるため、forEachの時とは違ってメリットが実感しやすいかと思います。

var result = [];
for(const row of data){
   if(row.season.indexOf('') > -1){
      result.push(row);
   }
}

map -配列の各要素に対して関数を適用して別の配列を作成する

const result = data.map(function(row){
   return row.code; //data配列からcode部分だけを抜き出した別の配列を作成する
});

//アロー関数で書くとこちら
//const result = data.map(row => row.code);

console.log(result);//->
/**
  * [ 'IP',
  'SG',
  'FE',
  'AP',
  'ST',
  'SA',
  'PM',
  'NW',
  'DB',
  'ES',
  'SM',
  'AU',
  'SC' ]
  */

配列(上記例ではdata)の各要素(例ではrow)を受け取って何らかの戻り値を返す関数を渡すことで、その関数の戻り値の配列を作ってくれるメソッドです。
上記例ではたまたまデータの絞り込み(codeだけを抜き出す)に使っていますが、mapの使用法は多岐に渡ります。
例えば以下のような使い方もよくあるパターンです。

const areaOfCircle = r => r * r * 3.14;//円の面積を求める関数

const radiuses = [1,2,3,5,8];//半径のリスト

//半径のリストから面積のリストを作成する
const areas = radiuses.map(areaOfCircle);

console.log(areas);//->[ 3.14, 12.56, 28.26, 78.5, 200.96 ]

このように、ある配列(上記例では半径のリスト)からそれと同じ長さの配列(例では面積のリスト)を作る時に使うメソッドだと理解頂けると良いと思います。

また、前述のfilterメソッドと組み合わせることにより、まるでSQLのSELECT文のように配列から任意の値を取り出すことができます。

const result = data
   .filter(function(row){
       return row.season.indexOf('') > -1; //開催時期に'春'が含まれているデータのみに絞り込んで
   })
   .map(function(row){
       return row.code;// codeだけを抜き出す 
   }); //まるでこんなSQLのよう -> SELECT code FROM data WHERE season.indexOf('春') > -1

//アロー関数で書く場合
//const result = data.filter(row => row.season.indexOf('春') > -1).map(row => row.code);

console.log(result); //->
/**
  [ 
  'SG', 
  'FE', 
  'AP', 
  'PM', 
  'DB', 
  'ES', 
  'AU', 
  'SC' 
  ]
 */

一見不思議な文法に見えますが、冷静に考えればそこまで不思議ではありません。filtermapも共に配列(Array)のメソッドであり、その戻り値が配列(Array)であるため、その戻り値に対してさらにfiltermapを繋ぐことができるというわけです。

const result = data //dataはArrayなので.filterや.mapを呼ぶことができる
   .filter(function(row){
       return row.season.indexOf('') > -1; 
   })// filterの戻り値もArrayなのでさらに.filterや.mapを繋げることができる
   .map(function(row){
       return row.code;
   })// さらにmapの戻り値もArrayなので...とやっていくと論理的には無限に繋げることができる

少し関数型の話題から外れますが、こういったテクニックはオブジェクト指向のテクニックでメソッドチェーンと言います。頭の片隅に入れておくと良いでしょう。

reduce -配列の各要素に対して関数を適用して配列以外の何かを作り出す

const result = data.reduce(function(prev,row){
   //開催時期が"春"の試験数と"秋"の試験数をそれぞれカウントする
   var next = Object.assign({},prev);
   if(row.season.indexOf("") > -1) next[""] += 1;
   if(row.season.indexOf("") > -1) next[""] += 1;

   return next;
 },{"":0,"":0});

//これに関してはアロー関数で書いてもあまり変わらないので省略します

console.log(result);//->{ '春': 8, '秋': 8 }

他と比べて若干定義が複雑ですが、前の実行結果(上記例ではprev)と配列の各要素(例ではrow)を受け取って次の結果を返す関数と初期値(例では{"春":0,"秋":0},必須ではない)を受け取って、配列以外の戻り値(例ではObject)を返すメソッドです。
少しややこしいので、もう少し単純な例を見てみましょう。

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

const sum = nums.reduce(function(prev,num){
   return prev + num; //配列の要素を順に足していって合計を求める
});
/**
* つまり、
* 1回目 1 + 2 = 3
* 2回目 3 + 3 = 6
* 3回目 6 + 4 = 10
* 4回目 10 + 5 = 15
* という動きをして最終的に15を返す
*/
//アロー関数で書くとこちら
//const sum = nums.reduce((prev,num) => prev + num);

console.log(sum);//->15

このように配列の各要素を何らかの観点でまとめて(reduceして)、数値なりオブジェクトなり文字列なりを作ります。
敢えてSQLで例えるならGROUP BY句のようなものでしょうか。

少々扱いが難しいメソッドではありますが、例えばAPIから取ってきたデータの配列を自分の扱いやすいオブジェクトの形に変換するなど、意外と使いどころが多いメソッドなので使えるようになっておきましょう。

まとめ 関数型の良さをライトに取り入れよう

いかがだったでしょうか?

『関数型について興味を持ってググってみたらやれ純関数副作用モナド関手だって難しいことを言われてそっと閉じた。。。』

という方もたくさんいらっしゃるかと思いますが、ここまで解説したjavascriptを初めとした我々に取って身近な言語にもすでにそのエッセンスが入り始めています。『何だか難しそう。。。』と敬遠するのではなく、もっとライトな気持ちで『ループでも書けるけど、ちょっとfilterとmapを使って書いてみようかな』程度の感覚で関数型のエッセンスを取り入れてもらえたら幸いです。

そして興味を持ってもらえたら、ぜひ改めて純関数モナドなど例の難しい言葉たちを改めて調べてみてください。きっと皆さんのコーディングスタイルに良い影響を与えてくれると思います。

164
177
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
164
177