あなたは関数型プログラマになりたい(パート5)

関数型プログラミングの概念を理解するための最初の一歩を踏み出すことは、最も重要で、時には最も困難な手順です。しかし、そうである必要はありません。正しい視点ではありません。

前のパート:パート1、パート2、パート3、パート4

参照の透明性

参照透過性は、純粋な関数をその式で安全に置き換えることができることを説明するための凝った用語です。例はこれを説明するのに役立ちます。

代数では、次の式があったとき:

y = x + 10

そして言われた:

x = 3

xを方程式に代入して、次を取得できます。

y = 3 + 10

方程式はまだ有効であることに注意してください。同じ種類の置換を純粋な関数で行うことができます。

指定された文字列を単一引用符で囲むElmの関数は次のとおりです。

引用str =
    "'" ++ str ++ "'"

そして、それを使用するコードは次のとおりです。

findErrorキー=
    「見つかりません」++(引用キー)

ここで、findErrorは、キーの検索が失敗したときにエラーメッセージを作成します。

quote関数は純粋なので、findErrorの関数呼び出しをquote関数の本体(単なる式)に置き換えることができます:

findErrorキー=
   "「++(" '"++ str ++"' ")が見つかりません」

これは、私が逆リファクタリングと呼んでいるものです(これは私にとってより理にかなっています)。プログラマやプログラム(コンパイラやテストプログラムなど)がコードについて推論するために使用できるプロセスです。

これは、再帰関数について推論するときに特に役立ちます。

実行順序

ほとんどのプログラムはシングルスレッドです。つまり、一度に実行されるコードは1つだけです。マルチスレッドプログラムを使用している場合でも、ほとんどのスレッドはブロックされ、I / Oが完了するのを待っています。ファイル、ネットワークなど

これが、コードを記述するときに順序付けられたステップの観点から自然に考える理由の1つです。

1.パンを取り出す
2.トースターに2つのスライスを入れます
3.暗さを選択
4.レバーを押し下げます
5.トーストがポップアップするのを待ちます
6.トーストを取り除く
7.バターを出す
8.バターナイフを手に入れる
9.バタートースト

この例では、バターの取得とパンのトーストの2つの独立した操作があります。手順9でのみ相互依存関係になります。

ステップ7と8は互いに独立しているため、ステップ1〜6と同時に実行できます。

しかし、これを行うと、事態は複雑になります。

スレッド1
--------
1.パンを取り出す
2.トースターに2つのスライスを入れます
3.暗さを選択
4.レバーを押し下げます
5.トーストがポップアップするのを待ちます
6.トーストを取り除く
スレッド2
--------
1.バターを出す
2.バターナイフを手に入れる
3.スレッド1が完了するのを待ちます
4.バタートースト

スレッド1が失敗すると、スレッド2はどうなりますか?両方のスレッドを調整するメカニズムは何ですか?トーストの所有者:スレッド1、スレッド2、またはその両方?

これらの複雑さについて考えず、プログラムをシングルスレッドのままにしておく方が簡単です。

しかし、プログラムのすべての可能な効率を絞り出す価値がある場合、マルチスレッドソフトウェアを作成するために途方もない努力をしなければなりません。

ただし、マルチスレッドには2つの主な問題があります。まず、マルチスレッドプログラムは、記述、読み取り、理由付け、テスト、デバッグが困難です。

第二に、いくつかの言語、例えばJavascriptはマルチスレッドをサポートしておらず、サポートしているものはそれをひどくサポートしています。

しかし、順序が重要でなく、すべてが並行して実行された場合はどうなりますか?

これはおかしく聞こえますが、見た目ほど混oticではありません。これを説明するために、いくつかのElmコードを見てみましょう。

buildMessageメッセージ値=
    させる
        upperMessage =
            String.toUpperメッセージ
        quotedValue =
            "'" ++値++ "'"
    に
        upperMessage ++ ":" ++ quotedValue

ここで、buildMessageはメッセージと値を受け取り、大文字のメッセージ、コロン、および単一引用符で囲まれた値を生成します。

upperMessageとquotedValueが独立していることに注目してください。これをどうやって知るのですか?

独立のために真実でなければならない2つのことがあります。まず、それらは純粋な関数でなければなりません。他の実行の影響を受けてはならないため、これは重要です。

それらが純粋でない場合、それらが独立していることを知ることはできません。その場合、実行順序を決定するには、プログラムで呼び出された順序に依存する必要があります。これが、すべての命令型言語の仕組みです。

独立性のために真でなければならない2番目のことは、1つの関数の出力が他の関数の入力として使用されないことです。この場合、1つが終了するのを待ってから2つ目を開始する必要があります。

この場合、upperMessageとquotedValueは両方とも純粋であり、どちらも他方の出力を必要としません。

したがって、これら2つの関数は任意の順序で実行できます。

コンパイラーは、プログラマーの助けなしにこの決定を行うことができます。これは、純粋な関数型言語でのみ可能です。なぜなら、副作用の影響を判断することは不可能ではないにしても、非常に難しいからです。

純粋な関数型言語での実行順序は、コンパイラーによって決定できます。

これは、CPUが高速にならないことを考えると非常に有利です。代わりに、製造業者はより多くのコアを追加しています。これは、コードがハードウェアレベルで並列に実行できることを意味します。

