破壊的メソッド名には!をつけなければならない…なんて、誰が言ったの?

  • 106
    Like
  • 2
    Comment

そんなものは迷信に過ぎない。

はじめに

ここ数週間で何度か「破壊的メソッドなのに!がついてない」「!がついてないのに副作用がある」といった言説を何度か目にする機会があった。

「破壊的」と「非破壊的」

a.hoge(b)のようにメソッド呼び出しをしたとき、aの状態に影響を及ぼすメソッドを、Rubyでは一般的に「破壊的」と呼びます。メソッド呼び出しで前の状態を壊して、別の状態にしてしまふからです。

言葉で説明してもぴんとこないかもしれないので、「ふたつの配列をくっつけたい」といふ要望に対して、破壊的なパターンと、破壊的ではないパターンのコードを紹介します。

破壊ではないパターン

# ふたつの配列を用意する
a = [0, 2, 4, 6, 8]
b = [1, 3, 5, 7, 9]

c = a + b  # aとbを連結してcに代入

p a #=> [0, 2, 4, 6, 8]
p b #=> [1, 3, 5, 7, 9]
p c #=> [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]

破壊的なパターン

# ふたつの配列を用意する
a = [0, 2, 4, 6, 8]
b = [1, 3, 5, 7, 9]

a.concat(b)  # aにbを結合する

p a #=> [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
p b #=> [1, 3, 5, 7, 9]

# もう一回実行してみる
a.concat(b)  # aにbを結合する
#=> [0, 2, 4, 6, 8, 1, 3, 5, 7, 9, 1, 3, 5, 7, 9]

同じメソッドの破壊的バージョン

Rubyには、同じ機能のメソッドでも破壊的バージョンと非破壊的バージョン、つまりは「元のオブジェクトの状態を書き換へる」か「元のオブジェクトには影響を及ぼさず新しいオブジェクトを作成する」かのバリエーションを持ったものがあります。

そのようなメソッドは数多くあるのですが、ひとつの例としてArray#mapArray#map!を挙げます。

# 引数を2倍にして返す
twice = ->(n){ n * 2 }

p ary = [*0..10]
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 配列の全要素を2倍にする
p ary.map(&twice)
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# 3回実行
3.times do
  p ary.map(&twice)
end
#=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
#=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
#=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# 何回やっても結果は同じ

# こんどはmap!を使ってみる
p ary.map!(&twice)
#=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

p ary
#=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# aryが最初の状態から変化してる(この配列は破壊されてしまった)

# また3回実行
3.times do
  p ary.map!(&twice)
end
#=> [4, 8, 12, 16, 20, 24, 28, 32, 36, 40]
#=> [8, 16, 24, 32, 40, 48, 56, 64, 72, 80]
#=> [16, 32, 48, 64, 80, 96, 112, 128, 144, 160]
# (おのれディケイド、この配列はまたしても破壊された!)

このように、同じ配列名で「破壊的」と「非破壊的」のバリエーションがある場合には!をつけることで 区別 されています。

誤解

mapmap!のような例から、「すべての破壊的メソッドには!がつけられるべきだ」とする宗派があります。

それは誤解です。しかしその誤解は根深いようで、実に2000年10月のログが残ってゐます。(14年前!)

Subject: [ruby-list:25288] Re: Array#delete が破壊的な理由
From: matz zetabits.com (Yukihiro Matsumoto)
Date: Tue, 3 Oct 2000 01:15:46 +0900
References: 25280
In-reply-to: 25280

まつもと ゆきひろです

In message "[ruby-list:25280] Array#delete が破壊的な理由"
on 00/10/02, Yasushi Shoji writes:

を、bladeやら ML topicsで探したんですが、見つけられませんでした‥。

年始年末の Array#sliceや pop(3)の議論のも見たんですが、「deleteの意味か
ら戻り値を期待すべきではない」との意見はあったのですが‥‥。

どこぞに理由なんかは書いてないですかね?

「Array#delete が破壊的な理由」ですか。「最初にそういう風に
決めたから」という以上にはあんまりないですよねえ。

# !が*より*破壊的なだけってのは理解してるんですが‥
# つかってると、!がついてないと selfはそのままという様な気がしてきて‥。

そりはちょっとムリな期待では。!のついてない破壊的メソッドが
いったいいくつあるということを考えるとくらくらしそう。

まつもと ゆきひろ /:|)

([ruby-list:25288] Re: Array#delete が破壊的な理由より引用。@tadsanが整形を加へた)

少なくともRuby作者のMatzは「破壊的メソッド全てに!をつけるべき」といふ見解ではなさそうです。また、この発言には重要な示唆があります。

# !が*より*破壊的なだけってのは理解してるんですが‥
# つかってると、!がついてないと selfはそのままという様な気がしてきて‥。

「破壊的」の概念は 相対的である といふことも、2000年の時点で確立されてゐたようです。

Rubyのリファレンスにおいては

Rubyで使われる記号の意味(正規表現の複雑な記号は除く)には以下のようにあります。

def xxx!
「!」はメソッド名の一部です。慣用的に、 同名の(! の無い)メソッドに比べて より破壊的 な作用をもつメソッド(例: tr と tr!)で使われます。

(Rubyで使われる記号の意味(正規表現の複雑な記号は除く)より引用。強調部は@tadsanが付記)

「破壊的な作用」が相対的であることについて言及されてゐます。

初めてのRuby

yuguiさんの「初めてのRuby」では34ページの本文と脚注に言及があります。

巨大な反例

みなさまだいすきな Ruby on Rails さんは「!は破壊的」なんて虚構の原則にはまるっきり従ってません。

Railsが提供するオブジェクトは外部から見えにくい内部状態を持ってるし(例: ActiveSupport::SafeBuffer)、みなさまだいすきなActiveRecordのモデルはそれに加へて、savesave!のようなメソッドは、破壊的だなんだといった枠組からは完全に外れてます。

(ActiveSupport::SafeBufferにはhtml_safe?があるが、テンプレート内でこれに頼った処理を書くのは大概の場合において筋悪)

まとめ

「破壊的メソッドには!をつけなければならない」とは古くからある 迷信 であって、同名のメソッドに!がついてるときは「要注意バージョン」「(相対的に)破壊的である」といふ、開発者から利用者への目印です。

また、「破壊」といふ作用は相対的なものです。!のあるなしで、「なし」バージョンに副作用がないことを保証するものではありません。

みなさまにおかれましては、「破壊的なのに!がないぞ!」などと、因縁をお付けになりませんよう…………。