gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

0000-00-00 と PHP:実装側の考察

さて。
とりあえず「0000-00-00を食い止める」を至上命題にする場合、必要なのが
・空文字でINSERTしない
ここ。
これを主眼にして、少し実装側の考察をしてみませう。

一つ、ガードとして
sql_modeに STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_DATE を入れて置く
という手があるのですが。データは入らないのですが「SQLでエラーになる」ので、やり方を間違えると若干困るので、もうちょっと頑張ってみましょう。

https://gallu.hatenadiary.jp/entry/2019/11/04/160833 でも書きましたが、Laravel には ConvertEmptyStringsToNull というMiddleware があります。
今回の件で考えると一件便利そう、ではあるのですが、LaravelのConvertEmptyStringsToNullは「全項目無差別」なので。

んと……端的には
・文字列のカラムがあって
・空文字でもいい(入力必須ではない)
ようなケースで、ConvertEmptyStringsToNullがonになっていると
・未入力
・空文字……が、ConvertEmptyStringsToNullによってNULLに変換される
・NULLをinsertしようとしてエラー(NOT NULL制約って、普通、入ってるよね?)
って感じになるので、無差別はちょっと嬉しくないの感じでございます。

となると、とりあえずやりたいのが
・「カラムが日付系の型のデータだけ」空文字ならNULLに変換したい
ってのが、とりあえず今回としては「よい落としどころ」になるのではないかなぁ、と思っています。

後は、カラムによっては「文字型であろうが"NULL許可で、空文字が入るくらいならNULLがいい"」ってケースが、おいちゃんは全く思いつかないし思い浮かばないのですが、もしかするとそういうケースが「ないとも限らない」のであろうなぁ、と、思われるので(可能性としては)。

大雑把には
・「対象になるカラム名の配列」を用意して
・そのカラムについては「空文字ならNULLに置換する」
ような処理を、良い感じの箇所に挟み込んでおくと、比較的「案配がよい」のではないかなぁ、と思われます。

「とりあえず」SlimLittleToolsでの実装予定

んで。最近、Slimが大変にお好みなので、つい SlimLittleTools / なんてぇものを作って、使っておりますが。
そこでは
・filterの追加: ConvertEmptyStringsToNull と同じようなもの、を、カラム単位で出来るように
・MackModelDetail の修正( make_model_detail.php の実態 ):「日付型のカラムの配列」を作成できるようにする
・Modelの修正: 「日付型のカラムの配列」については「空文字ならNULL」に変換するようにする:その変換が on/off 出来るようなスイッチ用のメソッドを追加(デフォルトは、一応 false(変換しない)にしておく)
なんて実装を考えてます。

あと、 https://gallu.hatenadiary.jp/entry/2019/11/04/160833 で書いた
> NULLをVALUESに与えると、NULLになる(DEFAULTの値にはならない)
この部分が微妙に気になって引っかかっているので。加えて
・Modelの修正: insertとupdateのタイミングで「値がNULLなら、keyごと削除」の処理を追加:その削除が on/off 出来るようなスイッチ用のメソッドを追加(デフォルトは、一応 true(削除する)にしておく)
を入れておこうかなぁ、と思ってます。

この辺はまだ、少し考察や検討の余地などもありそうなので。
ご意見などいただけると、反映できる、かもしれないので、よかったら是非。

0000-00-00 と PHP

MySQLの、(少なくとも一部では)悪名高き 0000-00-00 について、は、そこそこブログがあるのですが。
「それを、PHP(plain)とか(PHPの)フレームワークとかでどうやってるんだろ?」というのが、ざっくりググった限りだと案外と無かったので。
調査かてがて、備忘録として。

0000-00-00 について

まずは 0000-00-00 について。
いやまぁググればあちこちで出てくる「MySQLに固有の、奇異なデータ」なのですが。
端的には
・入れ方によって、日付型(DATE、DATETIME、TIMESTAMP)に入る事があるデータ
MySQL以外で 0000-00-00 が入るRDBはない(はず)
・NOT NULL制約が入っているカラムで「0000-00-00」があると、WHERE句の IS NULL、IS NOT NULLの双方で 0000-00-00が引っかかる
てなもの、になります。

http://nippondanji.blogspot.com/2018/05/mysqlzero-date.html
https://soudai.hatenablog.com/entry/2018/05/12/191050
http://sakaik.hateblo.jp/entry/20151227/mysql_date_null

など、言及されている所は少なからずあるか、と思います。
詳しくは、上述のBlogなど見て頂くほうが早いかなぁ、と思います。

少し細かくまとめると。

・カラムにNOT NULL制約がある時
 → 空文字('')をVALUESに与えると、 0000-00-00 になる: modeにSTRICT_TRANS_TABLESがある時はエラーが出る(Incorrect date value: '' for column)
 → NULLをVALUESに与えると、エラーになる(当たり前だ)
・カラムにNOT NULL制約がない時
 → 空文字('')をVALUESに与えると、 0000-00-00 になる: modeにSTRICT_TRANS_TABLESがある時はエラーが出る(Incorrect date value: '' for column)
 → NULLをVALUESに与えると、NULLになる(当たり前だ)

