次の方法で共有


F の関数型プログラミングの概念の概要#

関数型プログラミングは、関数と不変データの使用を強調するプログラミングのスタイルです。 型指定された関数型プログラミングは、関数型プログラミングが F# などの静的型と組み合わされている場合です。 一般に、関数型プログラミングでは次の概念が強調されています。

  • 使用するプライマリ コンストラクトとしての関数
  • ステートメントではなく式
  • 変数に対する変更できない値
  • 命令型プログラミングに対する宣言型プログラミング

このシリーズでは、F# を使用した関数型プログラミングの概念とパターンについて説明します。 その過程で、いくつかの F# も学習します。

用語

他のプログラミング パラダイムと同様に、関数型プログラミングには、最終的に学習する必要があるボキャブラリが付属しています。 常に表示される一般的な用語を次に示します。

  • 関数 - 関数は、入力が与えられたときに出力を生成するコンストラクトです。 より正式には、あるセットから別のセットにアイテムを マップ します。 この形式主義は、特にデータのコレクションに対して動作する関数を使用する場合に、多くの点で具象に持ち上げらされます。 これは、関数型プログラミングにおける最も基本的な (および重要な) 概念です。
  • - 式は、値を生成するコード内のコンストラクトです。 F# では、この値をバインドするか、明示的に無視する必要があります。 式は、関数呼び出しで簡単に置き換えることができます。
  • 純度 - 純度は、戻り値が同じ引数に対して常に同じであり、その評価に副作用がないことを示す関数のプロパティです。 純粋関数は、その引数に完全に依存します。
  • 参照透過性 - 参照透過性は、プログラムの動作に影響を与えずに出力に置き換えることができる式のプロパティです。
  • 不変性 - 不変性は、値をインプレースで変更できないことを意味します。 これは、変数とは対照的であり、その場で変更される可能性があります。

例示

次の例では、これらの主要な概念を示します。

機能

関数型プログラミングで最も一般的で基本的なコンストラクトは関数です。 整数に 1 を追加する単純な関数を次に示します。

let addOne x = x + 1

その型シグネチャは次のとおりです。

val addOne: x:int -> int

署名は、"addOnex という名前のintを受け入れ、intを生成します" と読み取ることができます。 より正式には、 addOne は整数のセットから整数のセットに値を マッピング します。 -> トークンは、このマッピングを示します。 F# では、通常、関数シグネチャを見て、それが何をするのかを理解できます。

では、署名が重要なのはなぜですか? 型指定関数型プログラミングでは、関数の実装は、多くの場合、実際の型シグネチャよりも重要ではありません。 addOneが整数に値 1 を加算するのは実行時に興味深いですが、プログラムを構築するときに、intを受け入れて返すという事実は、この関数をどのように実際に使用するかを知らせるものです。 さらに、(型シグネチャに関して) この関数を正しく使用すると、問題の診断は addOne 関数の本体内でのみ実行できます。 これは、型指定された関数型プログラミングの背後にある原動力です。

式は、値に評価されるコンストラクトです。 アクションを実行するステートメントとは対照的に、式は値を返すアクションを実行すると考えることができます。 式は、ほとんどの場合、ステートメントではなく関数型プログラミングで使用されます。

前の関数 addOne考えてみましょう。 addOneの本体は式です。

// 'x + 1' is an expression!
let addOne x = x + 1

addOne関数の結果の型を定義するのは、この式の結果です。 たとえば、この関数を構成する式は、 stringなどの別の型に変更できます。

let addOne x = x.ToString() + "1"

関数のシグネチャは次のようになります。

val addOne: x:'a -> string

F# のどの型でも ToString() 呼び出すことができるため、 x の型はジェネリック ( 自動一般化と呼ばれます) にされ、結果の型は stringです。

式は関数の本体だけではありません。 他の場所で使用する値を生成する式を使用できます。 一般的なものは ifです。

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

if式は、resultという値を生成します。 resultを完全に省略して、if式をaddOneIfOdd関数の本体にすることもできます。 式について覚えておくべき重要な点は、式が値を生成することです。

戻り値が何もない場合に使用される特殊な型 ( unit) があります。 たとえば、次の単純な関数を考えてみましょう。

let printString (str: string) =
    printfn $"String is: {str}"

署名は次のようになります。

val printString: str:string -> unit

unit型は、返される実際の値がないことを示します。 これは、その作業の結果として返す値がないにもかかわらず、"作業を行う" 必要があるルーチンがある場合に便利です。

