Date post: | 02-Jul-2015 |
Category: |
Engineering |
Upload: | tanukkii |
View: | 4,483 times |
Download: | 5 times |
ECMAScript6による 関数型プログラミング
株式会社トライフォート 安田裕介
自己紹介• 名前:安田裕介
• Trifortに今年入社の新卒1年生
• Webフロントエンジニアやってます
• JavaScript, Scala, C++が好き
• GitHubアカウント: TanUkkii007
関数型プログラミングとは?
• 第一級オブジェクトとしての関数
• イミュータビリティ(不変性)
副作用を排除し関数オブジェクトを駆使する プログラミングパラダイム
関数型プログラミングの2大構成要素
拡張性と保守性の高いコードを書く手法として 近年注目を集めている
JavaScriptと 関数型プログラミングの関係JavaScriptは関数型プログラミング言語である
Schemeの第一級関数オブジェクトを受け継いだ言語
ECMAScript6は関数型の以下の機能をも可能にする
1. 変更不可能な変数の宣言 2. パターンマッチ 3. 再帰による繰り返し処理 4. 不変なデータ構造
1.変更不可能な変数の宣言
• 関数型プログラミングでは値の変更を行わない
• 変数への再代入を行わない
const宣言子
※strictモードの場合、再代入しようとするとTypeErrorとなる
※strictモードでない場合、再代入は暗黙に失敗する
※letと同様ブロックスコープをもつ
再代入できない変数を宣言する
1 "use strict"; 2 const foo = "foo"; 3 4 foo = “bar”; 5 //TypeError: foo is read-only
2.パターンマッチ
• 関数型プログラミングではパターンマッチによる値の取り出しを行う
• パターンマッチにより代入などの副作用を減らすことができる
分割代入(デストラクチャリング)
1 var [a,b] = [1,2];! 2 console.log(a, b);! 3 // 1 2! 4 ! 5 [b, a] = [a,b];! 6 ! 7! 8 var {name: name, family: {sister: sister}} !! ! ! ! ! ! = {name: 'John Doe', family: {sister: 1}}! 9 console.log(name, sister);! 10 // "John Doe" 1
配列パターンによる抽出
分割代入による値の交換
オブジェクトパターンによる抽出
配列やオブジェクトからパターンによって値を抽出する
※for in/ofループや関数の引数でも使えます
3.再帰による繰り返し処理
• 繰り返し処理の方法として再帰とループの2つの方法がある
• 再帰の方が代入などの副作用がなく、短く書ける
• 関数型プログラミングでは繰り返し処理に再帰を使う
• 末尾呼び出し最適化により再帰でのスタックオーバーフローを回避する
末尾呼び出し最適化関数呼び出しが末尾呼び出しかどうかを判定し、
末尾呼び出しの場合、最適化する
call(call(call(call())))
• 関数は呼び出し元に戻るため、その位置を記憶する
• でもそれが関数本体の末尾なら、戻る必要はない
そこで末尾呼び出し最適化がおきる
例)階乗の計算 1 // 再帰による階乗計算! 2 function factorial1(n) {! 3 if (n === 0)! 4 return 1;! 5 return n * factorial1(n - 1);! 6 }! 7 ! 8 //ループによる階乗計算! 9 function factorial2(n) {! 10 var result = 1;! 11 for (var i = 1; i <= n; ++i) {! 12 result *= i;! 13 }! 14 return result;! 15 }! 16 ! 17 factorial1(100000);! 18 // too much recursion! 19 factorial2(100000);! 20 // Infinity
←再帰ではスタックオーバーフロー
←ループではInfinityではあるが成功
←これが末尾呼び出し
末尾呼び出し最適化が実装されれば 解決される!!
@k_matsuzaki さんに指摘してもらいました。これでは最適化されません。正しくは↓
function factorial1(n, acc) { if (n == 0) return acc; return factorial1(n - 1, n * acc); }
factorial1(100000, 1);
4.不変なデータ構造
• 関数型プログラミングにおける配列やリストなどのデータ構造は不変であり、自身の値を変更しない
プロキシ既存のオブジェクトをラップし、その一部の内部メソッドをECMAScriptコードで実装して挙動を
変えることを可能にするオブジェクト
プロキシオブジェクトの作り方!!!new Proxy(target, { get: //proxy[name] set: //proxy[name] = val apply: //proxy() construct: //new Proxy() deleteProperty: //delete proxy[name] })
ラップするターゲットオブジェクト内部メソッドに実装を与えるハンドラーオブジェクト
がおきたときの処理を 定義できる
※ハンドラーの各メソッドをトラップという ※トラップは全部で14個ある ※定義されていないトラップ
ではデフォルトの挙動が用いられる ※apply, constructトラップは関数がターゲットとなるときのみ有効
例)プロキシによる イミュータブルな配列
pop, push, reverse, shift, sort, splice, unshift
関数型プログラミングでは値を変更しない ES6のプロキシを使って不変な配列を作ってみよう
方針JavaScriptの配列には自身を変更する破壊メソッドがある
破壊メソッドにsliceを挟むことで非破壊メソッドに変える
1 var immutable = { 2 Array: function(...array) { 3 return immutable.createArray(array); 4 }, 5 createArray: function(array = []) { 6 return new Proxy(array, { 7 get: function(target, name, receiver) { 8 var mutator 9 = immutable.mutators.filter(x => x === name)[0]; 10 if (mutator) { 11 return (...args) => { 12 var copy = target.slice(); 13 copy[mutator].apply(copy, args); 14 return immutable.createArray(copy); 15 }; 16 } else { 17 return target[name]; 18 } 19 } 20 }); 21 }, 22 mutators: ["pop","push","reverse","shift","sort","splice","unshift"] 23 };
破壊メソッドの判定用の配列
通常の配列と同様 Arrayコンストラクタで 配列を作れるようにする
1 var immutable = { 2 Array: function(...array) { 3 return immutable.createArray(array); 4 }, 5 createArray: function(array = []) { 6 return new Proxy(array, { 7 get: function(target, name, receiver) { 8 var mutator 9 = immutable.mutators.filter(x => x === name)[0]; 10 if (mutator) { 11 return (...args) => { 12 var copy = target.slice(); 13 copy[mutator].apply(copy, args); 14 return immutable.createArray(copy); 15 }; 16 } else { 17 return target[name]; 18 } 19 } 20 }); 21 }, 22 mutators: ["pop","push","reverse","shift","sort","splice","unshift"] 23 };
プロキシを 作成しているのはここ
!!5 createArray: function(array = []) { 6 return new Proxy(array, { 7 get: function(target, name, receiver) { 8 var mutator 9 = immutable.mutators.filter(x => x === name)[0]; 10 if (mutator) { 11 return (...args) => { 12 var copy = target.slice(); 13 copy[mutator].apply(copy, args); 14 return immutable.createArray(copy); 15 }; 16 } else { 17 return target[name]; 18 } 19 } 20 }); 21 }
ターゲットに配列を使用
コピーした配列で プロキシを再作成
破壊メソッドなら sliceを挟む関数を返す
getトラップ を定義
プロキシで作った不変配列は配列とどう違うのか?
!1 var array = new immutable.Array(1,2,3); 2 var nativeArray = new Array(1,2,3); 3 4 array[array.length - 1]; //3 5 nativeArray[nativeArray.length - 1]; //3 6 7 for (var v of array) console.log(v); //1 2 3 8 for (var v of nativeArray) console.log(v); //1 2 3 9 10 11 var result = array.push(4).reverse(); //[4,3,2,1] 12 array === result; //false 13 array; //[1,2,3] 14 15 nativeArray.push(4); 16 var nativeResult = nativeArray.reverse(); //[4,3,2,1] 17 nativeResult === nativeResult; //true 18 nativeArray; //[4,3,2,1]
使い方と挙動に 違いはない
不変配列では元の配列は 変わっていない
組み込みの配列では 元の配列は変わっている
←不変配列←組み込みの配列
まとめ
ECMAScript6の表現力で 関数型プログラミングを楽しもう!
1. 変更不可能な変数の宣言 2. パターンマッチ 3. 再帰による繰り返し処理 4. 不変なデータ構造
→ const宣言子 → 分割代入 → 末尾呼び出し最適化 → プロキシ
関数型の機能をES6でどう実現するかを見てきた