「機能的思考」の
第1部と
第2部では、関数型プログラミングの問題のいくつかと、それらがJavaとその関連言語にどのように関係しているかを調べました。 このパートではレビューを続けます。Scala言語の前のパートの番号の分類子のバージョンを示し、
カリー化 、
部分適用 、
再帰などの理論的な問題について説明し
ます 。
Scalaの数字の分類子
Scalaバージョンは、少なくともJavaプログラマー向けに最小限の神秘的な構文が含まれているため、最後に保存しました。 (分類子の要件を思い出してください:
完全 、
過剰、または
不十分として分類する必要があります。完全数とは、除数としての自分を除く除数が合計で彼に等しい数です。同様に、超過数の除数の合計は自分よりも高く不十分-少ない。)
リスト1. Scalaでの数値の分類子package com.nealford.conf.ft.numberclassifier object NumberClassifier { def isFactor(number: Int, potentialFactor: Int) = number % potentialFactor == 0 def factors(number: Int) = (1 to number) filter (number % _ == 0) def sum(factors: Seq[Int]) = factors.foldLeft(0)(_ + _) def isPerfect(number: Int) = sum(factors(number)) - number == number def isAbundant(number: Int) = sum(factors(number)) - number > number def isDeficient(number: Int) = sum(factors(number)) - number < number }
この時点までにScalaを見たことがない場合でも、このコードは読みやすいはずです。 前と同じように、私たちにとって興味深い2つの方法は、
factors()
と
sum()
です。
factors()
メソッドは、1から指定された数値までの数値のリストを取得し、標準のScala
filter()
ライブラリメソッドを適用し、右側のコードブロックをフィルター基準(述語とも呼ばれます)として使用します。 特別な構造がコードブロックで使用されます-
暗黙的なパラメーター *を使用すると、不要な場合にパラメーター名の代わりに
_
記号を使用できます。 Scala構文の柔軟性により、演算子と同じように
filter()
メソッドを呼び出すことができます。 構築
(1 to number).filter((number % _ == 0))
は、より便利だと思われる場合にも機能します。
sum
メソッドは、おなじみの
折りたたみ左操作(Scalaで
foldLeft()
メソッドとして実装)を使用します。 この場合、パラメーターの名前は必要ないので、名前の代わりに
_
記号を使用します。これにより、コードブロックの構文がよりシンプルで明確になります。
foldLeft()
メソッドは、最初のパートに登場したFunctional Javaライブラリーの同じ名前のメソッドと同じことを行います。
- 初期値を取得し、指定された操作を使用して、リストの最初の要素と組み合わせます。
- 結果を取得し、同じ操作をリストの次の要素に適用します。
- リストが終了するまでこれを続けます。
これは、数値のリストへの追加などの操作のアプリケーションの汎用バージョンです。0から開始し、最初の要素を追加し、結果を取得して2番目の要素を追加し、リストの最後まで続行します。
単体テスト
前のバージョンでは単体テストを表示していませんでしたが、すべての例がテストされています。 Scalaには、ScalaTestという便利な単体テストライブラリがあります。 リスト2は、リスト1の
isPerfect()
メソッドをテストするために最初に作成したテストを示しています。
リスト2. Scalaでの数値の分類子の単体テスト @Test def negative_perfection() { for (i <- 1 until 10000) if (Set(6, 28, 496, 8128).contains(i)) assertTrue(NumberClassifier.isPerfect(i)) else assertFalse(NumberClassifier.isPerfect(i)) }
しかし、あなたと同じように、私はもっと機能的に考えることを学ぼうとしているので、2つの理由から2つのコードを好まないのです。 最初に、彼は反復を自分で実行します。これは、問題を解決するための必須のアプローチを示しています。 第二に、
if
2番目のブランチは私にとって本当に重要ではありません。 どのような問題を解決しようとしていますか? 私の分類器は不完全な数を完全なものとして定義しないことを確信しなければなりません。 リスト3は、この問題の解決策を示していますが、表現が少し異なります。
リスト3. Scalaでの数値の分類子の代替単体テスト @Test def alternate_perfection() { assertEquals(List(6, 28, 496, 8128), (1 until 10000) filter (NumberClassifier.isPerfect(_))) }
リスト3は、完全と見なされる1〜100 00の数字のリストと既知の完全な数字のリストが等しいことを確認します。 機能的思考は、プログラミングのアプローチだけでなく、コードのテスト方法も拡張します。
部分使用とカレー
上記で示したリストをフィルタリングするための機能的なアプローチは、機能的な言語とライブラリの間で非常に一般的です。 パラメーターとして渡すコードを(リスト3の
filter()
メソッドで)使用すると、再利用のための「異なる」アプローチが示されます。 デザインパターンによって定義された古いオブジェクト指向の世界から来た場合は、このアプローチを本Design Patterns
**のパターン手法メソッドと比較してください。 継承者が作成されるまで詳細の定義を延期するために抽象メソッドを使用して、基本クラスでアルゴリズムの準備を定義します。 機能的なアプローチにより、操作をパラメーターとして渡し、メソッド内で適切に適用できます。
再利用を実現する別の方法は、
カリー化です。 数学者のHaskell Curry(Haskellプログラミング言語も彼の名誉にちなんで名付けられました)にちなんで名付けられた、カリー化は、多くのパラメーターの関数を1つのパラメーターの関数のチェーンに変換することです。 これは、部分適用と密接に関連しています。これは、関数の1つ以上の引数の値を修正して、アリティの低い新しい関数を作成する手法です。 それらの違いを理解するために、カリー化を説明するリスト4のコードを見てみましょう。
リスト4. Groovyでの実行 def product = { x, y -> return x * y } def quadrate = product.curry(4) def octate = product.curry(8) println "4x4: ${quadrate.call(4)}" println "5x8: ${octate(5)}
リスト4では、2つのパラメーターを取るコードのブロックとして
product
を定義しました。 組み込みのgroovy
curry()
メソッドを使用して、
product
を基本要素として使用して、2つの新しいコードブロック
quadrate
と
octate
を作成します。 Groovyを使用すると、コードの呼び出しが非常に簡単になります。call()メソッドを明示的に呼び出すか、言語が提供する構文シュガー(括弧のペア)を使用できます。
部分適用は、カリー化を繰り返すより一般的な手法ですが、結果の関数を引数の数に制限しません。 Groovyは、リスト5に示すように、
curry()
メソッドを使用して両方の手法を実装します。
リスト5. Groovyでのカリー方式を使用した部分使用とカリー化の比較 def volume = { h, w, l -> return h * w * l } def area = volume.curry(1) def lengthPA = volume.curry(1, 1)
リスト5のボリュームコードブロックは、既知の式を使用してボックスのボリュームを計算します。 最初の引数
volume
を1に固定して、
area
コードブロック(四角形の面積を計算)を作成します。ラインの長さを返すブロックのベースとしてボリュームを使用するには、部分適用とカリー化の両方を使用できます。
lengthPA
は、最初の2つのパラメーターを1に固定することにより、部分的なアプリケーションを使用します
lengthC
は、同じ結果を得るためにカレーを2回適用します。 違いは非常に微妙であり、結果は同じですが、カリー化と部分的使用という用語を実際の機能プログラマーの同義語として使用すると、おそらくエラー
***が通知されます。
関数型プログラミングは、命令型言語と同じ目標を達成するための新しいツールを提供します。 これらのツールの関係はよく理解されています。 先ほど、コンポジションを再利用ツールとして使用する例を示しました。 そのため、カレーと作曲を組み合わせる可能性に驚かないでください。 リスト6のGroovyコードを見てください。
リスト6.構成と部分的なアプリケーション def composite = { f, g, x -> return f(g(x)) } def thirtyTwoer = composite.curry(quadrate, octate) println "composition of curried functions yields ${thirtyTwoer(2)}"
リスト6では、2つの関数を組み合わせた
composite
コードブロックを作成します。 このブロックでは、2つのメソッドを一緒に接続するために、部分的なアプリケーションを使用して
thirtyTwoer
を作成します。
部分的な使用とカリー化により、Pattern Methodパターンなどのメカニズムに類似した結果を達成できます。 たとえば、リスト7に示すように、
adder
ブロックを構築するだけで、
incrementer
コードブロックを作成できます。
リスト7.フィーチャーの構築 def adder = { x, y -> return x + y } def incrementer = adder.curry(1) println "increment 7: ${incrementer(7)}"
リスト8に示すドキュメントの例に示すように、Scalaは自然にカリー化をサポートしています。
リスト8. Scalaでの実行 object CurryTest extends Application { def filter(xs: List[Int], p: Int => Boolean): List[Int] = if (xs.isEmpty) xs else if (p(xs.head)) xs.head :: filter(xs.tail, p) else filter(xs.tail, p) def dividesBy(n: Int)(x: Int) = ((x % n) == 0) val nums = List(1, 2, 3, 4, 5, 6, 7, 8) println(filter(nums, dividesBy(2))) println(filter(nums, dividesBy(3))) }
リスト8のコードは、
dividesBy()
メソッドを使用して、
dividesBy()
メソッドを実装する方法を示してい
filter()
。 匿名関数を
filter()
メソッドに渡し、カリー化を使用して最初の
dividesBy()
パラメーターをこのブロックの作成に使用される値に修正します。
dividesBy()
メソッドに数値を渡すと、Scalaはカリー化を使用して新しい関数を自動的に作成します。
再帰フィルタリング
関数型プログラミングに関連付けられることが多いもう1つの概念は再帰です。これは(Wikipediaによれば)「自己類似の方法で何かを繰り返すプロセス」です。 コンピューターサイエンスでは、これはアクションを繰り返す方法であり、それ自体からメソッドを呼び出します(最初に、この一連の呼び出しを完了できることを確認します)。 多くの場合、再帰は、オブジェクトの縮小リストで同じ操作を実行することに基づいているため、理解しやすいコードにつながります。
リストのフィルタリングを見てください。 反復的なアプローチでは、フィルター条件を取得し、ループ内のループで見たくない要素を破棄します。 リスト9は、Groovyでの単純なフィルター実装を示しています。
リスト9. Groovyでのフィルター処理 def filter(list, criteria) { def new_list = [] list.each { i -> if (criteria(i)) new_list << i } return new_list } modBy2 = { n -> n % 2 == 0 } l = filter(1..20, modBy2) println l
リスト9の
filter()
メソッドは、リストと選択基準(コードブロック)を入力として受け取ります。 内部では、条件を満たしている場合、新しいリストに別の要素を追加してリスト内を移動します。
リスト8に戻りましょう。これは、Scalaでのフィルターの再帰的な実装です。 リストを操作するための関数型言語の標準パターンに従います。 リストは、先頭の値と他の要素のリストという2つの要素の組み合わせと見なされます。 多くの関数型言語には、このイディオムを使用してリストを走査するための特別な方法があります。
filter()
メソッドは最初にリストが空かどうかをチェックします-これは再帰を終了するための必要条件です。 リストが空の場合は、単にメソッドを終了し、そうでない場合は、パラメーターとして渡された条件の値を計算します。 この値が満たされた場合(つまり、出力リストでこの要素を確認したい場合)、この要素と残りの要素のフィルターされたリストで構成される新しいリストを返します。 条件が満たされない場合、残りの要素(ヘッドを除く)のフィルターされたリストを返します。 Scala言語リスト演算子は、両方のケースを読みやすく、簡単に認識できるようにします。
現在、原則として再帰を使用していないようです-再帰はツールボックスに含まれていません。 しかし、これは主に命令型言語がそれを弱くサポートし、その使用を本来よりも困難にしているという事実によるものです。 関数型言語は、単純な構文と再帰のサポートを追加することにより、コードを再利用する別の簡単な方法になります。
おわりに
このパートでは、機能的思考の世界についての私のレビューを締めくくります。 偶然にも、この記事のほとんどはフィルタリングに関するものであり、それを使用して実装する方法を数多く示しています。 しかし、これは予想されることです。 多くの機能的パラダイムはリストを中心に構築されます。これは、プログラミングがオブジェクトのリストの処理を伴うことが多いためです。 リストを操作するための強力なツールを使用して言語とライブラリを作成することは論理的です。
次のパートでは、関数型プログラミングの基本概念の1つである不変性について詳しく説明します。
注釈
* -著者がここで間違えたようです。 クロージャ内の関連する変数の名前ではなく、
_
構成体は、
暗黙的なパラメータではなく、匿名関数のプレースホルダ構文と呼ばれ
ます 。 暗黙的なパラメーターは、関数シグネチャの
implicit
キーワードによって導入され、完全に異なるセマンティクスを持ちます。
** -ロシア語の翻訳は正式には「オブジェクト指向設計のトリック」と呼ばれていました。 この本を認識している人はほとんどいないので、テキストでは使用しませんでした。
*** -このセクションの例は少しわかりにくいように見えます。 セクションの冒頭でカリー化と部分適用の定義を確認し、これらの例をあまり真剣に受け止めないことをお勧めします。
PS翻訳を手伝ってくれて、このシリーズをやろうというアイデアをありがとう