
純粋ではない機能
所要時間:1 分
関数型プログラミングのパラダイムを意識する開発者は日に日に増えている。このパラダイムは、純粋な関数はテストや並列化が容易なため、バグのない効率的なコードを約束する。
実際には、本格的な関数型アプリケーションはまだ抽象的なものである。しかし、関数型プログラミングパラダイムからの特定の概念は、非関数型言語に適用されることが多くなってきている。また、この「部分的に関数的な」アプローチは、多くの一般的な「問題」をより良い方法で解決するのに役立ちます。
今日は、そのような関数概念のひとつである純粋/不純関数について詳しく見ていこう。
注:関数の定義にはKotlinベースの擬似コードを使用する。
純粋機能
その 純粋関数は副作用を持たない関数です。言い換えれば、この関数はパラメータとして渡された値以外の値を取得したり変更したりしてはいけません。このように、同じ引数で関数を呼び出すと、常に同じ出力(戻り値)が得られます。まずは純粋な関数を定義することから始めましょう:
fun max(a: Int, b: Int) {
if (a > b)
return a
else
return b
}
この max関数は2つの引数、2つの Numbersを取り、それらのうち最大のものを返す。この関数は、関数スコープの外からいかなる値にもアクセスしたり変更したりしないことに注意してください。 純粋関数.この関数を引数 2と 7でこの関数を呼び出すと、必ず 7.
このコンセプトをよりよく理解するために、コインの裏側、つまり関数の純粋性を破るさまざまな方法を詳しく見てみよう。
不純な機能
不純な機能 不純関数とは副作用を持つ関数のことで、関数のスコープ外(関数本体の外)から値を変更したりアクセスしたりするものです。
明らかな副作用
最も単純な例は、外部プロパティを変更して状態を保存する関数である。
val loalScore = 0
val getScore(score: Int): Int {
loalScore = score
return loalScore
}この場合、後続のメソッド呼び出しが同じ値を返すため、不純物が強く現れることはない:
getScore(12) // returns 12
getScore(6) // returns 6
getScore(3) // returns 3この関数は外部値を変更するが、値を代入しているため各呼び出しで同じ値を返す。しかし、これは常にそうであるとは限りません。別の不純な関数を考えてみよう:
val addScore = 0
val addScore(score: Int): Int {
loalScore + score
return loalScore
}この関数は外部の値を変更し、呼び出すたびに異なる結果を返すため、「より強い不純物」を持っている:
addScore(12) // returns 12
addScore(6) // returns 18
addScore(3) // returns 21状態を維持することの欠点は、テストがより複雑になることである。
次の関数は、関数スコープの外から値にアクセスしている:
fun getString(length: Int): String {
return Random().nextString(length)
}今回、同じ引数で関数を呼び出すと、それぞれ異なる値が返される:
getString(2) // returns "ab"
getString(2) // returns "hh"
getString(2) // returns "zk"先に紹介した副作用はかなり見つけやすいが、副作用はもっと微妙なことも多い:
あまり目立たない副作用
興味深い副作用のシナリオは、関数の引数として渡されたオブジェクトの変更である:
fun increaseHeight(person: Person) {
person.height++
}この関数を同じ Person同じインスタンスでこの関数を複数回呼び出すと、関数の外側の値(Personインスタンスに格納されている値)が変更されるため、出力が異なります。
関数がスローする例外は、発見しにくい副作用の好例である:
fun addDistance (a:Int, b:Int): Int {
if(a < 0) {
throw IllegalAccessException("a must be >= 0")
}
return a + b
}
副作用を作り出すもうひとつの興味深い方法は、単に別の副作用関数を呼び出すことである:
fun firstFunction() {
addDistance(-5, 7)
}
fun addDistance (a:Int, b:Int): Int {
if(a < 0) {
throw IllegalAccessException("a must be >= 0")
}
return a + b
}
もうひとつ、あまり知られていない副作用として、ロギングがある。実際のサンプルを見てみましょう。 ビデオチャットのサンプルを見てみましょう:
private PublisherKit.PublisherListener publisherListener = new PublisherKit.PublisherListener() {
@Override
public void onStreamCreated(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream " + stream.getStreamId());
}
@Override
public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamDestroyed: Publisher Stream Destroyed. Own stream " + stream.getStreamId());
}
@Override
public void onError(PublisherKit publisherKit, OpentokError opentokError) {
Log.d(TAG, "PublisherKit onError: " + opentokError.getMessage());
}
};上記のコードでは、副作用としてのロギングはアプリケーション・ロジックに影響を与えませんが、アプリケーションで何が起こっているかを理解するのに役立ちます。後で、開発者がコールバックによって返されたデータを使用し、より多くの副作用を導入するのは簡単です。
機能がPureImpureかどうかを判断する
関数が不純であることを示す手がかりは2つある。最初のケースを見てみよう:
list.getItem(): String上の例では、関数はパラメータを取らず、値を返している。これは、おそらくその値がクラスの状態から取得されたものであることを意味します。関数が値を返さない場合について考えてみましょう:
list.setItem("item")関数名を見れば、paramがクラスの状態を変更するために使われる可能性が高いことがわかる。
最後に、引数も返される値もないコンボがある:
list.sort()
これらは手がかりに過ぎない。常にそうであるとは限らないが、これらの手がかりはしばしば良い純度の指標となる。
概要
関数型プログラミングのパラダイムでは、理想的にはすべての関数が純粋である。
しかし、実世界の多くのアプリケーションでは、物事はそれほど二元的ではありません。特に、アプリケーションが永続性、ユーザー入力、ネットワーク・データ・アクセスのような外部リソースを必要とする場合、不純な関数を避けられないことがあります。これらを持つことは、関数とアプリケーション全体の純粋さを壊しますが、それは悪いことではありません。
通常、1つのアプリケーションには、純粋な関数と不純な関数が混在しています。アプリケーションのテストを容易にし、バグを回避するのに役立つので、純粋/不純を意識することは良い習慣です。