blog

関数型プログラミング

つの関数が組み合わされ、オリジナルと同じ順序で実行された場合。結果は同じです。 lodash/fpモジュール関数が優先され、データは遅れます。 1つの引数を渡された関数は、残りの引数を待つ新しい関数を...

Mar 1, 2020 · 7 min. read
シェア

関数型プログラミングとは

第一に、JavaScriptでは関数が第一級市民であるため、Javascriptでは関数型プログラミングが可能です。これは、関数ができることを変数ができることを意味し、 アロー関数Promiseオブジェクト、 拡張演算子など 、ユーザーが関数型プログラミングのテクニックを十分に活用できるよう、ES6標準に多くの言語機能が追加されています。Javascriptでは、関数はアプリケーションのデータを表します。注意深い読者ならお気づきでしょうが、関数は文字列や数値、その他の任意の変数と同じようにキーワード var を使って宣言することができます:

const optiSplicing = (...agrs) => val => agrs.reverse().reduce((acc, fn) => fn(acc), val); 
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const Font = arr => arr.toUpperCase();
// const newFn = optiSplicing(optiSplicing(Font, first), reverse);
const newFn = optiSplicing(Font, optiSplicing(first, reverse));
console.log(newFn(['wqw', 'asa']), 8888)
console.log(Font(first(reverse(['wqw', 'asa']))), 999)

ES6仕様では、同じ関数をアロー関数を使って書くことができます。関数型プログラマーは小さな関数をたくさん書くので、アロー関数を使う方がずっと簡単です:

const _ = require('lodash');
// const log = v => {
// console.log(v);
// return v;
// }
const trace = _.curry((tag, v) => {
 console.log(tag, v);
 return v;
})
// _.split(str, sep);
const split = _.curry((sep, str) => _.split(str, sep) );
// _.toLower()
// join
const join = _.curry((sep, str) => _.join(str, sep) );
const map = _.curry((fn, arr) => _.map(arr, fn))
const f = _.flowRight(join('-'), trace('map  '), map(_.toLower), trace('split  '), split(' '));
console.log(f("NEVER SAY DIE"))

関数は変数なので、オブジェクトに追加することができます:

const _ = require('lodash');
const fp = require('lodash/fp');
const trace = _.curry((tag, v) => {
 console.log(tag, v);
 return v;
})
const f = fp.flowRight(fp.join('-'), trace('map  '), fp.map(_.toLower), trace('split  '), fp.split(' '));
console.log(f("NEVER SAY DIE"))

これらの文の効果は同じで、関数を1ogという変数に格納します。さらに、キーワード const を使用して 2 番目の関数を宣言します。この関数の主な目的は、その関数がオーバーライドされないようにすることです。JavaScriptでは、配列に関数を追加することもできます:

const fp = require('lodash/fp');
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first),fp.map(fp.toUpper), fp.split(' '))
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))

関数は他の変数と同じように、他の関数に引数として渡すことができます:


// class Container {
// constructor (value) {
// this._value = value;
// }
// map (fn) {
// return new Container(fn(this._value))
// }
// }
// const r = new Container(5)
// .map(x => x + 1)
// .map(c => c * c)
class Container {
 // 静的メソッド。コンテナを使って取得できる。
 static of (value) {
 // 静的メソッドは値を渡す。 新しいオブジェクトを返す。
 return new Container(value)
 }
 // new関数のオブジェクトはここに渡される。
 constructor (value) {
 // この関数はドロップバック関数と呼ばれる。._value変更の値。
 this._value = value;
 }
 // map 新しいコンテナ・オブジェクトのメソッドである。
 map (fn) {
 // mapメソッドを呼び出すとき。 コールバック関数があるだろう。このコールバック関数は、この._value関数の値。
 // const val = fn(this._value);
 return Container.of(fn(this._value));
 }
}
// Container 外部関数は、コンテナ.of()静的メソッドの取得
// このスタティック・メソッドは、リターンとして新しいオブジェクトを返す。
// コンテナ.of(5)関数のパラメーターはこの._value.
// コンテナのマップオブジェクトを呼び出すと、コールバック関数を使ってこの._value 
const r = Container.of(5)
 .map(x => x + 1)
 .map(c => c * c)
console.log(r, 8)

JavaScriptは関数型プログラミング言語であると言えます。これは関数がデータであることを意味します。関数は変数と同じように、アプリケーション内部で保存したり、取り出したり受け渡したりすることができます。

序数と宣言数

関数型プログラミングは、より広範なプログラミングパラダイムである宣言型プログラミングの一部でもあります。宣言型プログラミングは補助的なプログラミングスタイルであり、このスタイルのアプリケーションコードの特徴として、実行のプロセスよりも実行結果をより多く記述することが挙げられます。宣言型プログラミングの理解を深めるために、ゴールに到達するための具体的なプロセスに焦点を当てたコードを特徴とする命令型プログラミングと比較します。文字列をURL互換にする」という、より一般的なタスクを例にとってみましょう。一般的には、文字列内のスペースをすべてハイフンに置き換えることで実現できます。スペースはURLアドレスと互換性がないためです。まず、命令型プログラミングスタイルを使ってこのタスクを完了させます:

class Maybe {
 static of (value) {
 return new Maybe(value)
 }
 constructor (value) {
 this._value = value
 }
 map (fn) {
 // 入ってくるポイントの値がnull undefinedであるかどうかを判断するために、この関数をリセットする。._valuenullの値はコールバック関数を呼び出さない。
 // これは純粋関数の考え方に沿っている。入力と出力が対応している。
 return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value))
 }
 isNothing () {
 return this._value === null || this._value === undefined
 }
}
const r = Maybe.of('undefined')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => split(' ')
)
console.log(r);
// 問題は連鎖呼び出しだ。何が問題なのかわからない。

プログラムの構造を見ると、このようなタスクをどのように完了させるかということにしか関心がありません。forループとi文が使われ、等価演算子を使って代入が行われています。コードを単独で見ても、それ以上のことはわかりません。命令型プログラミング・スタイルでは、ユーザーが何をやっているのかを正確に理解できるように、広範なコメントで補う必要があります。では、同じ問題を解決するために宣言的プログラミング・スタイルを使ってみましょう:


class Left {
 static of (value) {
 return new Left(value)
 }
 constructor (value) {
 this._value = value
 }
 map (fn) {
 return this;
 }
}
class Right {
 static of (value) {
 return new Right(value)
 }
 constructor (value) {
 this._value = value;
 }
 map (fn) {
 return this.isNothing() ? Right.of(null) : Right.of(fn(this._value))
 }
 isNothing () {
 return this._value === null || this._value === undefined;
 }
}
// const r1 = Right.of(12).map(x => x + 2);
// const r2 = Left.of(12).map(x => x + 2);
// console.log(r1, r2, 888)
function parseJSON (str) {
 try {
 return Right.of(JSON.parse(str))
 }
 catch (e) {
 return Left.of({error: e.message})
 }
}
const r = parseJSON('{"name": "zs"}').map(x => x.name.toUpperCase());
console.log(r)

string.replaceメソッドを使用することで、文字列内のスペースがすべて置き換えられます。空白をどうするかの詳細は、rep1ace関数の中に抽象的にカプセル化されています。宣言型プログラムでは、構文自体が何が起こるかを記述し、関連する実装の詳細は隠します。宣言型プログラムは、コード自体が何が起こるかを記述しているので、特定の目的のために解釈するのが簡単です。

関数型プログラミングの基本概念

関数型プログラミングと「関数」と「宣言型」の意味を理解したところで、関数型プログラミングの核となる概念である不変性純粋関数データ変換高階関数再帰について説明します。

1. データを不変に保つ 2. できるだけ純粋な関数を使用し、パラメータを1つだけ受け取り、データまたは他の関数を返す 3. できるだけ再帰を使用してループを処理します。

MayBe

イミュータビリティとは不変性を意味します。関数型プログラミングでは、データはイミュータブルであり、修正することはできません。ネイティブのデータ構造を変更することなく、これらのデータ構造のコピーに編集を加え、ネイティブのデータの代わりに使用します。不変性が機能するメカニズムを理解するために、データがどのように変更されるかを見てみましょう。色を表すオブジェクトを見てみましょう:

const fp = require('lodash/fp') class IO {
static of (x) { return new IO(function () { return x }) } 
constructor (fn) {
this._value = fn 
} 
map (fn) {
// 現在の値と渡されたfnを組み合わせて新しい関数にする。
return new IO(fp.flowRight(fn, this._value)) }
}
// コール let io= IO.of(process).map(p => p.execPath) console.log(io._value())

Javascriptでは、関数の引数は実際のデータを指します。このように色採点を設定することは、元のカラーオブジェクトを変更することになり、元のクリーチャーを破壊することなく色採点関数を上書きすることが可能であるため、少々厄介です:

1.0オブジェクトの割り当て
const fs = require('fs') const fp = require('lodash/fp')
let readFile = function (filename) { 
 return new IO(function() { 
 return fs.readFileSync(filename, 'utf-8') }) 
 }
let print = function(x) {
 return new IO(function() { 
 console.log(x) return x }) 
 }
// IO(IO(x)) 
let cat = fp.flowRight(print, readFile) 
//  
let r = cat('package.json')._value()._value() 
console.log(r)

0bject.assignメソッドはコピーメカニズムです。

2.拡張演算子

同じ関数は、ES6仕様ではarrow関数、ES7仕様ではオブジェクト拡張演算子を使用して記述できます。rateColor関数は、拡張演算子を使用してカラーオブジェクトを新しいオブジェクトにコピーし、そのスコアリングをオーバーライドします:

const fp = require('lodash/fp') // IO Monad 
class IO {
static of (x) { 
 return new IO(function () { return x }) 
 } 
 constructor (fn) {
 this._value = fn 
 } 
 map (fn) {
 return new IO(fp.flowRight(fn, this._value)) }
 join () {
 return this._value() 
 } 
 flatMap (fn) {
 return this.map(fn).join() 
 }
}
let r = readFile('package.json') .map(fp.toUpper) .flatMap(print) .join()

