Ramdaスタイルの考え方:宣言型プログラミング

1.最初のステップ
2.機能を組み合わせる
3.部分使用(カリー化)
4.宣言型プログラミング
5. Quintessential表記
6.不変性とオブジェクト
7.不変性と配列
8.レンズ
9.結論

この投稿は、Ramda Style Thinkingというタイトルの関数型プログラミングシリーズの4番目のパートです。

3番目の部分では、部分的なアプリケーションとカリー化の手法を使用して、複数の引数を取ることができる関数の組み合わせについて説明しました。

小さな機能的なビルディングブロックを記述し、それらを結合し始めると、算術、比較、ロジック、フロー制御などのJavaScript演算子をラップする多くの関数を記述する必要があることがわかります。 退屈に思えるかもしれませんが、私たちはラムダの背後にいます。

しかし、最初に、少し紹介します。

命令型と宣言型


プログラミング言語と記述スタイルを分離するには、さまざまな方法があります。 これらは、静的タイピングと動的タイピング、インタープリター言語とコンパイル言語、高レベルと低レベルなどです。

別の同様の区分は、命令型プログラミングと宣言型です。

深く掘り下げることなく、命令型プログラミングは、プログラマーがコンピューターに何をすべきかを指示し、その方法を説明するプログラミングスタイルです。 命令型プログラミングは、毎日使用する多くの構造を提供します:フロー制御( if - then - else構文とループ)、算術演算子( +-*/ )、比較演算子( ===><など) .d。)、および論理演算子( &&|| && )。

宣言型プログラミングは、プログラマーがコンピューターに何をすべきかを指示し、彼らが望むものを説明するプログラミングスタイルです。 コンピューターはさらに、目的の結果を取得する方法を決定する必要があります。

古典的な宣言型言語の1つはPrologです。 Prologでは、プログラムは一連の事実と一連の推論規則で構成されています。 質問をすることでプログラムを開始すると、Prologの推論規則のセットは事実と規則を使用して質問に答えます。

関数型プログラミングは、宣言型プログラミングのサブセットと見なされます。 関数型プログラムでは、関数を宣言し、これらの関数を組み合わせて何をしたいのかをコンピューターに説明します。

宣言型プログラムでも、命令型プログラムで実行する同様のタスクを実行する必要があります。 フロー制御、算術、比較、およびロジックは、引き続き作業する必要がある基本的な構成要素です。 しかし、これらの構造を宣言的なスタイルで表現する方法を見つける必要があります。

宣言的な代替


命令型言語であるJavaScriptでプログラミングしているため、「通常の」JavaScriptコードを記述する際に標準の命令型構文を使用するのが普通です。

しかし、パイプラインなどの構造を使用して機能変換を記述する場合、必須の構造は作成されたコード構造に適合しなくなります。

この不快な状況から抜け出すために、ラムダが提供する基本的な構成要素のいくつかを見てみましょう。

算術


2番目の部分では、一連の算術変換を実装して、組立ラインを示します。

 const multiply = (a, b) => a * b const addOne = x => x + 1 const square = x => x * x const operate = pipe( multiply, addOne, square ) operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169 

使用するすべての基本的なビルディングブロックの関数をどのように記述するかに注目してください。

Ramdaは、標準の算術演算の代わりに使用するための加算減算乗算、および除算関数を提供します。 したがって、自己記述関数を使用した場所でramd multiplyを使用でき、カレーのadd関数を利用してaddOneを置き換えることができます。 addOnemultiplyを使用しmultiply squareを記述することもできます。

 const square = x => multiply(x, x) const operate = pipe( multiply, add(1), square ) 

add(1)増分演算子( ++ )に非常に似ていますが、増分演算子は変数を変更して、突然変異を引き起こします。 最初の部分から学んだように、不変性は関数型プログラミングの主要な原則であるため、 ++またはその従兄弟である--を使用したくありません。

add(1)subtract(1)を使用して増減できますが、これら2つの操作は非常に一般的であるため、Ramdaはincdecを代わりに提供します。

したがって、パイプラインをもう少し単純化できます。

 const square = x => multiply(x, x) const operate = pipe( multiply, inc, square ) 

subtractは、値を否定するための二項演算子-代わりですが、まだ単項演算子があります。 マルチmultiply(-1)使用することもできますが、Ramdaはこのタスクを実行するための否定関数を提供します。

比較


また、 第2部では、人が投票する資格があるかどうかを判断するための関数をいくつか作成しました。 そのコードの最終バージョンは次のとおりです。

 const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

一部の関数では標準の比較演算子(この場合は===および>= )を使用していることに注意してください。 可能な限り、Ramdaはこれらすべての代替品も提供しています。

===代わりにequalsを、 >=代わりにgteを使用するようにコードを変換しましょう。

 const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18) const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

Ramda< > gt<のlt <および<= lteも提供します。

これらの関数は通常の順序で引数を取るように見えることに注意してください(最初の引数は2番目の引数よりも大きいのでしょうか?)。 これは、それらを単独で使用する場合は理にかなっていますが、関数を組み合わせる場合は混乱する可能性があります。 これらの関数は「データが最後に来る」という原則に違反しているため、組立ラインや同様の状況で使用する場合は注意が必要です。 そして、それがフリップとプレースホルダー( __ )の利点です。

equals加えてequals 2つの値がメモリ内の同じスペースへの参照であるかどうかを判断するための同一性がまだあります。

===は、主なアプリケーションのケースがいくつかあります。文字列または配列が空( str === ''またはarr.length === 0 )であることを確認し、変数がnullまたはundefinedかどうかを確認します。 Ramdaは 、両方の場合に便利な関数isEmptyisNilを提供します。

