Rails5 で ActionController::Parameters#fetch の返り値に []= を実行しても params の値を変更できない

## 現象
Rails5 で`ActionController::Parameters#fetch` の返り値に `ActionController::Parameters#[]=` を使用しても `Hash` の値を変更できないという現象です。

```ruby:サンプルコード（Rails5の場合）
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params.fetch(:param_key)[:hash_key] = "modified"

params.fetch(:param_key)[:hash_key]
# => "original"
```

Rails4 では `ActionController::Parameters#fetch` の返り値に `ActionController::Paramers#[]=` を使用すると `Hash` の値を変更できていました。

```ruby:サンプルコード（Rails4の場合）
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params.fetch(:param_key)[:hash_key] = "modified"

params.fetch(:param_key)[:hash_key]
# => "modified"
```

## 開発環境
Rails5: v5.0.7.2
Rails4: v4.2.11.1

## 解決できた方法
### fetch ではなく [] で実装する方法
`ActionController::Parameters#fetch` を使わずに `ActionController::Parameters#[]` を使用すると、 `params` の値を変更することができました。

```ruby:サンプルコード（Rails5）
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params[:param_key][:hash_key] = "modified"

params[:param_key][:hash_key]
# => "modified"
```

## 背景
Rails4 から Rails5 にアップデートしたところ、 `ActionController::Parameters` の `fetch` と `[]=` を使って `params` の値を変更する箇所が、うまく動作しないことに気づきました。

```ruby:サンプルコード
params.fetch(:schedule)[:start_at] = DateTime.parse("#{date} #{params.dig(:schedule, :start_at).in_time_zone('Tokyo')} JST")
```

Rails5 で `ActionController::Parameters` の実装が変わったことがきっかけではないかと考え、 Rails の内部実装を調査することにしました。

## 調査結果
### Rails5 だと fetch の返り値に []= を実行しても参照元の値を変更できないことがある
- `fetch(key)` で参照しようとした値が `Hash` だった場合、
    - `Hash` を `ActionController::Parameters` のインスタンスに変換して返します
    - 返ってきたインスタンスに対して `[]=` を実行しても参照元の値は変更されません

```ruby:rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def fetch(key, *args)
  convert_value_to_parameters(
    @parameters.fetch(key) {
      if block_given?
        yield
      else
        args.fetch(0) { raise ActionController::ParameterMissing.new(key) }
      end
    }
  )
end
```