rateColor関数は、JavaScriptの新バージョンの構文機能を使ったもので、前のものとほとんど同じです。カラーオブジェクトをイミュータブルオブジェクトとして扱うので、構文が少なくなり、見た目も少しすっきりします。

3.配列連結
console.log('global begin') // 最初に実行される
function bar () { // ここを通過することが宣言されている。彼がどうであれ。
 console.log('bar task') //  
}
function foo () { //  
 console.log('foo task') //  
 bar() // これも知っている。
}
foo() // ボス、この人は誰だ。この人知ってる。知り合いと話すのはいいことだ、まずは実行してみよう。
console.log('global end') // 出力

Array.concat メソッドは配列を連結します。この場合、 新しい 色のタイトルを含むオブジェクトを生成し、それをネイティブ配列のコピーに追加します。

ES6の拡張演算子を使って配列を連結することもできます。ここではJavaScriptの新しい構文が使用されており、その効果は以前のaddcolor関数と同等です:

console.log('global begin')
setTimeout(function timer1 () {
console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2 () {
console.log('timer2 invoke')
setTimeout(function inner () {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')
ストアード関数

純粋関数とは、入力パラメータのみに依存する結果を返す関数で副作用を発生させずグローバル変数やアプリケーションの状態を変更しません。ストアド関数の核となる概念です:

  1. 関数は少なくとも1つの引数を取る必要があります。
  2. この関数は値か他の関数を返さなければなりません。
  3. 関数は、渡された引数を変更したり、影響を与えたりすべきではありません。

純粋関数を理解するために、次に純粋でない関数を見てください:

selfEducate関数は純粋な関数ではありません。引数を取らず、値や関数を返しません。また、スコープ外の変数 Frederick を変更します。selfEducate関数が実行されると、"世界 "は変わります。これは副作用があります。書き換えます:

最後に、このバージョンのse1fEducateは純粋な関数になります。この関数の戻り値は、渡された引数personに基づいて生成されます。この関数は、渡された引数を変更することなく新しいpersonオブジェクトを返すので、副作用はありません。

データ変換

データが不変である場合、アプリケーションは内部でどのように状態遷移を行うのでしょうか? 関数型プログラミングでは、ある種類のデータを別の種類のデータに変換し、関数を使用して変換されたデータのコピーを生成することでこれを行います。 これらの関数は、命令型コードを少なくし、複雑さを大幅に軽減します。javascriptの関数的な性質を最大限に活用するためには、次の2つのコア関数が必須です:Array.mapと Array.reduceArray.map 例1:

Array.mapの例2: オブジェクトの配列内のオブジェクトを変更する純粋な関数を作成する必要がある場合、m3p関数がその作業を行います。次の例では、学校の配列を変更することなく、"Stratford "を "HB Woodlawn "に変更します。これらの変数は、元の配列に影響を与えることなく、更新された配列上で作られます:

配列をオブジェクトに変換する必要がある場合、Array.mapと0bject.keysを使用することで、上記の目的を達成することができます。0bject.keysメソッドを使用すると、オブジェクトの属性キーの配列を取得することができます。例えば、schools オブジェクトを schools の配列に変換することができます:

reduce関数を使用すると、配列を数値、文字列、布:値、オブジェクト、さらには関数など、任意の値に変換することができます。次の例では、数値の配列から最大値を求める方法を示します。配列を数値に変換するには reduce メソッドを使用します。

(高次関数

Array.mapArray.filterArray.reduceはすべて関数を引数として渡すことができるので、すべて高次関数です。

次に、商関数を実装する方法を見てみましょう。次の例では、invokeIf というコールバック関数が作成され、条件が true のときに呼び出され、条件が false のときに別のコールバック関数が呼び出されます;

CurTyingは、高階関数を使用する関数型プログラミング技法です。カリー化とは、実際には、すでに完了した操作の結果を、残りの操作が完了して利用可能になるまで保持する仕組みのことです。これは、関数の中の関数、つまりカリー関数を返すことで実現されます。カリー関数の例:

再帰的

再帰は、ユーザーが作成した関数が自分自身を呼び出すテクニックであり、ループを含む実世界の問題を解決する際に、再帰関数は代替手段を提供することができます。 以下の関数は、forループを使用して実現することもできますし、もちろん再帰を使用することもできます:

再帰のもう1つの利点は、関数型プログラミングの技法であり、非同期処理をうまく処理できることです。

Read next

アンチシェイクとスロットリングの実装

1. ディザリング 2. スロットリング 高頻度のイベントがトリガーされますが、n秒に1回しか実行されないため、スロットリングによって関数の実行頻度が薄まります。アンチディザリングとの違いは、各関数呼び出しが最後に実行されるのに対し、アンチディザリングは最後にしか実行されないことです。

Mar 1, 2020 · 1 min read