20
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

マジカルな値に名前をつけるべきかどうかはケースバイケース

Last updated at Posted at 2021-09-16

これは何?

マジカルな値に名前をつけるべきかどうかはケースバイケースだと思う、という話をとりとめもなく書く。

例を書きながら考える

錐の体積

錐の体積がほしい場合、

c++
double v = h*s*(1.0/3.0);

などと書くわけだけど、この (1.0/3.0) に名前をつけて

c++
constexpr double ONE_THIRD = 1.0/3.0;
double v = h*s*ONE_THIRD;

などとする人はいないと思う。わかりやすさが増えないから。
// 錐なので3で割る というコメントは有意義かもしれないし、この計算を関数という形にすることで計算に名前をつけるのは大いに有意義だけど、 ONE_THIRD のような形で数値リテラルに名前をつけるのは無意味というより有害だ。

しかし、これが ruby だと (1.0/3.0) と書いてしまうとこの除算が実行時に計算されてしまうので ONE_THIRD のような値で受けることには意味があるかもしれない。

角度の変換の例

その値がソースコード上に複数回現れる場合は、ほぼ間違いなく名前をつけたほうがいい。
いわゆる DRY 原則ということでもある。

とは思うものの、

c++
double ha = std::sin(a*(PI/180.0));
double hb = std::sin(b*(PI/180.0));

のような場合、PI/180.0 を 2回書くことに抵抗はあまりない。

PI/180.0 が二度登場しているんだから名前をつけるべき、という立場にはあまり反対しないが、

c++
double ha = std::sin(a*DEG_TO_RAD);
double hb = std::sin(b*DEG_TO_RAD);

私見では、名前をつけてもわかりやすくなる気がしない。
C++ であれば、実行速度の面でもメリットもない。
まあ、

c++
double ha = std::sin(deg_to_rad(a));
double hb = std::sin(deg_to_rad(b));

と、関数にするんだったら最初の例より良くなっていると言えるかもしれない。

deg_to_rad はどうなるかというと、素朴に書けば(PI は定義されているとして)

c++
inline double deg_to_rad( double x ){
  return x*(PI/180.0);
}

となるだろう。
この関数を

c++
inline double deg_to_rad( double x ){
  constexpr double DEG_TO_RAD = (PI/180.0);
  return x*DEG_TO_RAD;
}

と書くことはないと思う。
わかりやすさが増えないと思うから。

色空間の変換

例えば。CIE XYZ から (なんらかの)RGB に変換したいとする。
RGB の種類によって変換する行列は変わるんだけど、Wikipedia にある値がそのまま使えるとするなら、以下のような計算をすることになる。

{\displaystyle {\begin{bmatrix}R\\G\\B\end{bmatrix}}={\begin{bmatrix}0.418\,47&-0.158\,66&-0.082\,835\\-0.091\,169&0.252\,43&0.015\,708\\0.000\,920\,90&-0.002\,549\,8&0.178\,60\end{bmatrix}}{\begin{bmatrix}X\\Y\\Z\end{bmatrix}}}

これを 色空間の変更とかが全然予定されていない状況で、C言語で実装する場合。

c
rgb_color rgb_from_xyz( xyz_color const * s){
  // from https://ja.wikipedia.org/wiki/CIE_1931_%E8%89%B2%E7%A9%BA%E9%96%93
  rgb_color r={
    0.41847*s->x - 0.15866*s->y - 0.082835*s->z,
    -0.091169*s->x + 0.25243*s->y + 0.015708*s->z,
    0.00092090*s->x - 0.0025498*s->y + 0.17860*s->z
  };
  return r;
}

と、書くと思う。
行列ライブラリが使えたら(そして使い勝手が十分よければ)名前をつけた行列定数を定義するかもしれないけど、そうでなければこのように数値リテラルをそのまま書く。9つのリテラルに m00〜m22 までの名前をつけたりはしない。

逆向きの変換も必要な場合、逆行列を事前に計算しておいてその値をリテラルとして埋め込んだ関数を用意する。ある意味 DRY ではなくなってしまうわけだけど、わかりやすさ・単純さ・実行時のコスト、なんかを考えると DRY であるようにするために払えるコストではないと思う。

ビジネスに絡む計算

金の計算をするソフトウェアの経験は殆どないので自信はないけど、例えばなにかの料金が以下のように

重さ(0.1g単位、四捨五入) 料金(総額)
200g 未満 100円
200g〜999.9g 重さ(g)から 200 を減じた値に 0.625円 を乗じた値に 100円 を加算した額(1円未満切り捨て)
1000g以上 重さ(g)から 1000 を減じた値に 0.5円 を乗じた値に 600円 を加算した額(1円未満切り捨て)

なっているとする。
これを

ruby
def some_charge_from_raw_weight(raw_weight)
  # see https://somewhere.this.charge.table.defined
  weight = (raw_weight*10r).round * 0.1r
  return 100 if weight<200
  return 100+((weight-200)*0.625r).floor if weight<1000
  return 600+((weight-1000)*0.5r).floor
end

とすることは悪くないと思う。
重さによって段階が分かれること、各段階で重さの1次関数で求まることや1円未満切り捨てであることなどのすべてを前提とすることができないのであれば、 1000.625r に名前をつけたりデータベースに入れたりすることにほとんど意味はない。

それと、この計算が正当であることを確認するための資料の在り処をコメントに書いておくことは重要だと思う。

料金体系変更のたびに計算式自体が変わる可能性が無視できない場合にこういうことになると思う。

まとめ

名前をつけるメリットが少なめな例ばかりをあげたけど、もちろん多くの場合はメリットがある。ほとんどと言っていいぐらい。

名前の有効範囲が狭い、いい名前がない、値が使われている状況が十分に饒舌である、などがメリットが少なくなる要因だと思う。
一行文字数を減らすために名前をつけるとかいうこともあると思う。

「いい名前がない」が要因のひとつなので、命名力が低いと名無しのリテラルを書きがちかもしれない。

他の多くのことと同様「普通こうするといいことがあるよね」は「必ずこうしなければならない」を意味しない。
「普通こうするといいことがあるよね」を足がかりにしつつケースバイケースで判断するしかないと思う。

わかりやすくなるかどうか。保守しやすくなるかどうか。実行速度への影響はどうか。などを総合的に判断して、やりたいことに合わせて判断する。わかりやすくはならないけど実行速度のために変数で受けるとかいうこともあると思う。

というわけで、タイトルのとおりケースバイケース。

20
5
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?