残念ながら、命令型言語では、非常に粗いレベルを除き、これらのコアを最大限に活用することはできません。ただし、そのためには、プログラムのアーキテクチャを大幅に変更する必要があります。

Pure Functional Languagesを使用すると、1行のコードを変更することなく、きめ細かいレベルでCPUコアを自動的に活用できる可能性があります。

タイプ注釈

静的型付け言語では、型はインラインで定義されます。以下に、いくつかのJavaコードを示します。

public static String quote(String str){
    return "'" + str + "'";
}

入力が関数定義とどのようにインラインになっているかに注目してください。ジェネリックがあるとさらに悪化します。

private final Map  getPerson(Map  people、Integer personId){
   // ...
}

目立つように型を太字にしましたが、それでも関数の定義に干渉します。変数の名前を見つけるには、注意深く読む必要があります。

動的に型付けされた言語では、これは問題ではありません。 Javascriptでは、次のようなコードを記述できます。

var getPerson = function(people、personId){
    // ...
};

厄介な型情報のすべてが邪魔されることなく、これは非常に読みやすくなっています。唯一の問題は、タイピングの安全性を放棄することです。これらのパラメーターを簡単に逆に渡すことができます。つまり、人の場合はNumber、personIdの場合はObjectです。

プログラムが実行されるまでわからないので、実稼働に移行してから数か月かかる可能性があります。 Javaではコンパイルされないため、これは当てはまりません。

しかし、両方の長所を活用できたらどうでしょう。 Javascriptの構文のシンプルさとJavaの安全性。

できることがわかりました。以下に、タイプ注釈付きのElmの関数を示します。

追加:Int-> Int-> Int
x y =を追加
    x + y

タイプ情報が別の行にあることに注意してください。この分離は世界に違いをもたらします。

ここで、型注釈にはタイプミスがあると思うかもしれません。私はそれを最初に見たときにしたことを知っています。最初の->はコンマにするべきだと思いました。しかし、タイプミスはありません。

暗黙の括弧でそれを見るとき、それはもう少し理にかなっています:

追加:Int->(Int-> Int)

つまり、addは、Int型の単一のパラメーターを受け取り、単一のパラメーターIntを受け取り、Intを返す関数を返す関数です。

暗黙の括弧が示された別のタイプアノテーションを次に示します。

doSomething:文字列->(Int->(文字列->文字列))
doSomethingプレフィックス値suffix =
    プレフィックス++(toString値)++サフィックス

これは、doSomethingがString型の単一のパラメーターを受け取り、Int型の単一のパラメーターを受け取り、String型の単一のパラメーターを受け取り、Stringを返す関数を返す関数であることを示しています。

すべてが単一のパラメーターを取る方法に注意してください。これは、すべての機能がElmでカリー化されているためです。

括弧は常に右側にあるため、必要ありません。したがって、次のように書くことができます。

doSomething:文字列-> Int->文字列->文字列

関数をパラメーターとして渡す場合、括弧が必要です。それらがなければ、型注釈はあいまいになります。例えば:

takes2Params:Int-> Int-> String
takes2Params num1 num2 =
    -何かをする

とは非常に異なります:

takes1Param:(Int-> Int)-> String
takes1Param f =
    -何かをする

takes2Paramは、Intと別のIntの2つのパラメーターを必要とする関数です。一方、takes1Paramでは、Intと別のIntを受け取る関数に1つのパラメーターが必要です。

マップのタイプアノテーションは次のとおりです。

map:(a-> b)->リストa->リストb
マップfリスト=
    // ...

ここで、fは型(a-> b)であるため、括弧が必要です。つまり、a型の単一のパラメーターを取り、b型の何かを返す関数です。

ここで、タイプaは任意のタイプです。タイプが大文字の場合、それは明示的なタイプです。文字列。タイプが小文字の場合、どのタイプでもかまいません。ここで、aはStringにできますが、Intにもできます。

(a-> a)が表示される場合、入力タイプと出力タイプは同じでなければならないということです。それらが何であるかは関係ありませんが、一致しなければなりません。

しかし、mapの場合、(a-> b)があります。つまり、異なる型を返すことができますが、同じ型を返すこともできます。

ただし、aの型が決定されると、aは署名全体に対してその型でなければなりません。たとえば、aがIntでbがStringの場合、署名は次と同等です。

(Int-> String)-> List Int-> List String

ここでは、aのすべてがIntに置き換えられ、bのすべてがStringに置き換えられています。

List IntタイプはリストにIntsが含まれることを意味し、List Stringはリストにストリングが含まれることを意味します。 Javaや他の言語でジェネリックを使用している場合、この概念はおなじみのはずです。

私の脳!!!!

今のところ十分です。

この記事の最後の部分で、日々の仕事で学んだこと、つまりFunctional JavascriptとElmをどのように使用できるかについて説明します。

次:パート6

これが気に入ったら、下のをクリックして、他の人がこれを中に表示するようにします。

ElmのFunctional Programmingを使用してWebアプリの開発を学び、支援するWeb開発者のコ​​ミュニティに参加したい場合は、FacebookグループのLearn Elm Programming https://www.facebook.com/groups/learnelm/をご覧ください。

私のTwitter:@cscalfani