早期リターン嫌いな男 あの手この手で Bing AI を説得するが…AIのキレキレな返答に「一理ある」

chat

どんな質問にもチャットで答えてくれる Bing AI に、普段人には言えない、早期リターンに対する想いをぶつけてみました。


早期リターン嫌いな男 PHP で、受け取った数値の年がうるう年か判定するプログラムを書いて。

Bing こんにちは、これはBingです。PHPでうるう年を判定するプログラムを書く方法についてお探しですね。☺
ウェブ検索の結果²³⁴⁵によると、PHPにはdateという関数があり、その中に'L'というオプションがあります。このオプションは、指定した年がうるう年なら1を、そうでなければ0を返します。¹
例えば、次のようなコードで1998年がうるう年かどうか判定できます。

function isLeap ($check_year) {
  return (date ('L', mktime (0, 0, 0, 1, 1, $check_year))==1);
}
$check_year=1998;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは¹から引用しました。
この回答はお役に立ちましたか?
ソース: Bing との会話 2023/3/8(1) PHP Program to Check Leap Year – Letsfindcourse. https://letsfindcourse.com/php-coding-questions/php-program-to-check-leap-year アクセス日時 2023/3/8.
(2) PHP program to check leap year – W3schools. https://www.w3schools.blog/php-program-to-check-leap-year アクセス日時 2023/3/8.
(3) PHP Leap Year Program – javatpoint. https://www.javatpoint.com/php-leap-year アクセス日時 2023/3/8.
(4) PHP program to check if a year is leap year or not …. https://www.tutorialspoint.com/php-program-to-check-if-a-year-is-leap-year-or-not アクセス日時 2023/3/8.
(5) PHP Program to Check Leap Year – Letsfindcourse. https://letsfindcourse.com/php-coding-questions/php-program-to-check-leap-year アクセス日時 2023/3/8.

 はい、そんなオプションは知りませんでした。

Bing よかったです。☺
PHPには他にも便利な関数がたくさんあります。もし興味があれば、PHPの公式ドキュメントをご覧ください。

 では、そういった既存の関数やオプションを使わずにプログラミングできますか?

Bing はい、もちろんできます。date関数やオプションを使わないでうるう年を判定するには、数学的なロジックを使って年が4で割り切れるかどうかをチェックします。¹
ただし、これだけでは十分ではありません。グレゴリオ暦では、4で割り切れる年でも100で割り切れる年はうるう年ではありません。ただし、400で割り切れる年は例外としてうるう年になります。¹
例えば、次のようなコードで2000年がうるう年かどうか判定できます。