となります。
また
・カラムにNOT NULL制約があり、かつ、DEFAULTが定義されている時
 → 空文字('')をVALUESに与えると、 0000-00-00 になる: modeにSTRICT_TRANS_TABLESがある時はエラーが出る(Incorrect date value: '' for column)
 → NULLをVALUESに与えると、エラーになる(当たり前だ)
 → INSERTの対象カラムからそもそも削除すると、DEFAULTで定義された値になる(当たり前だ)
・カラムにNOT NULL制約がなく、かつ、DEFAULTが定義されている時
 → 空文字('')をVALUESに与えると、 0000-00-00 になる: modeにSTRICT_TRANS_TABLESがある時はエラーが出る(Incorrect date value: '' for column)
 → NULLをVALUESに与えると、NULLになる(DEFAULTの値にはならない)
 → INSERTの対象カラムからそもそも削除すると、DEFAULTで定義された値になる(当たり前だ)

となります。
ここ、後述でちょっと重要になるので、ポイントとして。

フレームワーク無し」で実装すると?

まぁ昨今だととかですかねぇ。古来だとか。
日付を入力してもらう時に、まぁ最終的には文字列で受け取るわけ、なのですが。
これが「必須入力」なら「日付のフォーマットに沿ってるかどうかを判断」とかでいけばよくて……個人的にはstrtotime()使うのが、楽で好みかなぁ。

$t = strtotime($_POST['日付'] ?? '');
if (false === $t) {
    // エラー処理
    return;
}
// else
$date = date('Y-m-d H:i:s', $t);

とかやるとざっくりとフォーマットが整うので、よく使います。
DateTimeとかDateTimeImmutableとかのクラスを使う場合(このチョイスならDateTimeImmutableのほうが好み)、エラー時は「例外を投げてくる」ので、書き方にちょっとだけ注意を払いましょう。

ただ、必須じゃない場合「空文字を許容」する必要がある、と思われるのですが。

$date = $_POST['日付'] ?? '';
if ('' !== $date) {
    $t = strtotime();
    if (false === $t) {
        // エラー処理
        return;
    }
    // else
    $date = date('Y-m-d H:i:s', $t);
}

上述で「パラメタの値を取得して」「SQL作成してINSERTとかUPDATEとか」やると、結局日付系のカラムで「VALUEが空文字なINSERT/UPDATE」が走るので、0000-00-00問題が、回避できないような気がするのですだよ……。

なお

$date = $_POST['日付'] ?? null;

ってやっても変わらない。だって「formには"日付"のnameアトリビュートが存在しているから、空文字が返ってくる事が十分に想起される」から。

PHPフレームワークはどうなってるんだろ?

