return true to winとは
2018/02/15にphpusers-ja.slack.comでバカウケした、「関数の引数に任意の値を入れ、関数の実行結果がbool trueになれば勝ち」というコードパズルです。
Level 1からLevel 11までの11問が配備されています。
開催会場はこちら => https://returntrue.win/
画面の見方
No. | 名称 | 説明 |
---|---|---|
1 | コード | ここに記載されたコードが実行され、その結果がbool trueであるようにします。 |
2 | 引数 | ここに任意の値を記載し、Submitを押して実行すると結果が判ります。 |
3 | Score | 引数に入力している文字数です。 |
3 | Well done! | 正解した後に表示されます。next stepのリンクから次の問題に進みましょう! |
5 | Your score: | 実行した後に表示されます。現在実行した内容のScoreです。 |
6 | Your best score: | 実行した後に表示されます。現在までにこのブラウザで実行した最良のScoreです。 |
7 | Absolute best score: | 全てのreturn true to winをプレイした人の中での最良のスコアです。 |
8 | Leve 1... | 他の問題へのリンクです。詰まったら先の問題を解いてみるのも良いでしょう。 |
ということで早速ネタバレを開始しようか
!!注意!!以降ネタバレ 一通り自分でチャレンジしてから見た方が良いです。
ここに書いてある回答はGistにまとめてあります。
理屈はいいから早く解答を知りたい人はGist見ちゃった方が早いでしょう。
まずはお上品な回答から
"Manners Maketh Man."(マナーが人を作る。)
NoticeとかWarningとか許されないよね!
Level 1
true
見紛うこと無きtrue。もはやどうしようもないくらいtrue。trueって、いいよね。
勝利条件がbooleanでのtrue
なので、'true'
とか1
とかfunction () {}
とかnew class {}
は許されません。
まぁ序の口ですよね。
Level 2
問題:関数を実行した結果がstring 'true'となる関数を渡すこと
function () {return 'true';}
地味にお行儀良く無くない?
'true'
と同じ型での比較なため、とりあえず 'true'
を返しておけばいい。
・・・んですが、まさか'gehr'
にそんな意味が。
Level 3
問題:intキャストしたら1以上1以下かつ、int 1ではないもの
'1'
大分判りにくいですが、分割してみると判り易くなります。
// $xをintにキャストして1以上
(((int) $x) >= 1)
&&
// $xをintにキャストして1以下
(((int) $x) <= 1)
&&
// $xがint 1ではない事
($x !== 1)
文字列の1はキャストすれば1なので、1以上、1以下を通過。
そして最後は'1'
と1
の比較になるので通過となります。
Level 4
問題:0 == $xとなること
0
case 0:
に意識が持っていかれるかもしれませんが、switch文は上から順に判定します。
なので、$a == $x
となる0を入れておけばOK。
とはいえこれPHPなんでfalse
でも'0'
でもnull
でも'0asdf'
でも動くんだよね・・・
Level 5
4444
そらまぁstrlenで文字列長を得るんだからこうなりますわな。
最短での回答は人によっては大変衝撃的。
衝撃の展開はのちほど。
Level 6
new class {public function __invoke () {return $this;}}
お上品に書こうとするとPHP7からの機能である、無名クラスを使いたくなる。
無名クラスとはその名の通り、クラス名の指定なしでいきなりnewできてしまうクラス。
クロージャーを使った関数の引き渡しのようにその場で生成したクラスのインスタンスを引き渡すかのような使い方ができる。
また、__invokeは地味に5.3.0から使えていた機能。
これはインスタンスを動的関数のように呼び出した場合に実行されるメソッド。
割とFrameworkよりの抽象処理を書くようになるとよく使うようになります。
@tadsan による別解。
$f = function () use (&$f) {return $f;}
最初こっちやろうとしていてリファレンス渡しの仕方間違えていて諦めたやつ。
2年前に案件で使ってたのにねorz
なおこの書き方はクロージャーで再帰呼び出しするという中々素敵な事が出来ます。
$max = 10;
$n = 1;
/**
* $max回$nに$nを足して返します。
*/
$result = ($function = function ($n, $i = 0) use (&$function, $max) {
return $max <= $i ? $n : $function($n + $n, ++$i);
})($n);
var_dump($result);
// $max回$nに$nを足して返します。
for ($i = 0;$i < $max;$i++) {
$n = $n + $n;
}
$result = $n;
var_dump($result);
/* 実行結果
* int(1024)
* int(1024)
*/
Level 7
問題:stdClassのインスタンスであり、配列にキャストした場合の0番目の要素がtrueであること
(object) [true]
なので、逆順で0番目の要素がtrueの配列をオブジェクトにキャストして返すだけという。
(object)でキャストするとその結果はstdClassのインスタンスになるからね。
設問がほぼ答えというやさしいせかい。
Level 8
問題:BarまたはBarを継承したクラスのインスタンスであり、クラス名がBarではないこと
new class extends Bar {}
そのまま new Bar
とかやりたくなるやらしー問題。
継承さえしてしまえば、get_classで取得できるクラス名は末端のクラス名になるので如何に継承するかがポイント。
Level 9
問題:数値として計算が可能でかつ、$yと型比較無しで同じ値とならないこと
false
booleanへの変換と型の相互変換を知っていて、かつ、前置加算子を知っていないと何を言っているのか判らなくなる奇問。
注意: 加算子/減算子は、数値や文字列にしか影響を及ぼしません。配列やオブジェクトそしてリソースには、何も変更を加えません。同じく NULLに減算子を適用しても何も起こりませんが、NULLに加算子を適用すると 1 となります。
分割した解説。
// false + 1の場合だと、加算演算子は((int) false) + 1として計算する。
// そのため0 + 1となる。
$y = ((int) false) + 1; // $y === 1
// $xには前置加算子がついているが、加算子は数値や文字列にしか影響を及ぼさないため、falseのままとなる。
// その際、$xはintにキャストされ、0として振る舞う。
// そのため、((int) false) != $yとなり、0 != 1となる。
return ((int) false) != $y; // 0 != 1
Level 10
問題:関数として実行可能でかつ、$objectのaメソッドの返り値をtrueにすること
function (&$x) {$x = new Bar(true);}
間違いなく最難関。
渡されるオブジェクトを書き換える以外に値を伝播させる手段がない上にプロパティのセッターが無いという極悪仕様。
おまけにアクセス修飾子がprivateなため継承してメソッドを変化させることもできない。
しかも関数定義でリファレンス渡しにしておかなければ、呼び出し元に変数の変化を伝えるすべが無いというね。
Level 11
[null]
お行儀の良い回答ならば余裕すぎる。
この問題の真価はAbsolute best scoreにある。
次いで手段を選ばない!お行儀が悪くても構わない!絶対の!良い!スコア!
Level 1
!0
否定論理演算子とbooleanへの変換と型の相互変換の合わせ技。
数値の前の否定論理演算子が付くことでboolにキャストされ、それが論理NOTで反転されbool trueになるという仕組み。
分解するとこうなる
$x = (bool) 0; // ここでfalseとなる
$x = !$x; // falseが反転してtrueとなる
この先地味に多様されるテクニックなのでここで覚えておいてください。
同僚が45秒くらいでさらっと回答していて愕然とした問題でした。
Level 2
str_rot13
@tadsan のヒントにより到達。
お行儀の良い方でも書いた通り、'gehr'
に意味があった。
まさかのrot13変換とは・・・
ROT13について詳しくはこちら。
Level 3
!0
Level 1と同じなので解説なし。
Level 4
0
お行儀の良い方と同じ答えなので解説なし。
Level 5
🌸
サクラサク(2000年にそんな名前の曲あったな
さて、ここからぼちぼちダーティになっていきます。
この入力だとそのような名前の定数はない!としてNoticeになっちゃうんだよね。
まず、PHPの定数は文字またはアンダースコアで始まり、任意の数の文字、数字、 アンダースコアが後に続く文字列が定数ラベルとして認識されます。
正規表現で示すと次のようになります。 [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*
なんとUTF-8で書かれた文字列もラベルとして認識されます。とことんまでやるならこんなすっごーい!事もできます。
また、未定義の定数を使用した場合、ちょうどstringとして コールしたかのように(CONSTANT vs "CONSTANT")、 PHPはその定数自体の名前を使用したと仮定します。
この際、E_NOTICE が発生します。
そのため、実行時点ではdefine('🌸', '🌸');が先に実行されたと仮定した状態で処理が行われます。
そして、🌸は4バイトのUTF-8文字(U+1F338)です。
判定に使用しているstrlen関数は文字数ではなく、バイト数を数えます。
そのため、この答で必要な4を得る事が出来ます。
ちょっとだけ興味深いのがこのreturn true to winを作った人が非日本語圏の人だということ。
ここ数年で絵文字がemojiとして世界中で使われるようになり、そのためマルチバイトについての見識が世界中で広まっているようです。
Level 6
($x=session_id)($x).$x
この短縮方法はCLIでは動きません。
session_id関数の挙動を良く知っていないとたどり着けない境地。
分解するとこうなる。
$x = session_id; // 関数の動的呼び出しのために関数名の文字列を代入 Level 5での定数の自動定義テクニックを使い併せて文字列を短縮化
$x($x).$x; // session_id('session_id').'session_id'として実行し、セッションIDを'session_id'に変更。
// session_id関数は現在のセッションIDが存在しない場合空文字を返すので、最終的に''.'session_id'が実行されていることになる。
$x === $x(); // ひとつ前の行でセッションIDが'session_id'として設定されているため、$x()は'session_id'を返す。$xはそのまま'session_id'なのでtrueとなる。
Level 7
(object)[!0]
trueの表現を短縮化しただけ。
Lavel 8
new class extends Bar{}
削れる空白を削っただけ
Lavel 9
a
intにキャストして0になれば何でもOK。
文字列の最初の部分により値が決まります。文字列が、 有効な数値データから始まる場合、この値が使用されます。その他の場合、 値は 0 (ゼロ) となります。
上記仕様から次の解答でもAbsolute best scoreを1にできます。
🌸
なおその他にもNaNという意味的にスマートな回答もあります。
Level 10
function(&$x){$x=new$x(1);}
インスタンスが入っている変数でnewするとインスタンスのクラスでnewできる。
class Bar{}
$bar = new Bar();
var_dump($bar, new $bar);
/* 実行結果
* object(Bar)#1 (0) {
* }
* object(Bar)#2 (0) {
* }
*/
Level 11
[]
配列に存在しない添え字の値を取得しようとするとNoticeとなるがnullが返るのでtrueとなる。
配列に定義されていないキーへアクセスしたときの挙動は、 未定義の変数にアクセスしたときと同じです。 E_NOTICE メッセージが発行され、 返される結果は NULL となります。
以上です。
最後にちょっとだけ告知
Project ICKXはPHPer Kaigi2018にシルバースポンサーとして協賛しています。
2018/03/09と2018/03/10に練馬区立・産業プラザ Coconeriホールで開催される 現在PHPを使用している、過去にPHPを使用していた、これからPHPを使いたいと思っているエンジニアが、技術的なノウハウを共有するためのカンファレンス です。
この記事を書いた私も参加しますので、会場で見かけられましたらお気軽にお声がけください。