ロジック


2番目の部分 (およびそれより少し高い部分 )では、 &&および||代わりに、 bothおよびboth関数を使用しました。 。 また、場所のcomplementについても話しました!

これらの結合された関数は、関数が同じ値の演算を結合するときに非常に機能します。 上記のwasBornInCountrywasNaturalizedおよびisOver18すべて、人物のオブジェクトに適用されます。

しかし、時々 &&を適用する必要があります|| そして! 異なる意味に。 特別な場合のために、Ramdaは関数andorおよびnotを提供します。 私は次のように思う: andor 、およびvalueで動作せbothboth 、、 or bothが関数で動作する。

主に|| デフォルト値を取得するために使用されます。 たとえば、次のように書くことができます。

 const lineWidth = settings.lineWidth || 80 

これは一般的なイディオムであり、ほとんどの場合機能しますが、「偽」を判断するためにJavaScriptロジックに依存しています。 0が有効なパラメーターである場合はどうなりますか? 0は偽の値であるため、行の値は80になります。

上記で学習したisNil関数を使用できますが、 isNilはより論理的なオプションdefaultToがあります。

 const lineWidth = defaultTo(80, settings.lineWidth) 

defaultTodefaultToの2番目の引数をチェックします。 チェックが失敗した場合、受け取った値を返します。そうでない場合は、渡された最初の引数を返します。

条件


実行の流れを制御することは、関数型プログラミングではそれほど重要ではありませんが、必要な場合があります。 最初の部分で説明した反復関数のコレクションは、ループのほとんどの状況を処理しますが、条件は依然として非常に重要です。

ifElse


年を取得して次を返す関数forever21作成しましょう。 しかし、彼女の名前が示すように、21歳から彼はその意味のままです。

 const forever21 = age => age >= 21 ? 21 : age + 1 

条件( age >= 21 )と2番目のブランチ( age + 1 )は両方ともage関数として書くことができることに注意してください。 最初のブランチ( 21 )を定数関数( () => 21 )として書き直すことができます。 これで、 ageを受け入れる(または無視する)3つの関数ができます。

これで、 RamdaのifElse関数を使用できるようになりました 。これは、 if...then..elseまたはそのより短い従兄弟である三項演算子( ?:と同等?:

 const forever21 = age => ifElse(gte(__, 21), () => 21, inc)(age) 

前述したように、比較関数はユニオン関数のようには機能しないため、ここでプレースホルダー( __ )の使用を開始する必要があります。 代わりにlteを使用することもできます。

 const forever21 = age => ifElse(lte(21), () => 21, inc)(age) 

この場合、「21はage以下」と読む必要があります。 読みやすく、わかりにくいので、残りの投稿ではサロゲートバージョンを使用します。

定数


定数関数は、このような状況で非常に役立ちます。 ご想像のとおり、Ramdaはショートカットを提供します。 この場合、略語はalwaysと呼ばれます

 const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age) 

Ramdaは、 T always(true)およびalways(false)略語としてTおよびFも提供します。

アイデンティティ


alwaysDrivingAge別の関数を作成してみましょう。 この関数は、 ageを取得し、その値がgte 16の場合にそれを返しますgte未満の場合、16を返します。これにより、誰でも車を運転するのに十分な年齢のふりをすることができます。

 const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), a => a)(age) 

比較の2番目の分岐( a => a )は、関数型プログラミングのもう1つの典型的なパターンです。 これは「アイデンティティ」として知られています「アイデンティティ関数」という用語の正確な翻訳はわかりません。これを選択してください-およそのトランス )。 つまり、受け取った引数を単に返す関数です。

ご想像のとおり、RamdaはID関数を提供します。

 const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age) 

identityは複数の引数を取ることができますが、常に最初の引数のみを返します。 最初の引数以外の何かを返したい場合、より一般的なnthArg関数があります。 これは、 identityを使用するよりもはるかに一般的ではありません。

「いつ」と「限りない」


論理分岐の1つがIDであるifElse式も典型的なパターンであるため、Ramdaはより短縮されたメソッドを提供します。

この場合のように、2番目のブランチがアイデンティティである場合ifElse代わりにifElse使用できます。

 const alwaysDrivingAge = age => when(lt(__, 16), always(16))(age) 

条件の最初のブランチがIDである場合、 unlessを使用できます。 gte(__, 16)を使用するための条件を逆にすると、 gte(__, 16)を使用できます。

 const alwaysDrivingAge = age => unless(gte(__, 16), always(16))(age) 

条件


Ramdaは、 switchまたはif...then...else式チェーンを置き換えることができるcond関数も提供します。

 const water = temperature => cond([ [equals(0), always('water freezes at 0°C')], [equals(100), always('water boils at 100°C')], [T, temp => `nothing special happens at ${temp}°C`] ])(temperature) 

Ramdaコードでcondを使用する必要はありませんでしたが、何年も前にLispで同様のコードを書いたので、 condは古い友人のように感じます。

おわりに


命令型コードを宣言型関数コードに変換するためにRamdaが提供する一連の関数を調べました。

次へ


お気付きかもしれませんが、私たちが書いた最後のいくつかの関数( forever21drivingAgeおよびwater )はすべてパラメーターを受け取り、新しい関数を作成してから、この関数をパラメーターに適用します。

これは一般的なパターンであり、Ramdaはすべてをきれいな外観にするツールを提供します。 次の投稿「 Pointless Notation 」では、同様のパターンに従う関数を簡素化する方法を検討しています。

Source: https://habr.com/ru/post/J349674/


All Articles