ソフトウェア概論

自学用メモ。

関数の副作用

関数や手続きがシステムの状態を変える現象を指す。また、システム外部とのやり取りも含む。 具体的な例:

  • グローバル変数の変更:関数内でグローバル変数の値を変える。
  • ファイル操作:ファイルの読み込みや保存。
  • データベース操作:データの追加、更新、削除など。
  • 画面出力:ユーザーインターフェースへの情報の表示。

純粋関数と副作用

純粋関数:与えられた入力に基づいて結果を返すだけの関数。外部の状態に影響を与えない。この性質のため、テストがしやすく、再利用性が高い。また、動作が予測しやすい。

副作用の利点・欠点:

  • 利点:実際のアプリケーションでは、データの永続化やユーザーとのインタラクションなど、副作用が必要な操作が多い。これにより、具体的なタスクを実行する。
  • 欠点:副作用を持つ関数は、他の部分との依存関係が強くなることがある。そのため、テストやデバッグが難しくなることがある。また、不具合の原因となることも。

副作用のある関数のテスト方法

  • Mock:副作用を持つ部分(例:データベースや外部API)を模倣する。これにより、テスト中に実際の副作用を発生させずに関数の動作を検証できる。モックを使うと、特定の戻り値やエラーを模倣して、関数の反応をテストすることができる。

  • Stub:関数やメソッドの実装を一時的に置き換える。これにより、テスト中の動作を制御できる。スタブを使うと、関数がどのように呼び出されたか、何回呼び出されたかなどの情報を取得できる。

  • Spy:関数やメソッドの呼び出しを監視する。スパイを使うと、関数がどのように、何回、どの引数で呼び出されたかを検証できる。

  • 状態のリセット:テストの前後でシステムの状態を初期化する。これにより、一つのテストが他のテストの結果に影響を与えることを防ぐ。

  • 統合テスト:システムの複数の部分が連携して正しく動作するかを検証する。モックやスタブを使わず、実際の環境での動作を確認する。

値渡しと参照渡しの違いとその影響(Go言語の例)

プログラミングにおいて、関数やメソッドに変数を渡す方法として、値渡しと参照渡しがある。これらの方法は、関数内での変数の扱い方や副作用の有無に影響を与える。

1. 値渡し (Pass by Value)

  • 引数として変数のコピーが関数に渡される。
  • 関数内で引数の値を変更しても、関数の外部にある元の変数の値は変わらない。
  • 副作用が発生しづらい。

例:

package main

import "fmt"

func f(x int) {
    x = 10
}

func main() {
    a := 5
    f(a)
    fmt.Println(a)  // 5が出力される
}

structの場合も同様。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func f(p Person) {
    p.Name = "Modified"
    p.Age = 100
}

func main() {
    person := Person{Name: "Original", Age: 20}
    f(person)
    fmt.Println(person)  // {Original 20} が出力される
}

2. 参照渡し (Pass by Reference)

  • 引数として変数のアドレス(ポインタ)が関数に渡される。
  • 関数内で引数を通じて変数の値を変更すると、関数の外部にある元の変数の値も変わる。
  • 副作用が発生する可能性が高まる。

例:

package main

import "fmt"

func f(x *int) {
    *x = 10
}

func main() {
    a := 5
    f(&a)
    fmt.Println(a)  // 10が出力される
}

structの場合も同様に、フィールドの値を変更することができる。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func f(p *Person) {
    p.Name = "Modified"
    p.Age = 100
}

func main() {
    person := Person{Name: "Original", Age: 20}
    f(&person)
    fmt.Println(person)  // {Modified 100} が出力される
}

速度の観点

  1. データの大きさと速度:

    • 基本型(int, float, boolなど)や小さなデータ構造では、値渡しは効率的。
    • しかし、大規模なデータ構造や配列の場合、データのコピーが必要になるため、速度が落ちる可能性がある。この時、ポインタ渡しの方が効率的に動くことが多い。
  2. 関数の呼び出し頻度:

    • 関数が頻繁に呼ばれる場面では、値渡しでのデータのコピーが増える。その結果、パフォーマンスに影響が出るかもしれない。
    • ポインタ渡しを利用することで、このようなオーバーヘッドを減少させ、パフォーマンスを向上させることができる。
  3. メモリのアクセスパターン:

    • ポインタを利用すると、メモリの異なる位置を参照することが増え、キャッシュミスが起きやすくなる。
    • 一方、値渡しは連続したメモリ領域を使うため、キャッシュの効率が上がることがある。
  4. データの変更とサイドエフェクト:

    • ポインタ渡しを利用する場合、関数内でのデータの変更が元のデータに影響する。このため、サイドエフェクトに注意が必要。
    • 値渡しの場合、元のデータは変わらないため、このような心配は不要。

Reference

  1. Goのポインタ渡しは値渡しよりパフォーマンスが良いという誤解 …
  2. トリビア: Goの構造体 int型のフィールド数4つまでなら値渡しと …
  3. Go、ポインタにするか値にするかの方針を考えてみた - Qiita