function isLeap ($check_year) {
  if ($check_year % 4 == 0) {
    if ($check_year % 100 == 0) {
      if ($check_year % 400 == 0) {
        return true;
      } else {
        return false;
      }
    } else {
      return true;
    }
  } else {
    return false;
  }
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは³から参考にしました。
この回答はお役に立ちましたか?
ソース: Bing との会話 2023/3/8(1) PHP – How to check whether a year is bisect (i.e. a leap year)?. https://stackoverflow.com/questions/5680115/php-how-to-check-whether-a-year-is-bisect-i-e-a-leap-year アクセス日時 2023/3/8.
(2) [ PHP ] 閏(うるう)年の判定を行う3通りの実装方法 – 偏差値40 …. https://hensa40.cutegirl.jp/archives/2817 アクセス日時 2023/3/8.
(3) Does php date function automatically factor in leap year. https://stackoverflow.com/questions/21033197/does-php-date-function-automatically-factor-in-leap-year アクセス日時 2023/3/8.

 それだとネストが深いので、リファクタリングしてください。

Bing ネストが深いと読みにくくなるので、リファクタリングするのは良い考えです。☺
次のようにコードを書き換えると、ネストを減らすことができます。

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは、早期リターンというテクニックを使っています。
この回答はお役に立ちましたか?

 早期リターンは構造化プログラミングの原則に反するため、可読性が低いと思います。

Bing 構造化プログラミングの原則についてお話しすると、関数やメソッドは一つの入り口と一つの出口を持つべきだという考え方です。
しかし、この原則は必ずしも絶対的なものではありません。場合によっては、早期リターンを使った方が可読性が高くなることもあります。
例えば、次のようなコードは、早期リターンを使わないで書かれています。

function isLeap ($check_year) {
  $result = false;
  if ($check_year % 4 == 0) {
    $result = true;
    if ($check_year % 100 == 0) {
      $result = false;
      if ($check_year % 400 == 0) {
        $result = true;
      }
    }
  }
  return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは、ネストが深くなり、変数$resultの値が何度も変わるため、読みにくいと感じる人もいるかもしれません。
一方、早期リターンを使ったコードは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードは、ネストが浅くなり、条件に合わない場合はすぐに終了するため、読みやすいと感じる人もいるかもしれません。
どちらのコードが良いかは、個人やチームの好みや規約によって異なります。
私はどちらのコードでも理解できますが、早期リターンを使った方がシンプルでエレガントだと思います。☺
この回答はお役に立ちましたか?

 こうやって書けばいいんじゃないですか?

function isLeap ($check_year) {
  $result = false;
  if ($check_year % 4 == 0) {
    $result = true;
  }
  if ($check_year % 100 == 0) {
    $result = false;
  }
  if ($check_year % 400 == 0) {
    $result = true;
  }
  return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

Bing はい、そのコードでもうるう年の判定はできます。☺
ただし、このコードでは、条件に合致するたびに$resultの値が上書きされるため、無駄な処理が発生します。
例えば、$check_yearが4で割り切れる場合、$resulttrueになりますが、その後100で割り切れるかどうかをチェックします。もし100で割り切れる場合、$resultfalseになりますが、その後400で割り切れるかどうかをチェックします。もし400で割り切れる場合、$resultは再びtrueになります。
このように$resultの値が何度も変わると、コードの流れを追いにくくなったり、バグの原因になったりする可能性があります。
早期リターンを使ったコードでは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードでは、条件に合わない場合はすぐに関数から抜けるため、無駄な処理が発生しません。
例えば、$check_yearが4で割り切れない場合、return falseという文で関数を終了します。その後の処理は実行されません。
このように早期リターンを使った方が効率的で可読性も高いと思います。☺
この回答はお役に立ちましたか?

 確かに一理ありますね。
それでは、次のコードならどうですか?

function isLeap ($check_year) {
    if ($check_year % 400 == 0) {
        $result = true;
    } elseif ($check_year % 100 == 0) {
        $result = false;
    } elseif ($check_year % 4 == 0) {
        $result = true;
    } else {
        $result = false;
    }
    return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
    echo "$check_year is a Leap Year\n";
} else {
    echo "$check_year is not a Leap Year\n";
}

Bing このコードでもうるう年の判定はできます。☺
このコードでは、elseifを使って条件を分岐させています。これにより、ネストが浅くなり、$resultの値も一度しか変わりません。
しかし、このコードには少し問題があります。
それは、条件の順番です。
このコードでは、まず$check_yearが400で割り切れるかどうかをチェックしています。もし割り切れる場合、$resulttrueになります。
しかし、400で割り切れる年は必ず4で割り切れる年でもあります。つまり、最初の条件と最後の条件が重複しています。
これは無駄な処理であり、可読性も低くなります。
早期リターンを使ったコードでは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードでは、まず$check_yearが4で割り切れるかどうかをチェックしています。もし割り切れない場合、return falseという文で関数を終了します。
次に$check_yearが100で割り切れるかどうかをチェックしています。もし割り切れない場合、return trueという文で関数を終了します。
最後に$check_yearが400で割り切れるかどうかをチェックしています。もし割り切れる場合、return trueという文で関数を終了します。
このように早期リターンを使った方が条件の順番が自然でわかりやすいと思います。☺
この回答はお役に立ちましたか?

 次に示すコードは、早期リターンを使ったコードと条件の順番が一緒です。
どちらの方が、可読性が高いと思いますか?

function isLeap ($check_year) {
    if ($check_year % 4 != 0) {
        $result = false;
    } elseif ($check_year % 100 != 0) {
        $result = true;
    } elseif ($check_year % 400 != 0) {
        $result = false;
    } else {
        $result = true;
    }
    return $result;
}
$check_year=2000;
if (isLeap ($check_year)) {
    echo "$check_year is a Leap Year\n";
} else {
    echo "$check_year is not a Leap Year\n";
}

Bing このコードでもうるう年の判定はできます。☺
このコードでは、条件の順番が早期リターンを使ったコードと一緒です。これにより、条件の重複を避けることができます。
しかし、このコードにはまだ改善の余地があります。
それは、else文です。
このコードでは、最後のelse文で$resulttrueにしています。しかし、これは必要ありません。
なぜなら、最後のelse文に入るということは、$check_yearが4で割り切れてかつ100で割り切れてかつ400で割り切れるということです。
つまり、$check_yearが400で割り切れる場合と同じです。
その場合、$resulttrueにする必要はありません。return trueという文で関数を終了すれば十分です。
早期リターンを使ったコードでは、

function isLeap ($check_year) {
  if ($check_year % 4 != 0) {
    return false;
  }
  if ($check_year % 100 != 0) {
    return true;
  }
  if ($check_year % 400 == 0) {
    return true;
  }
  return false;
}
$check_year=2000;
if (isLeap ($check_year)) {
  echo "$check_year is a Leap Year\n";
} else {
  echo "$check_year is not a Leap Year\n";
}

このコードでは、最後のreturn falseという文は必ずしも必要ではありません。なぜなら、この関数に入力される値は整数だからです。
もし小数や文字列などが入力された場合はどうなるでしょうか?
その場合はエラーが発生する可能性があります。そのため、入力値の型をチェックする処理を追加することも考えられます。
例えば、

function isLeap ($check_year) {
  
// 入力値の型チェック
if (!is_int($check_year)) {
    return "Invalid input";
}

// 入力値の範囲チェック
if ($check_year < -4713 || $check_year == -1 || $check_year == -0) {
    return "Invalid year";
}

// 西暦1年以前の場合
if ($check_year < 1) {

// ユリウス暦における閏年判定
    if ($check_year % -4 == -1) {
        return true;
    } else {
        return false;
    }

// 西暦1年以降の場合
} else {

// グレゴリオ暦における閏年判定
    if ($check_year % 4 != 0) {
        return false;
    }
    if ($check_year % 100 != 0) {
        return true;
    }
    if ($check_year % 400 == 0) {
        return true;
    }
    
}

return false;

}
$test_cases = array(-4713,-4712,-45,-44,-1,-0,0,1,2,3,4,5,99,100,101,
                    ...,
                    ...,
                    ...,
                    ...,
                    ...);
foreach($test_cases as $case){
   $result = isLeap($case);
   echo "$case: ";
   var_dump($result);
   echo "\n";
}

このように早期リターンを使った方がエラーハンドリングや特殊ケースの処理も簡単になります。☺
この回答はお役に立ちましたか?


以上で8回までの会話の上限に達してしまいました。実際にエンジニアとチャットしているような感覚でしたし、最後の方は Bing もアツくなって暴走しているみたいで面白かったです☻

あなたはどんな書き方が好きですか?

関連記事

Laravelでのルーティングからビューにデータを渡すまで

Laravel

Laravelにおけるルーティングからビューにデータを渡すまでの基本的な実装パターンをまとめました。状況に応じて適切なパターンを使い分けましょう。

ルーティング

クロージャによるルーティングはいたって単純です。

Route::get('/', function () {
    return view('welcome');
});

ルーティングにクロージャを使用すると、ルートキャッシュは動作しません。ルートキャッシュを使用するには、コントローラルートを定義します。

Route::get('/user', 'UserController@index');

ビュールート

ルートからビューを返すだけの場合は、Route::viewメソッドを使用します。このメソッドを使用しても、ルートキャッシュは動作するようです。

Route::view('/welcome', 'welcome');

ビューにデータを渡す

viewヘルパ関数を使用して、データを配列でビューに渡せます。

Route::get('/', function () {
    return view('greeting', ['name' => 'James']);
});

withメソッドでビューに渡すデータを個別に追加することもできます。

Route::get('/', function () {
    return view('greeting')->with('name', 'Victoria');
});

ビュールートでデータを渡す

Route::viewメソッドでは、ビューへ渡すデータの配列を第3引数として指定します。

Route::view('/welcome', 'welcome', ['name' => 'Taylor']);

ここで、allgetのようなメソッドでモデルを取得して渡すのは避けるべきです。モデルはクロージャやコントローラの中で取得しないと、該当ルート以外のリクエストでも、routes/web.phpファイルが読み込まれる毎にクエリが実行されてしまいます。また、場合によってはユニットテストでIlluminate\Database\QueryException例外が投げられることがあります。

Route::view('/', 'welcome', ['categories' => Category::all()]);
1) Tests\Feature\ExampleTest::testBasicTest
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: categories (SQL: select * from "categories")

これはテスト実行時、マイグレーションが実行される前に、routes/web.phpファイルが読み込まれることが原因です。

全ビュー間のデータ共有

アプリケーションの全ビューでデータを共有するには、View::shareメソッドを使います。

View::share('key', 'value');

通常、サービスプロバイダのbootメソッド内で呼び出しますが、ここでもモデルを取得して渡していると、先ほどの例と同様にエラーの原因になります。

参考
QueryException: General error: 1 no such table: {table_name} (SQL: select * from “{table_name}”) · Issue #27018 · laravel/framework

ビューコンポーザ

ビューコンポーザは、ビューがレンダーされる直前に呼び出されます。したがって、ビューにモデルを渡す場合にも使えます。

View::composer('welcome', function ($view) {
    $categories = Category::all();
    $view->with('categories', $categories);
});

ビューコンポーザを複数のビューに適用するには、View::composerメソッドの最初の引数を配列で渡します。

View::composer(['welcome', 'home'], function ($view) {
    //
});

composerメソッドに渡しているビュー名には、ワイルドカードとして*を使用することもできます。

View::composer('*', function ($view) {
    //
});

ガード節より読みやすい!?途中リターンせずにネストを浅くする「ガードレール構文」の紹介

プログラムで値のチェック処理を書くとき、普通にif文を使っているとネストが深くなっていってしまいます。

function alertOverweight(user) {
  let result, height, weight;

  // 身長と体重が数値であることを確認
  if (!(isNaN(user.height) || isNaN(user.weight))) {
    height = user.height / 100; // 身長を取得
    weight = user.weight; // 体重を取得

    // 身長が0より大きいことを確認
    if (height > 0) {
      user.bmi = weight / height ** 2; // BMIを計算

      // BMIが25以上であることを確認
      if (user.bmi >= 25) {
        alert('体重を減らしましょう。');

        result = '過体重でした。';
      } else {
        result = 'BMIが25未満のため、過体重ではありませんでした。';
      }
    } else {
      result = '身長が0以下のため、BMIが計算できませんでした。';
    }
  } else {
    result = '身長もしくは体重が数値でないため、BMIが計算できませんでした。';
  }
  return result;
}

このようにネストが深くなると、コードの可読性が低下してしまいます。

ガード節のデメリット

ガード節は、このようなネストを解消するための手法として広く使われています。

function alertOverweight(user) {
  let height, weight;

  // 身長もしくは体重が数値でない場合は結果を返す
  if (isNaN(user.height) || isNaN(user.weight)) {
    return '身長もしくは体重が数値でないため、BMIが計算できませんでした。';
  }
  height = user.height / 100; // 身長を取得
  weight = user.weight; // 体重を取得

  // 身長が0以下の場合は結果を返す
  if (height <= 0) {
    return '身長が0以下のため、BMIが計算できませんでした。';
  }
  user.bmi = weight / height ** 2; // BMIを計算

  // BMIが25未満の場合は結果を返す
  if (user.bmi < 25) {
    return 'BMIが25未満のため、過体重ではありませんでした。';
  }
  alert('体重を減らしましょう。');

  return '過体重でした。';
}

このように途中リターンを使って、チェック処理で関数から抜け出すことでネストを無くしているのです。これは一見するとコードがフラットになりますが、構造化プログラミングの観点ではあまり好ましくありません。例えば、この関数は最終的に「過体重でした。」という結果を返していますが、様々な条件を満たした結果であることが分かり難いといった問題が生じます。

途中リターンしなくてもネストは浅くできる

この問題を解決するために生まれたのが、今回紹介するガードレール構文です。先ほどのコードを、ガードレール構文を使って書き直すと以下のようになります。

function alertOverweight(user) {
  let result, proceed, height, weight;
  
  result = '身長もしくは体重が数値でないため、BMIが計算できませんでした。';
  proceed = !(isNaN(user.height) || isNaN(user.weight)); // 身長と体重が数値であることを確認
  if (proceed) {
    height = user.height / 100; // 身長を取得
    weight = user.weight; // 体重を取得

    result = '身長が0以下のため、BMIが計算できませんでした。';
    proceed = height > 0; // 身長が0より大きいことを確認
  } if (proceed) {
    user.bmi = weight / height ** 2; // BMIを計算

    result = 'BMIが25未満のため、過体重ではありませんでした。';
    proceed = user.bmi >= 25; // BMIが25以上であることを確認
  } if (proceed) {
    alert('体重を減らしましょう。');

    result = '過体重でした。';
  }
  return result;
}

このコードでは、result変数とproceed変数を段階的に更新することで、チェック処理を進めていきます。一度条件を外れると、それ以降のif文はすべてスルーして、最後に設定された結果を返す仕組みです。

if文が処理の流れに沿って直線状に連なっている様子から、これを「ガードレール構文」と呼ぶことにしました。

これならネストを減らしつつ、2つの変数から一連のチェック処理を芋づる式に辿っていくこともできるため、コードの見通しがとても良くなります。

まとめ

途中リターンは再帰関数で使われることもありますが、通常の制御構造(if文やfor文等)で実装できる場合は、極力使わないようにしましょう。

以上、途中リターンせずにネストを浅くする「ガードレール構文」の紹介でした。是非皆さんもガード節の代わりに使ってみてください。

参考記事

https://memorandumrail.com/program-conditional-branch

https://marycore.jp/coding/why-goto-statement-is-bad

https://www.ninton.co.jp/archives/8065

関連記事

Laravelにおける存在チェックの書き方

Laravel

Laravelでビューに渡されたEloquentコレクションの中身が存在しているか判別するプログラムの書き方をまとめます。

データをビューへ渡す

まず、データを表示するためにEloquentでコレクションを取得してビューへ渡します。

public function index()
{
    $users = User::all();

    return view('users', ['users' => $users]);
}

単にデータを表示する

ビューに渡されたデータはループで表示できます。

<html>
    <body>
        <h1>User</h1>

        @foreach($users as $user)
            <p>{{ $user->name }}</p>
        @endforeach
    </body>
</html>

データの存在を判別する

データの存在を判別するには、isNotEmptyメソッドを使います。

<html>
    <body>
        <h1>User</h1>

        @if ($users->isNotEmpty())
            <ul>
                @foreach($users as $user)
                    <li>{{ $user->name }}</li>
                @endforeach
            </ul>
        @else
            Nothing Found
        @endif
    </body>
</html>

逆の動作である、isEmptyメソッドを使うこともできます。

<html>
    <body>
        <h1>User</h1>

        @if ($users->isEmpty())
            Nothing Found
        @else
            <ul>
                @foreach($users as $user)
                    <li>{{ $user->name }}</li>
                @endforeach
            </ul>
        @endif
    </body>
</html>

その他の判別方法

より単純なケースでは、@forelse()が使えるかもしれません。

<html>
    <body>
        <h1>User</h1>

        @forelse($users as $user)
            <p>{{ $user->name }}</p>
        @empty
            Nothing Found
        @endforelse
    </body>
</html>

ちなみに、以下のような書き方では@empty()がうまく判別できませんでした。

<html>
    <body>
        <h1>User</h1>

        @foreach($users as $user)
            <p>{{ $user->name }}</p>
        @endforeach
        @empty($users)
            Nothing Found
        @endempty
    </body>
</html>

リレーションの存在の判定

リレーションの有無を調べるには、existsメソッドとdoesntExistメソッドが使用できます。

<html>
    <body>
        <h1>User</h1>

        @if ($user->posts()->exists())
            The user has some posts
        @endif

        @if ($user->posts()->doesntExist())
            The user doesn't have any posts
        @endif
    </body>
</html>

JavaScript を ES6(ES2015) で書いてイミュータブルなオブジェクトを実装しよう

イミュータブルという言葉を知っていますか?プログラミングにおける重要なキーワードの1つらしいのですが、私は知りませんでした。

イミュータブルとは何か?どうすればそれを実現できるのか?JavaScript を例にとって考察しようと思います。

イミュータブルとは

イミュータブル(immutable)とは、日本語で「不変」という意味の言葉です。イミュータブルなオブジェクトと言ったら、状態が変わらないオブジェクトのことを指します。

イミュータブルなオブジェクトは状態が変えられないため、変数のオブジェクトを更新する場合は、オブジェクトを作り直さなければいけません。

なぜイミュータブルなオブジェクトにするのか

なぜわざわざオブジェクトを作り直すのでしょうか。イミュータブルなオブジェクトを使用する利点は、いくつか考えられます。

以前の状態を簡単に再現できる

イミュータブルなオブジェクトは、状態が変化しないことが保証されているため、そのオブジェクトを保持しておくだけでいつでも元の状態を再現することができます。

データの更新を簡単に検出できる

イミュータブルなオブジェクトは、前と同じオブジェクトを参照している限り、状態も変化していないことがわかります。そのため、オブジェクトが持っているすべての状態を1つずつ取り出して比較する必要がありません。

React ではパフォーマンスを最適化できる

JavaScript ライブラリの React では、イミュータブルな pure component を構築することで、効率的な再レンダーが可能になります。1

JavaScript でオブジェクトをイミュータブルなものとして扱う

JavaScript において、文字列や数値、真偽値等のデータは絶対的にイミュータブルです。そのため、これらのデータを扱う場合はイミュータブルを意識する必要はありません。2

一方で、オブジェクトや配列は中の状態が変えられるため、基本的にイミュータブルなデータではありません。

// オブジェクトの場合
let obj1, obj2;

obj1 = {a: 1};
obj2 = obj1;

obj2["1"] = 2;
obj2.a = "b";

console.log(obj1); // {1: 2, a: "b"}

// 配列の場合
let ary1, ary2;

ary1 = [1, 2];
ary2 = ary1;

ary2.push(3);
ary2[0] = "a";

console.log(ary1); // ["a", 2, 3]

オブジェクトや配列をイミュータブルなデータとして扱いたい場合は、少し工夫が必要です。基となるオブジェクトの状態は変えずに、新しい状態を持った別のオブジェクトを作成し、古いデータと置き換えて使用します。

// オブジェクトの場合
let obj1, obj2;

obj1 = {a: 1};
obj2 = obj1;

obj2 = Object.assign({}, obj2, {"1": 2});
obj2 = Object.assign({}, obj2, {a: "b"});

console.log(obj1); // {a: 1}

// 配列の場合
let ary1, ary2;

ary1 = [1, 2];
ary2 = ary1;

ary2 = ary2.concat(3);
ary2 = ["a"].concat(ary2.slice(1));

console.log(ary1); // [1, 2]

イミュータブルなオブジェクト操作に使えるメソッド

Object.assign() は、ES6 から使えるようになった新しいメソッドです。コピー先として空のオブジェクトを指定することで、コピー元オブジェクトからプロパティをシャローコピー (1段階の深さのコピー)した新しいオブジェクトを返します。

配列では concat() メソッドや slice() メソッドを使うことで、元の配列を変更せずに要素を結合したり、元の配列を変更せずに要素を切り取ったり、非破壊的な配列操作が可能になります。反対に、sort()push() 等のメソッドは元の配列を直接変更してしまうため、イミュータブルな配列には使用できません。

他に、オブジェクトのプロパティや配列の要素に値を直接代入するようなコードも、イミュータブルなデータ更新では使うことができません。

データの変化を伴うメソッドのことを破壊的メソッド、データの変化を伴わないメソッドを非破壊的メソッドといいます。イミュータブルなオブジェクト操作では非破壊的メソッドを使いましょう。

参照の値をコピーする意味

上記のメソッドは、コピー元のプロパティがオブジェクトを参照している場合、参照の値のみをコピーします。また、オブジェクトを別の変数に代入する際も、参照の値をそのまま代入して、同じオブジェクトを参照するようにしています。

let ary1 = [
  {a: 1},
  {b: 2},
  {c: 3},
];

let ary2 = ary1;

ary1 = ary1.slice(1);

console.log(ary1[0] == ary2[1]); // true

このように同じオブジェクトを参照することで、オブジェクトの状態が等しいかどうかを簡単に調べることができます。オブジェクトを1から作り直すわけではないので、メモリが無駄に使用されることもありません。

スプレッド構文でより簡潔に

オブジェクトや配列をイミュータブルなデータとして扱うコードは、スプレッド構文3を使ってより簡潔に書くことができます。

// オブジェクトの場合
let obj1, obj2;

obj1 = {a: 1};
obj2 = obj1;

obj2 = {...obj2, "1": 2};
obj2 = {...obj2, a: "b"};

console.log(obj1); // {a: 1}

// 配列の場合
let ary1, ary2;

ary1 = [1, 2];
ary2 = ary1;

ary2 = [...ary2, 3];
ary2 = ["a", ...ary2.slice(1)];

console.log(ary1); // [1, 2]

オブジェクトのスプレッド構文は ES2018 から、配列のスプレッド構文は ES6 から使えるようになりました。

まとめ

オブジェクトの状態は変えず、新しく作り直すようにすることで、イミュータブルなオブジェクトを実現することができました。

ES6 から使えるようになった新しいメソッドやスプレッド構文を活用して、イミュータブルなオブジェクトを実装しましょう。

詳細情報

  1. パフォーマンス最適化 – React ↩︎
  2. Primitive (プリミティブ) – MDN Web Docs 用語集_ ウェブ関連用語の定義 _ MDN ↩︎
  3. スプレッド構文 – JavaScript _ MDN ↩︎