前書き
ゆめみからの挑戦状題4弾を見つけたので、それを考えていた時に調べたことなどメモ
ゆめみからの挑戦状題4弾とは
該当ツイート
内容は単純な足し算関数に1行加えて凶悪な関数にする、というお題です。
"凶悪"の定義がないので、そう感じたらそれが正解?みたいな大喜利問題になっている気がする
とりあえず考えてみる
該当ツイートを引用リツイートで回答する、とのことなので他の人の回答も見れるのだが、
PC破壊しに行ったり、無限alertや、時限爆弾、計算誤差など、結構いろんな回答があった。
で、とりあえず考えてみるのだが、"凶悪な関数"というのが曖昧なので、個人的にルールをつけてみる。
- add関数なので、引数を加算する
- "1行加えて"なので1行、かつセミコロンを使用しない
- その上で、"add"をみただけでは想像つかない結果を返すようにする
この3つ。ちなみに念の為、個人的につけたルールなので、他の人の回答を縛るわけではない。
1つ目はそのまま。加算関数なのに加算しないのも変かな?と思ったので、加算結果を受け取れるようにはする。
2つ目は、JSはセミコロンがあれば1行で全て書くことができてしまうので、1行というルールを厳密に縛ってみる。
3つ目もそのまま。この規模によって凶悪さが決まるという感じになる。
思いついたもの
自己ルールの上で今回思いついたのは3つ。全て引用リツイートしたが、ここにも記載。
1つ目
const add = (a: number, b: number): number => {
// ここに1行追加して、凶悪な関数にしてください。
throw a + b
return a + b
}
return
ではなく、throw
で結果を返す。
戻り値number
と書いているが、実際やっていることはnever
だったりする。
try{
add(a, b)
}catch(e){
console.log(e)
}
とすることで普通に受け取ることが可能ではある。もし何らかの原因で他の例外が返ってきた場合は知らない。
2つ目
const add = (a: number, b: number): number => {
// ここに1行追加して、凶悪な関数にしてください。
var [c, d, a, b] = [0, 0, a, b].map(e => ({ valueOf: () => typeof c === "object" ? (c = e, '') : (d = c, c = {} as any, (d + e).toString().split("").map((f: any)=> ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', '-'][f === '-' ? '10' : Number(f)]).join(""))})) as any as number[]
return a + b
}
個人的に頑張ったもの。これの途中過程のメモのためにこれを書いてると言っても過言ではない、というか正解。
外から見た時の動作としては、a
b
に値を入れると、その結果が漢数字
で返ってくるというもの。
あ、書いている途中で気がついたけど小数点の存在を忘れているやつ、ここで修正しとこ。。。
なので、add(23,45)
とすると六八
と返ってくる。number
とは。
3つ目
const add = (a: number, b: number): number => {
// ここに1行追加して、凶悪な関数にしてください。
[a, b] = [BigInt(a), BigInt(b)] as any as number[]
return a + b
}
(整数値であれば)正常に計算され、一見普通に見える1が、返ってくる型がBigInt
になっている。number
とは(2回目)
number型とBigInt型は暗黙の変換ができないので、一度anyを返さないといけない。
あとes2020以降じゃないと動かない(用意されたplaygroundは2017だったので、プレイグラウンド→targetをes2020に変更)。
以上
ということで、全部宣言されたnumber
を無視するような感じの内容でした。
この3つの内2番目のものが、そこそこに苦戦したのでここからメモとして記録しておく。(ここから本題)
2つ目の試作
無職やめ太郎(本名)さん出題ということで、せっかくなのでワイ記法を使ってみる
ワイ「凶悪な関数か、なんかいいのあるやろか?」
ワイ「せっかくやから、後ろのreturn a + b
が活かせるコードにしたいなぁ」
ワイ「せや、足された結果が漢数字になって返ってくるコードはどうやろ?」
ワイ「でも事前にaとbに足された結果を入れて文字列結合はおもんないなぁ」
ワイ「せや、クラス作ってc#のoperator+みたいなことできひんやろか?」
名前曖昧だったので適当に検索したところ、演算子オーバーロードというらしいです。
調べてみたところ以下のサイトを発見。
果たして JavaScript で演算子オーバーロードは可能なのか - けんごのお屋敷
端的にまとめるとこんな感じ
- javascriptに演算子オーバーロードはない
- ただ、オブジェクトに対し+演算子を使うと、
valueOf
が呼び出される -
valueOf
はプリミティブ型しか返すことができない
ワイ「なるほど、valueOfを使えばやりたいことできそうやな」
ワイ「参考サイトではオブジェクトが返せなくて困ってたが、今回返したいのは漢数字やからこれで十分やで」
ワイ「ほな実装してみよか」
複数行で実装してみる
いきなり1行コードを書けるわけもないので、まずは複数行で書いてみる
const add = (a: number, b: number): number => {
// ここに1行追加して、凶悪な関数にしてください。
let c: number | undefined;
[a, b] = [a, b].map(v => {
const d = new Object(v);
d.valueOf = () => {
if (c === undefined) {
c = v;
return '';
} else {
const result = (c + v).toString().split("").map(v => ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', '-'][v === '-' ? '10' : Number(v)]).join('');
c = undefined;
return result;
}
};
return d;
}) as any as number[];
return a + b;
}
大体こんな感じ。癖で文末セミコロンが登場しているのは触れてはいけない。
簡単に説明。
まず先ほどの演算子オーバーライドの際に参考にしたサイトのコードを確認。
// 「オブジェクトからプリミティブへの変換」の項目の後半部分にあるコードの引用
var tmp;
Matrix.prototype.valueOf = function() {
if (tmp === undefined) {
// 左側オペランドの呼び出し。
// 後で参照するために外部の変数に値を保存して、戻り値は適当な値を返す。
tmp = this;
return 0;
} else {
// 右側オペランドの呼び出し。
// thisの値と外部変数の値を足し算してそれを返す。
var result = this.add(tmp);
tmp = undefined;
return result;
}
};
これが何をしているかというと、
-
a + b
の加算の前半だった場合、tmp
がundefined
なので、aの値をtmp
に一旦避難させる -
a + b
の加算の後半だった場合、tmp
にaの値があるので、それをbを合わせ、その値を返す。またtmp
はundefined
に戻しておく
ということをしている
このパターンを今回採用するために、a,b
をそれぞれ一旦オブジェクト化(変数d)し、そのvalueOf
を書き換えていくことにする。
それがこの箇所
let c: number | undefined;
[a, b] = [a, b].map(v => { // a,bそれぞれ
const d = new Object(v); // オブジェクト化
d.valueOf = () => { // valueOfの書き換え
if (c === undefined) {
c = v; // 避難
return ''; // 今回は文字列を返すため、0ではなく空欄
} else {
const result = 漢数字化処理();
c = undefined; // undefinedに戻す
return result; // 足した結果を返す
}
};
return d;
});
また関数化処理のところは、足した結果を置き換えて、結合するというシンプルなことをしているが、配列を定義しているのでややこしそうなコードになった。
1行にしていく
できたコードを1行にする。
・・・今回多用したカンマ演算子についてこれを書いているときに改めて確認してみる(遅い)
カンマ演算子 (,) は、それぞれの演算対象を(左から右に)評価し、最後のオペランドの値を返します。これにより、複数の式が評価される複合式を作成することができ、複合式の最終値はそのメンバー式の一番右端の値となります。
つまり、複数式が書けて尚且つ最後の式だけが返されるというもの。
これをうまく使えばc, dを一々0でmapする必要なかったのでは?と今更。
三項演算子
まずはvalueOfの条件分岐を三項演算子でまとめる。
先ほどのカンマ演算子を使えば、最後の一行をreturnと扱えるので、valueOfの関数自体を一行にまとめることができそう。
d.valueOf = () => c === undefined ? (c = v, '') : else処理
/* else処理 :
const result = 漢数字化処理();
c = undefined; // undefinedに戻す
return result; // 足した結果を返す
*/
カンマ演算子を使うには()
で括らないと使えないっぽいので、括弧で括る。
if側はこれで出来たのだが、else処理がうまくいかない。
というのも、この括弧内で変数定義ができないので、resultを前で宣言することができない。
仕方ないので、一旦手前でresultを宣言することで回避することにする。
let result: string;
d.valueOf = () => c === undefined ? (c = v, '') : (result = 漢数字化処理(c + v), c = undefined, result);
これで2行になった。変数定義は後で考えよう。
オブジェクト生成をまとめる
今new Object
してvalueOf
を上書きしている、という動作を1つにまとめる。
これは思いつけば簡単なのだが、思いつくまでに多少時間がかかった(気がする。ここ書くまでに若干時間が経ってあやふやに。。)
というのも、オブジェクト生成はnew
する必要なく{}
で作れてしまうので、それを使えばいいだけ。
あとはmapの中身が変数定義とオブジェクト生成だけになるので、そのままreturn
しても問題なくなる
let result: string;
return {
valueOf : () => c === undefined ? (c = v, '') : (result = 漢数字化処理(c + v), c = undefined, result)
};
変数定義をまとめる
現時点で残っている変数定義は、valueOfで避難させておくc
と、漢数字を入れるresult
だけ。
これを、先ほどのカンマ演算子で、mapの前に持ってくる。
そうするとmapの中身がreturnだけになるので、そのまま返せばいいのだが、オブジェクトは関数表記と同じ{}
で囲むので、普通に関数内として認識されてしまう。
ということで、返す方法を調べてみた
JavaScriptのアロー関数でオブジェクトを返す方法 | DevelopersIO
()
で括るだけらしい。意外とシンプルに出来る。
var c: any, result, [a, b] = [a, b].map(v =>
({
valueOf: () => c === undefined ?
(c = v, '') :
(result = 漢数字化処理(c + v),
c = undefined,
result)
})
) as any as number[];
あれ、これでいいじゃん。
ツイートの方でvar [c, d, a, b] = [0, 0, a, b].map
という書き方をしていたのは、
途中で変にエラーが出たからなのだが、別にする必要なかったらしい。
あとは漢数字化処理
という知らぬ間に避難させた処理を戻して、終わりです
せっかくなので小数点問題も修正したバージョンを。
const add = (a: number, b: number): number => {
// ここに1行追加して、凶悪な関数にしてください。
var c: any, result, [a, b] = [a, b].map(v => ({ valueOf: () => c === undefined ? (c = v, '') : (result = (c + v).toString().split("").map((o: any) => ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', '-', '点'][o === '-' ? '10' : (o === '.' ? '11' : Number(o))]).join(''), c = undefined, result)})) as any as number[];
// 改行ありver
// var c: any, result, [a, b] = [a, b].map(v =>
// ({
// valueOf: () => c === undefined ?
// (c = v, '') :
// (result = (c + v).num.toString().split("").map((o: any) => ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', '-', '点'][o === '-' ? '10' : (o === '.' ? '11' : Number(o))]).join(''),
// c = undefined,
// result)
// })
// ) as any as number[];
return a + b
}
とても長いけど1行になったので、個人的につけたルールを満たした凶悪な?関数が出来上がった。
終わりに
ツイートしたのと若干異なる一行が出来上がった。
でも気になっていたところが修正できたのでいいということにしよう。
-
数値の最後にnがつくので厳密には普通に見えない。ただ用意されたplaygroundで実行してみると同じだった。 ↩