[Source on GitHub](https://github.com/rails/rails/blob/v5.0.7.2/actionpack/lib/action_controller/metal/strong_parameters.rb#L582-L592)

`ActionController::Parameters#fetch` では `ActiveSupport::HashWithIndifferentAccess` クラスのインスタンスである `@parameters` に対して `Hash#fetch` メソッドが実行されます。

そして、その返り値を引数として `ActionController::Parameters#convert_value_to_parameters` が実行されます。

```ruby:rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def convert_value_to_parameters(value)
  case value
  when Array
    return value if converted_arrays.member?(value)
    converted = value.map { |_| convert_value_to_parameters(_) }
    converted_arrays << converted
    converted
  when Hash
    self.class.new(value)
  else
    value
  end
end
```

[Source on GitHub](https://github.com/rails/rails/blob/v5.0.7.2//actionpack/lib/action_controller/metal/strong_parameters.rb#L849-L861)

`ActionController::Parameters#convert_value_to_parameters` の引数に渡ってくる値のクラスが `ActiveSupport::HashWithIndifferentAccess` である場合、そのクラスは `Hash` を継承しているので、 `self.class.new(value)` が実行されます。

```ruby:サンプルコード
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params.fetch(:param_key)[:hash_key] = "modified"

params.fetch(:param_key)[:hash_key]
=> "original"
params[:param_key][:hash_key]
=> "original"
```

上記の調査を元にサンプルコードを説明すると、 `params.fetch(:param_key)` の返り値は `ActionController::Parameters.new({ hash_key: "original"})` になります。

上記で作成された `ActionController::Parameters` のインスタンスは `params` とは別のインスタンスなので、 `[:hash_key] = "modified"` を実行しても `params` の値は変更できません。

### [] の返り値に []= を実行すれば参照元の値を変更できる
- `[](key)` で参照しようとした値が `Hash` の場合、
    - `Hash` を `ActionController::Parameters` のインスタンスに変換して返します
    - 変換した値を参照元の `Hash` に再代入します
    - 再代入されているため、返ってきたインスタンスに `[]=` を実行すると参照元の値も変更できます

```ruby:rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def [](key)
  convert_hashes_to_parameters(key, @parameters[key])
end
```

[Source on GitHub](https://github.com/rails/rails/blob/v5.0.7.2//actionpack/lib/action_controller/metal/strong_parameters.rb#L561-L563)

`ActionController::Parameters#[]` では `ActionController::Parameters#convert_hashes_to_parameters` が実行されます。

```ruby:rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def convert_hashes_to_parameters(key, value)
  converted = convert_value_to_parameters(value)
  @parameters[key] = converted unless converted.equal?(value)
  converted
end
```

[Source on GitHub](https://github.com/rails/rails/blob/v5.0.7.2///actionpack/lib/action_controller/metal/strong_parameters.rb#L843-L847)

`[](key)` で参照しようとした値が `Hash` だった場合、 その `Hash` が `ActionController::Parameters#convert_value_to_parameters` によって、 `ActionController::Parameters` のインスタンスに変換されます。

さらに変換されたインスタンスを参照元の値に再代入した後に、そのインスタンスを返します。参照元に再代入しているため、返ってきたインスタンスの値を変更すると参照元の値も変更されます。

```ruby:サンプルコード
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params[:param_key][:hash_key] = "modified"

params[:param_key][:hash_key]
=> "modified"
```

上記の調査を元にサンプルコードを説明します。 `params[:param_key]` を実行することで、 `params[:param_key]` の値が `ActionController::Parameters` のインスタンスに変換され、さらに `params[:param_key]` に変換されたインスタンスが再代入されます。

`ActionController::Parameters` のインスタンスは `params[:param_key]` に再代入されているため、 `params[:param_key]` で返ってきたインスタンスに対して `[:hash_key] = "modified"` を実行すると、 `params` の値も変更されます。

## 備考
### Hash ではなく ActionController::Parameters であれば fetch でも変更できる
例えば、以下のサンプルコードのように一度 `ActionController::Parameters#[]` を実行してしまえば、 `params.fetch[:param_key][:hash_key] = "modified"` でも `params` の値を変更することができます。

```ruby:サンプルコード
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params[:param_key] # ここで params[:param_key] に ActionController::Parameters に変換されたインスタンスが再代入される
params.fetch(:param_key)[:hash_key] = "modified"

params[:param_key][:hash_key]
=> "modified"
```

まず `params[:param_key]` が実行されることで、 `params[:param_key]` に `ActionController::Parameters` のインスタンスが再代入されます。

つぎに `params.fetch(:param_key)` を実行すると、内部で `convert_value_to_parameters` が実行されるわけですが、 ここで引数に渡ってくる値は `ActiveSupport::HashWithIndifferentAccess` ではなく、 `ActionController::Parameters` のインスタンスになります。

```ruby:rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def convert_value_to_parameters(value)
  case value
  when Array
    return value if converted_arrays.member?(value)
    converted = value.map { |_| convert_value_to_parameters(_) }
    converted_arrays << converted
    converted
  when Hash
    self.class.new(value)
  else
    value
  end
end
```

よって、 `params.fetch(:param_key)` の返り値は `params[:param_key]` に再代入された `ActionController::Parameters` のインスタンスなので、 `params.fetch(:param_key)[:hash_key] = "modified"` を実行すれば、 `params` の値も変更されます。