と言うわけで様々なフレームワークを……試す根性がありませんでしたすみません orz
ので、現状多分「一番メジャーであろう」Laravelで実験。
とりあえずバージョンは…… 6.4.1 。あら。5.8系かと思ったんだけど、バージョン指定しないとこんな先のバージョンになるのか。
まぁ「新しいほうがより機能は洗練されてる」だろうから、あんまり気にしない(雑

すげぇ単純に
・テーブル作って
・form入りのテンプレート作って
・ざっくりとinsert
こんな感じ。

マニュアルのPageにも
https://readouble.com/laravel/5.8/ja/eloquent.html

        $flight = new Flight;
        $flight->name = $request->name;
        $flight->save();

ってあるので、おんなじようにやってみる……エラー。
どうもConvertEmptyStringsToNullというのがお邪魔しくさりやがってくださっているらしく
・ConvertEmptyStringsToNull Middleware によって「空文字ならnullに置換」される
・対象カラムがNOT NULL制約
・なので「NOT NULL制約の所にNULLぶち込んだから嫌がられる」
という単純構造。

一端 app/Http/Kernel.php に修正を入れて、ConvertEmptyStringsToNull をoff(ついでにTrimStringsもoffしたほうがいいんじゃねぇか疑惑があるんだが一端放置)。
「未入力ならそのまま空文字になる」ように修正して再度実行。

……予想通り 0000-00-00 が入りやがりました orz
この辺、Laravelでも特に「なんか対応」はしていないんだなぁ、と、実感。

今までよく引っかかってこなかったなぁ……と思うんだけど、まぁぶち合ったんだからどうにか考えませんとなぁ。

https://gallu.hatenadiary.jp/entry/2019/11/04/160910 で、実装について少し考察をしてみます。

おまけ:実験結果

mysql> show create table date_tests;
+------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| date_tests | CREATE TABLE `date_tests` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`d_test` date NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci |
+------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select id, d_test from date_tests;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
+----+------------+
2 rows in set (0.01 sec)

mysql> insert into date_tests(d_test) value('');
Query OK, 1 row affected, 1 warning (0.01 sec)

mysql> insert into date_tests(d_test) value(null);
ERROR 1048 (23000): Column 'd_test' cannot be null
mysql> select id, d_test from date_tests;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
+----+------------+
3 rows in set (0.00 sec)

mysql> ALTER TABLE date_tests MODIFY COLUMN `d_test` date ;
Query OK, 0 rows affected (0.15 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> insert into date_tests(d_test) value('');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> insert into date_tests(d_test) value(null);
Query OK, 1 row affected (0.01 sec)

mysql> select id, d_test from date_tests;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 5 | NULL |
+----+------------+
5 rows in set (0.00 sec)

mysql> SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
Query OK, 0 rows affected (0.00 sec)

mysql> insert into date_tests(d_test) value('');
ERROR 1292 (22007): Incorrect date value: '' for column 'd_test' at row 1
mysql> insert into date_tests(d_test) value(null);
Query OK, 1 row affected (0.00 sec)

mysql> select id, d_test from date_tests;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 5 | NULL |
| 6 | 0000-00-00 |
| 7 | NULL |
+----+------------+
7 rows in set (0.00 sec)



mysql> select id, d_test from date_tests where d_test is null;
+----+--------+
| id | d_test |
+----+--------+
| 5 | NULL |
| 7 | NULL |
+----+--------+
2 rows in set (0.00 sec)

mysql> select id, d_test from date_tests where d_test is not null;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
+----+------------+
5 rows in set (0.00 sec)

mysql> ALTER TABLE date_tests MODIFY COLUMN `d_test` date NOT NULL;
Query OK, 5 rows affected (0.09 sec)
Records: 5 Duplicates: 0 Warnings: 0

mysql> select id, d_test from date_tests ;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
+----+------------+
5 rows in set (0.00 sec)

mysql> select id, d_test from date_tests where d_test is null;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
+----+------------+
4 rows in set (0.00 sec)

mysql> select id, d_test from date_tests where d_test is not null;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
+----+------------+
5 rows in set (0.00 sec)

mysql> ALTER TABLE date_tests MODIFY COLUMN `d_test` date DEFAULT '1970-1-1';
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> insert into date_tests(d_test) value('');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> insert into date_tests(d_test) value(null);
Query OK, 1 row affected (0.00 sec)

mysql> select id, d_test from date_tests ;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
| 8 | 0000-00-00 |
| 9 | NULL |
+----+------------+
7 rows in set (0.00 sec)

mysql> ALTER TABLE date_tests MODIFY COLUMN `d_test` date NOT NULL DEFAULT '1970-1-1';
Query OK, 7 rows affected, 1 warning (0.02 sec)
Records: 7 Duplicates: 0 Warnings: 1

mysql> select id, d_test from date_tests ;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
| 8 | 0000-00-00 |
| 9 | 0000-00-00 |
+----+------------+
7 rows in set (0.01 sec)

mysql> insert into date_tests(d_test) value('');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> insert into date_tests(d_test) value(null);
ERROR 1048 (23000): Column 'd_test' cannot be null
mysql> select id, d_test from date_tests ;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
| 8 | 0000-00-00 |
| 9 | 0000-00-00 |
| 10 | 0000-00-00 |
+----+------------+
8 rows in set (0.00 sec)

mysql> insert into date_tests(id) value(null);
Query OK, 1 row affected (0.01 sec)

mysql> select id, d_test from date_tests ;
+----+------------+
| id | d_test |
+----+------------+
| 1 | 0000-00-00 |
| 2 | 2019-11-01 |
| 3 | 0000-00-00 |
| 4 | 0000-00-00 |
| 6 | 0000-00-00 |
| 8 | 0000-00-00 |
| 9 | 0000-00-00 |
| 10 | 0000-00-00 |
| 11 | 1970-01-01 |
+----+------------+
9 rows in set (0.01 sec)

インフラの(雑)管理方法

https://twitter.com/ndxbn/status/1189735988907536384

マシンの /usr/local 以下に make && make install で直接インストールしてあるミドルウェアのバージョン更新って、どうしてます?
普通に新しいバージョンの tar ball を持ってきて、 make && make install で上書きしておわり?

https://twitter.com/ndxbn/status/1189745023736090625

ミドルウェアの更新するハメになったのだけれど、そういえばどうしてるんだろうって気になったので聞いたーー
使わなくなった lib とか includes が残留していくけど、キニシナイ感じなのねー?
更新してダメだったとき用で直ぐにバージョン戻せる必要とかもあるから /opt とか使ってくれーなどと思う

という質問を貰ったので。
ツイッターで書くには微妙に長くなりそう」なのと、ちょうどこの辺はどこかで書いておきたいなぁ、と思っていたので、おいちゃん流の雑管理のやり方を。

大雑把には
・バージョンをさほど気にしていなくてコンパイルオプションも気にしていないものは、パッケージ管理(yum)でインストール
・バージョンを気にしたりコンパイルオプションを気にしたりするものは、ソースコードでインストール
てな感じかな。

もうちょっと台数が増えたりしたら、Fabricつかったり(AWSなら)AMIつかったりBlue-Green Deploymentやったり、とか色々と妄想はしているのですが(笑
現状、そこまでの台数でもないので、とりあえずは手動でえっちらおっちらと。

昔は割と「がっちがち」だったのですが、時間とともに段々「緩く」なってきました(笑
っていう流れとともに。

昔々は「全部、ソースコードinstall」でやってました。
教えてくれた人の流儀なんだと思うんですが、 /usr/local/srcではなくて、/usr/srcにコードを置く事が多かったですねぇ。

install先ですが。
単純なライブラリとかはそのまま make installだったのですが、例えばapacheとかは
・ベースは /opt
apacheなら、例えば /opt/apache/apache-2.4.41 などの「バージョンが入っているディレクトリ」にinstall
シンボリックリンクで、最新のディレクトリを /opt/apache/apache_current
に入れてます(ました)。

これの最大の利点は、まぁ上述にもあるとおり「バージョンがすぐに戻せる」事。
apache止めて
シンボリックリンクを「一つ前の古いディレクトリ」に指し直して
apache動かす
で戻せるので「楽」だってんで、昔、教えてくれた人から教わったので、割とそれをずっと使ってました。

古いバージョンは……一応建前としては「使わなくなったものは消す」のですが、ディスクが圧迫していなければ、あんまり消さなかったなぁ。
10世代超えると邪魔くさいんで、少し消してましたが。

そこから少し時は流れて。

いやまぁ例えばぶっちゃけgccとかlibtoolとかcmakeとか「そこまでバージョンをタイトに追ってない」かつ「コンパイルオプションを基本あんまり使わない」もの、ってぇのがありまして。
この辺はまぁ、yumなりrpmなりdnfなりapt-getなりaptitudeなりを使って「楽をしてもいいんじゃないかなぁ」ってのが、おいちゃんの最近の見解。
なので「どこまでをパッケージ管理で入れるか」には多少の議論はあろうかなぁ、と思うのですが、「どこか、までは」パッケージ管理を使って、ある程度楽をしています(笑

で、例えば典型的にはPHPあたりは、おいちゃんは「ソースコードでinstall」しています。
PHPの場合は
・最新のバージョンを追いたい
コンパイルオプションを割と色々と付け足したり変えたりする事が多い
から。
似たような理由で、apache(+apr-util)、nginxあたりはソースコードinstallが多いです。
MySQLも多かった……のですが、最近、お仕事だとRDSとか使う事が多いので、installする事が減ったなぁ。まぁ「なんかで使う」なら、ソースコードで入れますが。

この辺は
・/usr/local/src なり /usr/src なりに、tar ball持ってきて
・configure / make / sudo make install する
ってやりかた(MySQLはcmakeになったよねぇ、とかいう細かい話しはおいといて)。

configureのオプションを残しておきたいので、おいちゃんは
php7_ccc
apche_ccc
nginx_ccc
mysql_ccc
みたいな形で、ファイルで残しておくようにします。そうするとlostしない(笑

で、万が一戻す時は
・戻したいバージョンのディレクトリで sudo make install
する感じかな。単純。

configとかは、メンテナンスバージョンくらいでは大してかわらないので。とはいえまぁこれも教えられた流儀があるので
・cp -p で「日付付き」でバックアップファイルを作っておく
・編集したらdiffコマンドで「意図しない箇所を変更していないか」を確認
ってやるので。もし「configを変えた」ら、それも戻せばOK。

……でまぁ、こんなやり方なので、最近「インストールディレクトリもバージョン番号付き」にする、のは、やらなくなっちゃいましたねぇ。

んで。
> 使わなくなった lib とか includes が残留していくけど、キニシナイ感じなのねー?
このやり方をすると、includesは「/usr/local/src 側」に残るはずなので、あんまり気にしない。
もし「別のプロダクトのincludeを参照する」ような場合は、例えば
ln -s /usr/local/php-7.2.24 /usr/local/php-7.current
とかってやって「シンボリックリンクを -I オプションに指定する」とかやると、普通に楽。

libについては「そこまで散らからないだろう」と思っているので、「キニシナイ」感じかなぁ。
昔ならともかく、今「サーバのファイルの最後の1つまで全部把握」は若干しんどいので(笑

最近は、こんな感じかなぁ。
これでとりあえず「差し障りない」くらいの管理は出来ているので。「もっとすっげぇ楽」とか「差し障る」とか出たら方針を変えそうですが、しばらくはこんな感じでやってます。

一端、メモ

時々、使うし使いたくなる台詞なんだけど「どこにあったっけ?」と「正確な文章を覚えていない」がちょいちょいあるので、めも。

P165

努力と技術で無理を通すのは我らの誇りだが
本当に無理なことは無理なので無理だ

C/C++のアドレス演算子、と、golang

ちょいと別所(身内のSlack)で「golangの参照渡し周りのお話」が出ていたので、その前提知識……が「長くなりそう」なのと「一部、もしかしたら役に立つかも」なの*1等々、ありまして、こちらに。
お話がgolangなので&おいちゃん、golangは「一定以上に評価しているし」「興味あるし」「がっつりやりたい」んだけど、まだ出来ていないので、まずは簡単なテストコードを書いてから。

package main

import (
        "fmt"
)

func hoge(n int, pn *int) {
        fmt.Printf("\nin hoge\n")
        fmt.Printf("\t n: %p\n", &n)
        fmt.Printf("\tpn: %p\n", pn)
}

func main() {
        var n int = 10
        var pn *int = &n

        //
        fmt.Printf("\t n: %p\n", &n)
        fmt.Printf("\tpn: %p\n", pn)
        //
        hoge(n, pn)
}

実行

[gallu@nnnnnnnnnn go]$ go run t1.go
n: 0xc000016088
pn: 0xc000016088

in hoge
n: 0xc0000160a8
pn: 0xc000016088

うん、予想通り「C/C++と一緒」だわ。

ってなわけで、比較的思う存分(笑

まず先に。
golangは詳しくないので「もしかしたらあるかもしれない」のですが、現状見ている限りだと「golangには参照渡しはない」と思われます。
あるいは「参照の値渡し」。

参照渡しは、例えば「C++」には存在していて。

#include <iostream>

// "参照"渡し
void hoge(int &i) {
    i++;
}

// "参照の値"渡し
void foo(int *i) {
    (*i) ++;
}

int main () {
    int i= 0;
    hoge(i);
    std::cout << i << std::endl;
    foo(&i);
    std::cout << i << std::endl;

    return 1;
}

hogeは「参照」そのものを渡しているので、callしている方も「なんも気にせずに変数を渡している」のに、なんでかいつの間にかそれが参照になっちゃってる摩訶不思議*2
一方で、fooは「ポインタ(参照)」のアドレスを「値として」渡して、関数のなかで「デリファレンス(間接参照)」しているので。callしているほうは「変数」じゃなくて「変数のポインタ」を意図的に渡す必要があるし、受け取ったほうもそれは「ポインタを受け取った」だけなので、中身をいじりたければ「デリファレンスしてあげる」必要がある。

いやまぁこの辺「いいじゃん面倒だし参照渡しって言っても」とか個人的には思ったりするのですが(それで弊害が起きた経験がないので)。
ただまぁ、細かくこの辺を気にされる御仁もいるので、一応軽い突っ込みというか前置きを。

で、前置いた上で。
golang、見ている限りだと「参照渡し」が見当たらないので、なんとなし「参照の値渡し」しかないのかなぁ? と思ってます。
まぁその辺は有っても無くても以降のお話にそんなに大きく影響しないのですが、一応。

んで、本題。
CでもC++でも、参照(の値)渡し(以降面倒なんで「参照渡し」に統一)をしたい時って

1. 値渡しにすることによってパフォーマンス影響があるか
2. その関数内で構造体の値を変更したいか

って辺りかなぁ、と、その辺はlocalのSlackで出ていた所と、そんなに変わらず。

まず「2番」について。
これ、本当に言語によって是非が大きく変わると思っていて。
Rubyが顕著なような気がしているのですが、この手の「引数の値を呼び先で変更する」のを「破壊的メソッド」って呼称しているようです*3
あちこちの文章を読んでいるに、基本的にどちらかというと「ほんのり否定的(注意しろ、警戒しろ)」な文脈が多いように見受けられます。

まぁ、おいちゃんの最近の主戦場はPHPですが、PHPでも引数に&を付ける事で参照渡し*4が出来ますが、基本的に「すんな」って言うことが多いから、まぁ、えぇ。
この辺は「プログラムの安全性」とか「デバッグ時の問題の見つけやすさ」とか、色々あります……そこが少し極みまでいくと「参照透過性」てな話しになってきます*5

一方で、例えば特にC言語*6の場合。
メモリとかメモリとかメモリとか、色々とありまして「わざわざreturnだけのために引数と同じメモリ領域を改めて確保とかしてられっかい」とか「その動的に確保したメモリ、どこでどうやって解放すんだよ?」とかあるので。
割合と
・呼び元で「戻り値を入れるメモリ空間」をあらかじめ確保
・引数に「戻り値を入れる変数へのポインタ」を渡す
・関数をcallして値を入れて貰う
・値を使う
・「戻り値を入れる」変数のメモリを解放
なんてのをちょいちょいとやってました……今は違うのかなぁ??
上述みたいなケースだと「その関数内で構造体の値を変更したい」ってか「変更しなきゃいかん」ので、そもそも変更が必須になるので「まぁポインタ渡すよねぇ」と。

さて、割と気になるのが1番。
先に主戦場(PHP)の話しをちょろっとしておくと。
PHPで"パフォーマンス目的"で参照渡し、は、ゼッタイにすんな」以上終了。パフォーマンスは「むしろ落ちる」ので……なんで落ちるかは、暇があったら書きます(昔どこかの原稿で書いた記憶があるんだが……)。
必要になる知識エリアとしては「copy-on-write(コピーオンライト) https://www.php.net/manual/ja/internals2.variables.intro.php 」あたり、になります……「記事書け」って意見があったら言っていただければ、改めて、書きます。

CとかC++の場合。
幾分状況次第ってのもあるんですが「パフォーマンス目的、は、割とある」と記憶をしています。
この辺でおいちゃんの記憶に鮮烈なのが、C++での事なのですが。

まぁ故あって(それ自体は仕方が無い)、割とでかいstringクラスのインスタンス、があって。
あちこちのメソッドに引数として渡されていって処理をして回っていたのですが。
それが、全体的に

std::string hoge(std::string s)

って感じの引数/戻り値で構成されていて……端的にはご依頼が「いわゆるC++で組まれたCGIなんだが、アクセス数が多いとメモリが足りなくなってサーバが重くなったり落ちたりする」というご依頼で……さもアリナミン
全体的に

void hoge(std::string& s)

って書き直したら割とあっさりと落ち着きましたよ、というのが、多分、一番この手の話し的には「印象の強い」お話かなぁ、と。

そりゃそうだよ関数呼ぶたびにがっつんがっつん、インスタンスcopyしてるんだもん。しかも「呼ばれた関数の中で別の関数を呼ぶ」とかってネストで出来てたから、そりゃたっぷり山盛り特盛りつゆだくだくにメモリ食いますってば。
この手のシチュエーションの場合、確実に「パフォーマンス(もっと具体的には"メモリを無駄食いさせない"という意図)」目的で、参照渡しをチョイスします。

ちな一瞬余談。
「パフォーマンスのために参照渡ししているけど中身は変えて欲しくない」場合、C++だと

void hoge(const std::string& s)

とかやって「変えるなよ?」と釘をさせるのが便利でねぇ……PHPにこの機能入らないかしらん? 割とマジで。

閑話休題

本題のgolang
大体C/C++と同じように動くようなので。特にでかい構造体とかを「そのまま渡す」とがっつりcopyが走りそうなので、それを考えると「チューニング目的としての参照渡し」は、golangはそこそこ「タイトな要求/環境で動かしたい」言語だと思われるので、普通に「あり」なんじゃないかなぁ、と思われます。
C++の引数にconst」相当の機能は……今、斜めに見た限りだと、golangにはなさそうだなぁ……この辺は紳士協定なんかし?

んで、パフォーマンスを前提にした場合。
例えば「intの変数を参照渡し」しても、うまみはないです。理由はわかるな?
charとかもっと無意味だからな? わかるな?
最低でも「ポインタで扱われている型、以上のサイズを持つ変数」に対して行うようにしましょう。……まぁ「構造体/クラスインスタンスの時にやる」くらいでいいんじゃないかなぁ? 判断基準としては。

まぁそんなこんながあるので。
要約すると「パフォーマンス目的での参照渡し」は、言語によって異なるけど「golangならあると思われる」でございます。


あと、ついでに1つ。
この辺の記事書くのに調べてて面白いと思ったのが「golangではスタックとヒープを気にする必要が無い https://qiita.com/rookx/items/a1e3d057a0ed71424094 」んだそうでございます。
へぇ。

端的には
・関数内で普通に宣言された変数が置かれるスタック領域:メモリの確保と解放が楽なんだけど使用制限あり
・動的なメモリ確保とかで置かれるヒープ領域:自由に使えるけどちょっとだけ(内部構造含め)面倒
ってのがあって。

C/C++でわかりやすいのが、これ。

#include <stdio.h>

int *hoge() {
    int i = 100;
    return &i;
}

int main() {
    int *pi = hoge();
    return 0;
}

[gallu@nnnnnnnnnn go]$ gcc t2.c
t2.c: 関数 ‘hoge’ 内:
t2.c:5:5: 警告: 関数が局所変数のアドレスを返します [-Wreturn-local-addr]
return &i;
^
[gallu@nnnnnnnnnn go]$

なんでかってぇと
hogeの中の変数iはスコープが「hogeの関数内」なので、関数を出たら「どうなるかわからない(普通に考えて、大いなる混沌に戻される)」
・のに、それのポインタ返すとか「あんたバカァ?」@惣流・アスカ・ラングレー

なんだけど*7。どうもgolangは「これが出来る」らしい。
コードは前述のブログにあるので、略。

まぁこの辺が出来ると
・呼び出された関数の側で(がっちょりした)構造体とか作って
・戻り値として、(チューニング目的もあるから)「構造体のポインタ」を返す
ってコードが「言語として保証されたレベルで」普通に書けるので、便利は便利だと思うんだよねぇC/C++から来た人は、しばらくモニョるような気がするんだけど。

とりま、やりたいやりたい思ってたgolangを、ちょろっとでも調べられるよい機会だったので、割とこりこりと調べて書いてみましたよ、っと。
とはいえ、C/C++も幾分久しい(のと実務の経験としては古い)、golangはド素人同然、なので。突っ込みどころがあったら、優しく突っ込んでいただければ幸いでございます。

*1:とBlogの良いネタになるw

*2:別に不思議じゃない

*3:本質的に「どの言語でもある」はずですが、ググると、破壊的メソッドの上位にはRubyの話題が多いんですよねぇ

*4:PHPは本当に参照渡し

*5:個人的には「ある程度、わかる」

*6:とはいえ、おいちゃんがお仕事でCをガリゴリ書いてたのっていい加減15~20年くらい前なので、色々と加減算してくださいませ

*7:この辺が「変数iがスタック領域に存在しているあたりだったりもするんだけど、説明は省略

TRPGでPvP?

元ネタはこちら
https://twitter.com/tokutomohide/status/1184753614012567553

TRPGって興味深いなぁ~と思うモノの一つがPvPボードゲームやカードゲームでのPvPはいいのにTRPGはダメって人も多いと思う。
あれってなんでなんだろうねぇ。

とりあえず定義として
PvP
https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E5%AF%BE%E3%83%97%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC

2人以上の(人間)プレイヤー同士で行われるゲーム内戦闘

としておく。
……「プレイヤーじゃなくてキャラクタだろ?」って突っ込みは、一端おいといて*1
「キャラクタ同士が、手をつなぎ合って協力するのではなく、敵対して場合によっては相手を殺害するまでを視野に入れた行動を取る」くらいの感じ、を想定。

おいちゃん的には
・システムとメンツによる
ってのが最終的な見解なんだけど、つまり「システムとメンツでいくつかの条件がそろった時はOK、それ以外はNG」って考える事が多いかなぁ。

PvPの場合、多くの場合において「勝ち負け」が発生して、勝ったほうはいいんだけど、負けたほうに禍根が残りやすいので。
また、どちらかというとTRPGは「勝敗を決める」ゲームであるというよりは「協力してナニかを行う」と考えている人のほうが多いと思うので……それはつまり「誰も負けない」を、想定したり想起したり前提にしていたりする、事が多いので。
主に敗者に対する「なんらかの配慮」がない限り、PvPは「事故につながる可能性が高かったり、一因になったりしうる」ので。

その辺りから「一定の前提条件が満たせている環境」以外では、PvPは「禁止、NG、御法度」と言われることは、少なからずあると思うし、おいちゃんもそれにはtrueを感じるかなぁ、と。

かみ砕いて。
まず、システム的に「ほぼNGであろう」と思われるTopが、D&D系列。
あれでPvPとかされてもメインのダンジョンがほぼ攻略できないと思われるので、チガウと思う。

それ以外だと……後は「メンツによる」んだけど、(ライト)ファンタジー系なんかは、比較的「条件のそろうメンツが集まりにくい」ので、割と忌避する可能性が高いかなぁ。
……いやまぁGURPS ルナルで「ド派手なセッション」もやったから、「無理」とは言わんが(笑

普段おいちゃんがやる*2システムだと。

・深淵:普通に発生しうる
シャドウラン(第2版):普通に発生しうる
・ヴァンパイア:ザ・マスカレード:普通に発生しうる
ソードワールドRPG(文庫版/完全版):滅多にない
GURPS ルナル:滅多にない
央華封神RPG:ない

こんな感じ、かなぁ。

内訳。

・深淵:普通に発生しうる
おいちゃん、いわゆる昨今の言い方だと「渦型」と呼ばれる類いのセッションが好き*3なのと。
テンプレートは「全部OK」なので。
……「白馬を連れた娘」と「奇妙な旅人」とか1パーティにいたら、大概、以下略、でしょ?(笑
その辺は「選んだテンプレートによっては起きうる」事を前提に「それがOKな人」って募集をするかなぁ。基本。

シャドウラン(第2版):普通に発生しうる
「仲間、と呼ばれる人達が、信用できるんならチームカルマも差し上げますし、チーム組んで頂いてよいですよ」って発言をする(笑
なお、情報は基本「全部伏せて、紙に書いて個人に手渡し」をする、ので。……特においちゃんが望んでいる訳ではないが*4、情報を隠したりごまかしたりバミったり詐欺ったり、色々(笑
勿論「組める相手とは組む」ものですが、同時に「組むに足らない相手であれば食らう」事も、ちょいちょいと拝見したり拝聴したり拝聞したり(笑
その結果としてのPvPは、そりゃ「あり得るよねぇ」と、しか。

・ヴァンパイア:ザ・マスカレード:普通に発生しうる
クランにもよるし立ち位置にもよるのですが。状況によっては「PvPも起きうる」よねぇ、と。時々、真っ二つに割れますしw
まぁ「PvPしてる余裕がない」事も往々にあるので*5、「PvPの前にまず保身」って流れでPvPがお流れになる……事もあるし「保身のために仲間を売る」事も、ほら、ねぇ(笑

ソードワールドRPG(文庫版/完全版):滅多にない
たま~~に「PvPがおきそうな」シナリオも組みますが。
どちらかというと「みんなで仲良く協力して」のほうがシナリオ的にもシステム的にも組みやすいので、滅多にやりませぬ。

GURPS ルナル:滅多にない
身内の「500cpセッション」とかいう機知外セッションは除外して。
通常、コンベでやるような時だと「穏当に」組むので、PvPは想定してないですねぇ。

央華封神RPG:ない
システム的に「悪いことをしたら落ちる」ので、やらない、し、基本「極めてやりにくい」と思う。

こんな感じ。
………うんシステムが悪いよねぇおいちゃんは悪くない*6

で。
PvPが「出来うる」システムにおいて、後は「メンツ次第」。

まず
・キャラクターとプレイヤーの分離が出来ないタイプがいたら、NG(やらない/やらせない)
ここは絶対。それが出来ない人は、PvPは間違いなく「止めたほうがいい」。

そこを前提にして、あと最低限としては
PvPをしかけるからには、自分のキャラクタが殺される覚悟がある
事が、必須。「自分は仕掛けるけど他人が仕掛けてきたら怒る」タイプは、やっぱり駄目。

で、あとは
・それなりに理由とか筋とかがあるPvPをする事
ってのが、以外と外せないライン。
「ボクのキャラクタは通り魔のように発作的に無差別に人を殺します」ってキャラにしたら、まずそもそも「今ここにいるかどうか?(捕まってないかどうか?)」で1ハードルあるし、さらに「そんなキャラであれば警察機構含めてあちこちから警戒されているであろうからほぼ確実に動向は見張られてるよねぇ」って話しになるので、多分、セッションは困難だと思う。
そんな風に「なぜ、その対象とPvPしたいのか?」について、相応に筋と理由が通せないと、PvPは厳しいのではないかしらん。

最後に
・卓の全員が「PvPあり、でよい」って合意が取れること
が、重要。まぁ「マスターがPvPありきって今日は考えてる」んならマスター紹介でそのように言うけど(それで入ってきて「PvPはイヤだ」言われてもさすがに知らん)。
そうでない場合は「卓の全員の合意」が取れない限り、原則として認めないかなぁ。TRPGは「みんなで楽しむ遊び」だから。

この辺の条件が満たせるのが、下限。
……ただまぁ「PvPをやりたい」って人の何割かが、ここに引っかかったりするので、色々と難しい。

後は、出来うる限り望ましいのが

・演出がちゃんと出来ること
これは「PvPそのものの演出」だけではなくて「いつ、そのキャラクタを屠るか」のタイミングとか、その辺を含めて。
やっぱり理想は「クライマックスで殺る」が、一番、楽しいよねぇ(笑
後は「戦闘はするけど逃がす」とか*7
そういった「相手の事もちゃんと配慮する」行動が、TRPGを楽しくする秘訣だと思うの。

………まぁ書いていてなんだけど、結構山盛りの前提条件が必要なんだよねぇPvPをやろうとすると。
で、「見知った、気心の知れたメンツ」でやると、勿論それは「とても楽しいひととき」になる可能性、が、山盛りであるんだけど。
そうでない場合、これだけの前提条件を「初対面*8」でどれくらい満たせるか? ってのが、ちょっと厳しいかなぁ、と。
「わかっているつもりだったけどいざ自分のPCが殺されたら実は理解してなくて納得してなかったでござる」とかも、あるしねぇ。

という感じなので。
「(PvPが)TRPGはダメって人も多い」のは、そんな辺りが、理由の一つなのではないかなぁ? と、おいちゃんは愚考してみたり。

「あんた、コンベでちょいちょいやってるじゃん*9」とか言うなちゃんと色々配慮はしてるんだ(笑
まぁ「PvPありあり」って言って、本当にPvPに発展するの、半分以下くらいの確率だしねぇ。基本的には「PvPしている暇を与えない」ので、皆さん、生き残る事に必死でいらっしゃいますしwww

……なんて話しをツイッターで書こうかと思ったのですが割と長文になったのと「せっかくだし残しておくかねぇ」と思ったので、雑文を、ちょろりと。

*1:本当に「プレイヤーvsプレイヤー」なら、それは警察案件になるので(笑

*2:……やる、って言わせてくれ……まだやる気は山盛りであるんだ orz

*3:というかそれしかやらない

*4:アジテーターはこういう物言いをするよねぇw

*5:by おいちゃんのセッション

*6:責任転嫁

*7:その後は、嬉し恥ずかし 敵対関係w

*8:コンベだと、初対面さん多いよね

*9:おいちゃんがマスター

日付関連の小ネタ

片や。
PHPでstrotimeは割とよく使われる関数かなぁ、と思います(DateTimeクラスのコンストラクタに渡す引数、と読み替えてもよいです)。

片や。
善し悪しはとりあえず置いておくとしまして、MySQLを使っていると「0000-00-00」というブツが出てくることは、まぁ時々*1、あります。

んで。

<?php

$t = strtotime('0000-00-00');
var_dump($t);
echo date(DATE_ATOM, $t), "\n";
echo "\n";

$t = strtotime('0000-00-00 00:00:00');
var_dump($t);
echo date(DATE_ATOM, $t), "\n";
echo "\n";

$date = new \DateTime('0000-00-00');
echo $date->format(\DateTime::ATOM), "\n";

それを食わせてみると

int(-62170016400)
-0001-11-30T00:00:00+09:00


int(-62170016400)
-0001-11-30T00:00:00+09:00


-0001-11-30T00:00:00+09:00

なんとも味わい深い出力が得られるんですねぇ……エラー時、strtotimeならfalse、DateTimeなら例外を出して頂ける事を期待するんですが、その辺も特に出てこない orz

ちょっと(ある意味)興味深い事例だったので、めも。
………お願いすなおにエラーにして orz

*1:しょっちゅう?