これは命令型プログラミングとは対照的で、同等の if コンストラクトはステートメントであり、値の生成は多くの場合、変数の変更で行われます。 たとえば、C# では、コードは次のように記述される場合があります。

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

C# やその他の C スタイルの言語では、式ベースの条件付きプログラミングを可能にする 三項式がサポートされていることに注目してください。

関数型プログラミングでは、ステートメントを使用して値を変更することはまれです。 一部の関数型言語ではステートメントと変更がサポートされていますが、関数型プログラミングでこれらの概念を使用することは一般的ではありません。

純粋関数

前述のように、純粋関数は次のような関数です。

  • 常に同じ入力に対して同じ値に評価されます。
  • 副作用はありません。

このコンテキストで数学関数を考えると役に立ちます。 数学では、関数は引数のみに依存し、副作用はありません。 数学関数 f(x) = x + 1では、 f(x) の値は xの値にのみ依存します。 関数型プログラミングの純粋関数も同じです。

純粋関数を記述する場合、関数は引数のみに依存し、副作用につながるアクションを実行しないようにする必要があります。

グローバルで変更可能な状態に依存するため、非純粋関数の例を次に示します。

let mutable value = 1

let addOneToValue x = x + value

addOneToValue関数は、valueが 1 とは異なる値を持つよういつでも変更できるため、明らかに不純です。 グローバル値に依存するこのパターンは、関数型プログラミングでは避ける必要があります。

非純粋関数のもう 1 つの例を次に示します。これは副作用を実行するためです。

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

この関数はグローバル値に依存しませんが、 x の値をプログラムの出力に書き込みます。 これを行うことに本質的に間違いはありませんが、関数が純粋ではないことを意味します。 プログラムの別の部分が出力バッファーなどのプログラムの外部に依存している場合、この関数を呼び出すと、プログラムの他の部分に影響を与える可能性があります。

printfn ステートメントを削除すると、関数は純粋になります。

let addOneToValue x = x + 1

この関数は、printfnステートメントを使用して以前のバージョンよりも本質的に優れているわけではありませんが、この関数が実行するすべての関数が値を返すという保証をします。 この関数を何度でも呼び出すと、同じ結果が生成されます。値が生成されるだけです。 純度によって与えられる予測可能性は、多くの関数型プログラマが目指すものです。

不変

最後に、型指定された関数型プログラミングの最も基本的な概念の 1 つは不変性です。 F# では、既定ではすべての値が変更できません。 つまり、明示的に変更可能としてマークしない限り、インプレースで変更することはできません。

実際には、不変の値を使用するということは、プログラミングへのアプローチを"何かを変更する必要がある"から「新しい値を生成する必要がある」に変更することを意味します。

たとえば、値に 1 を追加すると、既存の値は変更されず、新しい値が生成されます。

let value = 1
let secondValue = value + 1

F# では、次のコードはvalue関数を変更しません。代わりに、等値チェックを実行します。

let value = 1
value = value + 1 // Produces a 'bool' value!

関数型プログラミング言語の中には、変更をまったくサポートしていないものもあります。 F# ではサポートされていますが、値の既定の動作ではありません。

この概念は、データ構造までさらに拡張されます。 関数型プログラミングでは、セットなどの不変のデータ構造 (およびその他の多く) には、最初に想定した実装とは異なる実装があります。 概念的には、セットに項目を追加してもセットは変更されず、追加された値を持つ 新しい セットが生成されます。 多くの場合、これは、データの適切な表現を結果として得ることができるように、値を効率的に追跡できる別のデータ構造によって実現されます。

値とデータ構造を操作するこのスタイルは非常に重要です。これは、新しいバージョンを作成するかのように何かを変更する操作を強制的に処理するためです。 これにより、プログラムで等価性や比較可能性などの一貫性を保つことができます。

次のステップ

次のセクションでは、関数について十分に説明し、関数型プログラミングで使用できるさまざまな方法について説明します。

F# で関数を使用すると 、関数を詳しく調べ、さまざまなコンテキストで関数を使用する方法を示します。

詳細については、次を参照してください。

関数型思考シリーズは、F# を使用した関数型プログラミングについて学習するためのもう 1 つの優れたリソースです。 F# 機能を使用して概念を説明する、実用的で読みやすい方法での関数型プログラミングの基礎について説明します。