がるの健忘録

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

「ジャッジをしない(するのが面倒くさい)」という選択

発端は、PHPで割と定期的に見る、こんなコードに起因したお話でございます。

if (!hoge()) {
    処理
}

まぁ関数でも変数でもよいのですが。
この辺を「良し」とする勢と「悪し」とする勢がいる、と認識をしておりまして。
おいちゃんは「良しとはしない勢(悪し勢の弱い目のあたり)」に生息をしております……んですが、「なんで?」ってな辺りのお話でございます。

まぁご大層な理屈とか理念とか理想とかがある、って分けでもございませんで。
端的に片付けると「ジャッジするのが面倒だから」というものぐさ精神の賜物、でございます(笑

もうちょいとかみ砕きまして。

この「ifの中を"論理演算子の否定"だけで書く」のが確実にまずいケースの典型例としては、strpos()があろうか、と思います。
この辺は最近あちこちでとやかく言われていると思われるので、説明は略。

$s = 'abcdef';
if (!strpos($s, 'a')) {
    echo "a is not find.\n";
}

あと、ちょっと思いつかないのですが、戻り値が「string|false」な関数なんかも「文字としての"0"がかえってくる」とアウト風味が満載でございます。

function f() : string|bool {
    return '0';
}

if (!f()) {
    echo "f() is false\n";
}

まぁ「空文字とか空配列とかNULLとか」がfalseになるくらいは、暗黙の変換でも「許容範囲かなぁ」と思わなくもないんですけど、……ねぇ。
この辺の「これの戻り値どうなんだろう?...暗黙の変換どうなるんだろう?」を毎回確認するくらいなら「暗黙の変換やめて明示すりゃよくね?」とか思ってきたりするわけですよ。

書く方は一回だけど、読む方は下手すりゃ毎回だったりしますしねぇ。

この辺の根っこに「決断には、案外とMPがかかる」って認識があるです。
例えば、こちら。
https://www.workport.co.jp/plus/articles/7576

あと、間違ったコードは間違って見えるようにする( 今、キャッシュしかないんですよねぇ…… https://web.archive.org/web/20190317182904/http://local.joelonsoftware.com:80/wiki/%E9%96%93%E9%81%95%E3%81%A3%E3%81%9F%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AF%E9%96%93%E9%81%95%E3%81%A3%E3%81%A6%E8%A6%8B%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B )的な事も少し想起しています。

なので。
おいちゃんが「if文の中は必ず条件式。論理演算子(否定)を書かない」のは「戻り値がどうなってその結果のbooleanへの暗黙の変換がどうなって...」って考えたり「この戻り値はOKで...この戻り値はNGだから...」とかいう決断をしたりするのが面倒だから、になります(笑

PHPで「型宣言された引数の型の名前」を知る方法 続編

PHPで「型宣言された引数の型の名前」を知る方法 - がるの健忘録 の続編。

いやPHP8になって「union typesとか増えたしなぁ」とか思って検証コード書いてみたので、勿体ないんで、忘れる前に(絶対忘れるだろうしw)、記録しておく。

<?php
namespace Foo;

//
class Bar {
}

// 調査用クラス
class Hoge {
    public function t(\Exception $e, Bar $ar, $s, int|float $i, mixed $m) {
    }
}

/*
//
$ref = new \ReflectionMethod('Hoge', 't');
//
$params = $ref->getParameters();
//
foreach($params as $p) {
    echo $p->getType() , "\n";
}
*/

// 一行にまとめてみるとこんなん
foreach(( new \ReflectionMethod('Foo\\Hoge', 't'))->getParameters() as $p) {
    echo $p->getType() , "\n";
}

結果

Exception
Foo\Bar

int|float
mixed

ほむ、概ね「すげぇ素直」にかえってくるんだ。
クラスは流石に完全修飾クラス名だけど。

NIZの呪文変換

似ザードリィとか呼称されております(笑)「NIZ」の呪文変換のCSVです。
指針としては
・おいちゃん、PC88の頃からやってるので、英字スペル
・サンダーを「MOLITO」とするお話もあるんだけど、記憶が間違ってなければ「MOLITOは1グループ」なので、サンダーはそのまま残す
ってな感じでございます。

# メイジ
スリープ,KATINO
スパーク,HALITO
ウィザードアイ,DUMAPIC
シールド,MOGREF
ダークネス,DILTO
メガシールド,SOPIC
メガファイア,MAHALITO
トータルテラー,MAMORLIS
フリーズ,DALTO
ギガファイア,LAHALITO
サフォケーション,MAKANITO
メガフリーズ,MADALTO
デッドリーエア,LAKANITO
ギガシールド,MASOPIC
シックスブーンズ,HAMAN
ターンアンデッド,ZILWAN
テレポーテーション,MALOR
セブンスブーン,MAHAMAN
ニュークリアブラスト,TILTOWAIT
# プリースト
ヒール,DIOS
パニッシュ,BADIOS
アーマー,KALKI
ブライトネス,MILWA
バックラー,PORFIC
メガアーマー,MATU
ディバイントラップ,CALFO
ロケートパーソン,KANDI
パラライズ,MANIFO
サイレンス,MONTINO
マジックライト,LOMILWA
キュアパラリシス,DIALKO
ネームモンスターズ,LATUMAPIC
ギガアーマー,BAMATU
メガヒール,DIAL
メガパニッシュ,BADIAL
キュアポイズン,LATUMOFIS
プロテクション,MAPORFIC
ギガヒール,DIALMA
ギガパニッシュ,BADIALMA
ファイアウォール,LITOKAN
レイズデッド,DI
デス,BADI
ワールウィンド,LORTO
キュアオール,MADI
ティールライフ,LABADI
エヴァキュエート,LOKTOFEIT
ワードオブカース,MALIIKTO
レイズアッシュ,KADORTO

2021/04/07 18:21 追記
TILTOWAIT と LOKTOFEIT の綴りを間違えていたので修正しました orz
他にミスとかあったら教えてくださいませ ノ

「useを使ってクラスが使えるようになる」までの道程

ちょいとお仕事で、要約すると
・useすればクラス名の解決が可能になるのではないか?
 → 参照したいクラスがあるファイルのnamespace\クラス名をuseで指定すれば使用はできるはず
という趣旨の質問をいただきまして。

これはとても興味深い内容だなぁ、と思ったので「Blogで解説してよい?」と聞いたら快諾いただいたので、早速、ネタに(笑

今回書くのに近しいお話が
namespaceとuse https://note.com/gallu/n/nd4efd1c17e25
autoloadについて https://note.com/gallu/n/n2d0bb7718ceb
requireとinclude https://note.com/gallu/n/n658fbf6fd7ce
に乗っておりますので、よかったら。


結論から書くと
・ use は「別名(エイリアス)の作成をするだけでそれ以外は何もしてくれない」ので「なにもなくてuseだけだとクラスは(多分)使えない」
・クラスの解決には「クラスのオートローディング」という機能を使う
 → 自力で実装してもいいけど、まぁ「既存の実装」があるので、ルールに則りつつそれを使うと楽

となります。
ちょいとこの辺を、昔々のあたりから紐解いてみませう。

以下のクラスがあるとします。

class Hoge
{
}

で、以下のPHPがあった場合、これはHogeクラスを解決できます。

<?php

class Hoge
{
}

//
$obj = new Hoge();

まぁねぇ。
ただこんな風に「1ファイルに全部書く」とか色々と以下略なので、別ファイルに切り出します。

Libs/Hoge.php

<?php

class Hoge
{
}

t.php

<?php
//
$obj = new Hoge();

これだと解決できません。なんでかってぇと「t.phpを実行しても、Hoge.phpを読み込むような記述がどこにもないから」。
古(いにしえ)の術者は、こんな風に解決をしていました。

t.php

<?php
require_once('./Libs/Hoge.php');

//
$obj = new Hoge();

require_onceでもrequireでもinclude_onceでもincludeでもいいんですが(詳しい解説は上述の notes 参照)*1
こんな風に「ファイルを読み込んでやる」と、使えるようになります。
……えぇまぁなので昔は「先頭に100行とか200行とかrequire_onceが書いてある」なんてブツも拝見した記憶がございます。

まぁ「この辺は出来るだけ簡略化したい」と考えるのが優秀なプログラマというものでございます。
上述のnotesの「autoloadについて」を読んでいただくと手っ取り早いのですが。

https://www.php.net/manual/ja/language.oop5.autoload.php

オブジェクト指向アプリケーションを作成する開発者の多くは、 クラス定義毎に一つのPHPソースファイルを作成します。 最大の問題は、各スクリプトの先頭に、必要な読み込みを行う長いリストを 記述する必要があることです(各クラスについて一つ)。
spl_autoload_register() 関数を使うと、 任意の数のオートローダーを登録でき、 クラスやインターフェイスが定義されていなくても自動的に読み込めるようになります。 オートローダーを登録すれば、PHPがエラーで止まる前にクラスをロードする最後の チャンスが与えられます。

こんな手法がございます。

つまり、オートローディングの設定がないと
Hogeを使おうとする
・見つからない → エラー

な所が、オートローディングを使うと
Hogeを使おうとする
・見つからない → オートローディングをしてみる → それでも見つからないならエラー

となって、1クッション増えるわけなんですね。
クッションが増えるだけなので、「本当にどこにもそのクラスが存在しない」なら最終的にエラーにはなるのですが。

となると次は「オートローディングを、自力じゃなくて他人様が実装しておいてくれないもんかしらん?」となるのですが。
昨今のPHPにおいて、比較的高確率で「composer」を使っているのではないか、と思います。
で、もし「composerを使っている」とすると、composerにはオートローディングの実装があるので、こいつを借用する事ができます。

「public/index.php の最初の三行(vendor/autoload.php の挙動) https://note.com/gallu/n/ne1bcb25c412a 」を見ていただくと色々書いてあるのですが。
雑に書くと
・とりあえず、composerでインストールしたライブラリは、オートローディングが通用するようになる
感じです。

……だけだと「じゃぁ自作のクラスはどうすんだよ?」って話になるのですが。
実はcomposerのautoload.phpは、composer.jsonの['autoload']['psr-4']に「名前空間名 => ディレクトリ名」を書いておくと*2、こちらも自動で解決してくれるようになります。

なので、例えば上述であれば。
仮に名前空間を「\Gallu 」と仮定して

Libs/Hoge.php

<?php
namespace Gallu;

class Hoge
{
}

composer.json

{
(前略)
    "autoload": {
        "psr-4": {
            "Gallu\\" : "Libs/"
        }
    },
(後略)


t.php

<?php
require_once('./vendor/autoload.php');

//
$obj = new \Gallu\Hoge();

という風に書くと
・\Gallu\Hoge は、ない
・autoload.phpで登録されたオートローディングが動き出す
 → Galluって名前空間だからLibsを探してみる
  → HogeクラスだからHoge.phpってファイルを探してみる
  → あるから require する
・使えるようになった!!

という流れで、使えるようになります。
これがオートローディングの世界です。

まぁ「特殊なニーズがある」のであれば「自力実装を追加する」とかってのもあり得るんでしょうが、普通の用途ならまずは「あるモノを使ってみる」からでよいのではないかなぁ、と思います。
学習用途であれば一度は「自力実装」してみていただきたい所ですが。


じゃぁ改めて「useってなによ?」って話なのですが。
序盤にも書いた通り「別名の作成」となります。

かみ砕いて。

$obj = new \Gallu\Hoge();

ここ。
このまんま書くのであれば、useいらんです。
なお、\Gallu\Hogeを「完全修飾名」って言います。

これくらいならまぁまだ、とも思わんでもないのですが。
これが例えば

\Foo\Bar\Baz\Piyo\Fuga\Hoge

とかいうクラス名で、これが何カ所にも出てくる時に毎回毎回

$obj = new \Foo\Bar\Baz\Piyo\Fuga\Hoge() ;

とか書いていくのも、ちょっと「ぞっとしない」お話でございます。

こんな時に便利なのが別名。カタカナ表記だとエイリアス。これがuse。
こんな風に定義ができます。

use \Foo\Bar\Baz\Piyo\Fuga\Hoge as Hogera;

$obj = new Hogera() ;

こんな風に書くと「Hogera、って出てきたら \Foo\Bar\Baz\Piyo\Fuga\Hoge ってことで一つよろノ」って感じになります。
まぁエイリアスの名前通り、まんまの機能ですね。

さて。
わざわざ「クラス名と違う名前を毎回考えるのも面倒」だと思うので、割とこうなります。

use \Foo\Bar\Baz\Piyo\Fuga\Hoge as Hoge;

$obj = new Hoge() ;

これで目出度しとしてもよいのですが、「Hoge as Hoge」がちょいとウザい……で、実はこれが省略可能。

use \Foo\Bar\Baz\Piyo\Fuga\Hoge;

$obj = new Hoge() ;

スッキルする、と共に、多分見慣れた構文。
こうやってuseは「別名」を付ける機能を持ちます、が、一方で「クラス存在の解決」にはなんにも関わっておりませんので、従って「useだけ、だと、クラスは解決できない(かもしれない)」となります。


なので。
もし「ちゃんとuseしているのにクラスが解決できなくてPHPがエラーを吐く」のであれば、基本的には「所定の場所にクラスがかかれたファイルがない」か「そもそもそのクラスが存在していない」ので。
楽ちん順のやり方としては
・とりあえず find コマンドでファイルを探してみる(普通、 クラス名.php ってファイル名のはずなので)
・ちぃと時間がかかるかもですが、grepで「class クラス名」で全ファイルをあさってみる
で、対象のファイルが
・あるんだけど位置が違う
・ない
のあたりをつけておくと、次のアクションに繋がりやすいかなぁ、と思うです。

残りのお話は個別の内容になってしまうので、commonな内容としてはこの辺まで。
質問とかあったら、コメントにお願いいたします ノ

*1:まぁ普通、プログラムならrequire_onceですな

*2:正確には、書いた後 php composer.phar dump-autoload を実行すると

探索系アルゴリズム用のメモ

A* とか ダイクストラ法 とかが可視化されてるサイト。
こないだググったら案外うまくヒットしなかったので、メモ用に記録。

qiao.github.io

ロジック裏で把握するのも大事だけど、こんな風に可視化状態で見れるのもよいよね。

やっぱすげぇ……

元ネタ
Linuxを生み出したリーナス・トーバルズが考える「優れたコード」とは何か?
gigazine.net

とりあえず、すげぇ雑にコード書いてみた。
gccコンパイル可能( -std=c99 オプション、付けてちょ)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//
typedef struct IntListItem {
  int value;
  struct IntListItem *next;
} IntListItem;
//
typedef struct {
    IntListItem *head;
} IntList;

void remove_01(IntList *l, IntListItem *target) {
  IntListItem *cur = l->head, *prev = NULL;
  while(cur != target) {
    prev = cur;
    cur = cur->next;
  }
  if(prev) {
    prev->next = cur->next;
  } else {
    l->head = cur->next;
  }
}
//
void remove_02(IntList *l, IntListItem *target) {
  IntListItem **p = &l->head;
  while( (*p) != target ) {
    p = &((*p)->next); // ちょっとだけ括弧を追記
  }
  *p = target->next;
}


int main() {
  //
  IntListItem *wk_before;
  IntListItem *wk_after;
  IntList l;
  l.head = NULL;

  // 作成
  for(int i = 0; i < 5; i++) {
    wk_after = (IntListItem *)malloc( sizeof(IntListItem) );
    wk_after->value = i + 1;
    wk_after->next = NULL;
    if (l.head == NULL) {
      l.head = wk_after;
    } else {
      wk_before->next = wk_after;
    }
    wk_before = wk_after;
  }


  // 表示
  IntListItem *wk;
  IntListItem *del;
  wk = l.head;
  while(wk != NULL) {
    printf("%d \n", wk->value);
    // 削除用のポインタを取得(後のテスト用)
    if (wk->value == 5) {
      del = wk;
    }
    wk = wk->next;
  }
  printf("\n\n");

  // 削除
  //remove_01(&l, del);
  remove_02(&l, del);
  // 表示
  wk = l.head;
  while(wk != NULL) {
    printf("%d \n", wk->value);
    wk = wk->next;
  }
  printf("\n\n");

  // 削除
  //remove_01(&l, l.head);
  remove_02(&l, l.head);
  // 表示
  wk = l.head;
  while(wk != NULL) {
    printf("%d \n", wk->value);
    wk = wk->next;
  }

  return 0;
}

スタンフォード大学の計算機科学講義「Computer Science 101(CS101)」で紹介されていたコード」がremove_01、ライナスさんのコードがremove_02。

とりあえずどっちも動く事を確認。

大学のほうのロジックは普通に理解。

ライナスさんのほう……あぁなるほど「targetにきたら"その次"にすっとばしてtargetを実質的に骨抜きにする」んだ。
あと「先頭の削除」の時は、初っぱなの処理が「List->head へのポインタ」だから、「*p = target->next」でそのまま書き換わるんだ。

うん「言われて見て熟考したらわかる」んだけど、これを「思いつく」のって、すンごいなぁ………
面白くもあり、高みが遠くもあり。

おいちゃんがプログラムで気にする所

まぁつまり「気にしないところ」も少なからずあるのですが。
「規約がある所では、書いてある範囲で規約を重視しましょう / 書いてない規約は"既存のコード"に出来るだけあわせましょう」とかいう最低限のお話とか、さらにもっと最低限未満のお話として「syntax errorとか脆弱性あるコードとか書いてくんなボケ*1とかあるのですが。

その辺は押さえた上で、じゃぁおいちゃんが「何を気にしていて」「それはなんでなの?」ってのを、自分自身の内面の整理を兼ねて書いてみようかなぁ、と。

多分、おいちゃんが一番気にするのは

YAGNI(You Aren't Going to Need it.):それ、要らないよね?
・DRY(Don't Repeat Yourself.):同じ知識を2箇所以上書くな

この2つ。
YAGNIのほうは割と簡単(な、はず)で、「まだ使わないけど使うかもと思ったので書いてみました」は大体ペケぽんの対象。
ただ、突き詰めていくと少しゴリゴリしてきて……その辺は

・KISS(Keep It Simple,Stupid):シンプルにしておけ

とも関連するんだけど、「いらんループがある」とか「無駄な中間データを作ってる」とか、色々と出てくる。
昔、とある友人のエンジニアさんから「おいちゃんのコード、なんでそんなに短いの?」って聞かれた事があって、その言葉がずっと気になってたんだけど、多分、この辺を意識していたんだと思う。

少しかみ砕くと。
多分、何割かの人は「簡単"に"コードが書ける」のをよしとしているように思うんだけど。
おいちゃんは「簡単"な"コードが書ける」のをよしとしているんだろうなぁ、と思う。

「簡単なコードを書く」のは、試行錯誤もあるし書き直しもあるし考察もあるし色々あるんだけど、その辺の手間はあんまり惜しまないかなぁ、的な。
アルルの女」あるいは職人の片思い( https://gallu.hatenadiary.jp/entry/2018/12/03/003823 )とか 職人がかける手間暇( https://gallu.hatenadiary.jp/entry/20081118/p1 )とか参照していただければ幸い。
そういう手間をかけるのか、それとも「とりあえず書けたし動いてるしいいじゃん」で片付けるのか、の違いなのかもしれない。

なので、その辺まで考えると、YAGNIとかKISSとかって「結構深いお話」になるんだけど。
その辺の深さを「どれくらいまで追いかけていくか」ってのは、とても面白いんじゃないかなぁ? と思うんだけどどうなんだろ。

とりあえずお仕事でコードを見る時は
・「同じ配列をなんどもぶん回してる」コードとかは一端気にする(無理でなければ、1つにまとめてもらう)
・「今は不要なコード」は削除してもらう
ってあたりが気になる所かなぁ。

で、次にDRY。……こっちのほうがより気にするかもしれない。
「同じコードを二度かかない」って勘違いするケースがあって、そこを起点にしちゃうと色々と「スカポンタンな異論が出てくる」。
そうじゃなくて「Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.(DeepL訳:すべての知識の断片は、システム内で単一の明確で権威のある表現を持たなければならない。)」なの。
この辺は DRYを勘違いしてたお orz( https://gallu.hatenadiary.jp/entry/20120410/p1 )を参照してくださいませ。

この話をすると

・PIE(Program Intently and Expressively):意図を表現して!!
・SLAP(Single Level of Abstraction Principle):抽象度は揃えろ!!

の話が絡んでくるんだけど。
「抽象度を揃えた状態で」「意図を表現して」「同じ意図の記述が二箇所以上あってコピペしている時」に、100tハンマーが飛び交います。

んで。この辺を前提にすると
・ここのコードもあっちのコードも「引数の10%の値を端数切り捨てでreturnしてる」から、1つにまとめよう
は、これがDRYにかなってるかは「わからない」んだよ。
だって「意図が見えない」んだから。

もしその処理が
・どっちも消費税計算
なんだとしたら、思考するまえに脊髄反射で共通化しつつ、コピペした人間を洗い出しましょう*2

でも
・片方は消費税計算
・もう片方は手数料計算
だとしたら。すくなくとも「SLAPに従って"処理を書いてある"レイヤーの実装」は、それは「混ぜちゃ駄目」なんだよ。
だって「消費税計算」と「手数料計算」なんだもん。混ぜてどうすんのさ?

だから、まず「SLAPとPIE」なんだよ。
PIEとちょりとずれるかもしれないけど「意図をコメントで書いてよ」なんだよ。まぁこれくらいなら「コメントじゃなくて関数名でちゃんと」でもよし。
んで、その「意図」が「同じ(=同じ知識の断片)」なのか「違う(=異なる知識の断片)」なのか、を判断したいし、しなきゃいけないんだよ。

ちな、じゃぁ「違う意図だけど同じ実装を書いている場合の、実装レイヤーをどうする?」ってのは、まぁ議論としてはあって。
つまり「抽象度を揃えて"一番具象のレイヤー"の実装」の場合。
おいちゃんなら

public function 端数切り捨て計算(float|int $元値, float|int $百分率)

とかって具象関数切って、とりあえず処理はここに集約するかなぁ。入り口は別々にして。
例えばこれで「手数料だけ端数切り上げ」になったら、切り上げ用の関数(メソッド)切って、そっちに処理移せばよいし。


この辺までは「1つのメソッドだったり処理だったりの書き方」の部分で。
ここからもう少し広域なお話。

基本的には

・単一責任の原則:1つのプログラムには1つのことをうまくやらせる@UNIXという考え方

を尊ぶので。
1つのクラスは「そのクラス(≒1インスタンスの意味)を20~30文字くらいで語れる」くらいの粒度で切るのが好きかなぁ。
なので「複数の役割(責任)が1つのクラスに渾然一体となっている」のは大変にお好まない感じ。
ただ「1つの責任の処理内容がそもそも割と膨大」な時は、程度問題でもあるんだろうけど「でっかくなっても、いいんじゃない?(とりあえず)」とは思う。
分解する必要性が見えてから分解すりゃいいんだし(ここもまた、レイヤーが違う YAGNI である。つまり「必要になってから分解する」)。

この辺が出来てくると、大分おちついてくるかなぁ。
後は、自分が書いたコードにしても人が書いたコードにしても、気になるところはあるか、と思うので。
コードリーダーとかにちゃんと相談してから、なんだけど

ボーイスカウトの規則:来た時よりも美しく

ってのも大事だと思う。
っつか定期的なリファクタ、大切だよねぇ。 明日になったら「明日」になるから、多分、綺麗にならない( https://gallu.hatenadiary.jp/entry/2019/04/17/220708 )辺りを参照していただければ幸い。

最後に。

・驚き最小の原則:自然な書き方にしようよ

ってのがあるんだけど、これ、割と難しいと思う。
おいちゃんは端的には「既存のコードを読んで出来るだけ"近い書き方"をする」ようにしてるかなぁ。
あと「同じ処理」の時は「同じ書き方」をするようにしてる。

ここから先「この言語だとこの書き方がよりよろし」とか色々あるんだけど、とりあえずざっくり、こんな事を考えながらコードをチェックしている気がする。
こーゆーのはまた色々、文章化してから後で読み返すと気づきがあったりもするので、一端まず、文面化してみませう。

突っ込み歓迎なので、なんかあったらコメントくださいませ。

*1:……どっちも実務上で実話 orz orz orz

*2:その後どうするかはしらにゃい