👻

一日一処: Rustの数当てゲームを別言語で模倣(JavaScript編)

2024/02/09に公開

Rustの数当てゲーム

先日Rustのドキュメントに記載されているゲームについて、記述した。個人的なプログラミングの学習方法としては、認識している言語同士でそれぞれ同じものを記述することにあると思っている。そうすることで、言語に対する理解などを他言語を基準に効率よく理解することができるからだ。もちろん、それぞれの言語で仕様が異なる場合もあるため、すべての状況において最適とは言えないが、このゲームをJavaScriptに置き換えることで、これから学習しようとしている人の助けになるかと思い、プログラミングを行った。

模倣の方向性

JavaScriptでの模倣は、単純にプログラムそのものをRustからJavaScriptに置き換えるという意味ではなく、Rustで記述されているすべてを可能な限り維持しつつ、JavaScriptで再現するということだ。これができると、単にJavaScriptの拡張性だけでなく、Rustの振る舞いにも歩み寄ることができると思う。Rustのコードについては、前述のリンクまたは、公式ドキュメントを参考にして、比較してほしい。
また、プログラミングの過程を作業しつつそのまま転記しているため、最終的な状態を見たい人は、末尾までスクロールするといいだろう。

模倣の準備

できることとできないことがある。
たとえば、Rustでは関数定義がfnに対し、JavaScriptではfunctionであったり、関数名に感嘆符!を使用できなかったり、変数宣言など、このあたりの仕組みについては、JavaScriptで再現するのが困難なため、標準的な記述とする。

模倣

まずはmain関数からだ。自動的に実行されるわけではないため、その後、関数名を参照し実行する。

function main() {

}
main()

続いては出力関数だ。println名称には寄せていく。

const println = (v) => console.log(v)

function main() {
    println("Guess the number!");
}
main()

ここはそれほど困難ではない。次の乱数の生成についても同様に関数を作り設定したいが、せっかくなので、Rustで記述されているような仕様に寄せた関数を作る。

const println = (v) => console.log(v)
const rand = {thread_rng: () => ({
  gen_range: (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
})}

function main() {
    println("Guess the number!");

    let secret_number = rand.thread_rng().gen_range(1, 100);
}
main()

JavaScriptには存在しないWコロンやRangeについては、再現できないが、同じような表現できた。次に無限ループだ。

// 省略
const Break = () => { throw 'break' }
const Continue = () => { throw 'continue' }
const loop = (callback) => {
  while(true) {
    try { callback() }
    catch(e) { if (e === 'continue') {continue} else {break} }
  }
}

function main() {
  // 省略

  loop(() => {
    Break()
  })
}
main()

せっかくloopという名称があるため、whileでの無限ループではなく、loop関数を拵えてみた。ただし、通常のbreakやcontinueだと、予約語となるため、先頭大文字の関数として定義した。ひとまずBreakでループは一度で終了するように記述している。
続いて、文字の出力だ。これは、前回同様なので、特に新しい要素はない。変数宣言まで合わせて記述する。

// 省略
const String_New = () => ({value: ''})

function main() {
  // 省略

  loop(() => {
    println("Please input your guess.");

    let guess = String_New()

    Break();
  })
}
main()

Rustの参照渡しを想定して、文字列用のオブジェクトを生成する似せた関数を準備した。文字列の標準入力は、例外処理などで若干手間取ったが、いい具合に再現できているのではないだろうか。

// 省略
const loop = (callback) => {
  while(true) {
    try { callback() }
    catch(e) {
      if (e === 'continue') {continue}
      else if (e === 'break') {break}
      console.log(e.message);
      break;
    }
  }
}
const String_New = () => ({value: ''})
const io = {stdin: () => ({
  read_line: (obj) => ({expect: (msg) => {
    obj.value = ''
    try{obj.value = require("fs").readFileSync(process.stdin.fd, "utf8")}
    catch(_) {}
    if (`${obj.value}` === '') {throw new Error(msg)}
  }})
})}

function main() {
  // 省略

  loop(() => {
    println("Please input your guess.");

    let guess = String_New()

    io.stdin()
      .read_line(guess)
      .expect("Failed to read line");

    console.log(guess)
    Break();
  })
}
main()

特に参照渡し風に値を挿入することと、expectメソッドで例外時のメッセージを設定するところは、再現度が高い様に思える。空文字を標準入力で渡すと、この例外が発生する。実際の仕様とは異なると思うが仕方ない。loop関数にも修正を加えた。
さらにここから、入力された文字を数値へ変換する仕組みを置き換える。

// 省略
const String_New = (that = null) => (that = {
  value: '',
  trim: () => {that.value = that.value.trim(); return that},
  parse: () => Number(that.value),
})
const match = (v, c) => isNaN(v) ? c.Err() : c.Ok(v)

function main() {
  println("Guess the number!");

  let secret_number = rand.thread_rng().gen_range(1, 100);

  loop(() => {
    println("Please input your guess.");

    let guess = String_New()

    io.stdin()
      .read_line(guess)
      .expect("Failed to read line");

    guess = match(guess.trim().parse(), {
      Ok: (num) => num,
      Err: (_) => Continue()
    })

    Break();
  })
}
main()

変数宣言は仕様上かけないが、matchに関しては、括弧の有無程度でほとんど同じ表現になったのではないだろうか。ここまで来ると、もはや別の言語なのではないか、と思ってしまう。

残り僅かだ。最後は、比較を行い、結果を出力する処理だ。これまでのコードのすべてを省略せずに記述する。

const println = (v) => console.log(v)
const rand = {thread_rng: () => ({
  gen_range: (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
})}
const Break = () => { throw 'break' }
const Continue = () => { throw 'continue' }
const loop = (callback) => {
  while(true) {
    try { callback() }
    catch(e) {
      if (e === 'continue') {continue}
      else if (e === 'break') {break}
      console.log(e.message);
      break;
    }
  }
}
const io = {stdin: () => ({
  read_line: (obj) => ({expect: (msg) => {
    obj.value = ''
    try{obj.value = require("fs").readFileSync(process.stdin.fd, "utf8")}
    catch(_) {}
    if (`${obj.value}` === '') {throw new Error(msg)}
  }})
})}
const String_New = (that = null) => (that = {
  value: '',
  trim: () => {that.value = that.value.trim(); return that},
  parse: () => Number(that.value),
})
const Ordering = { Less: 'Less', Greater: 'Greater', Equal: 'Equal' }
const match = (v, c) => {
  if (Ordering[v]) {c[Ordering[v]]()}
  else {return isNaN(v) ? c.Err() : c.Ok(v)}
}
Number.prototype.cmp = function(num) {
  return this > num ? Ordering.Greater : this < num ? Ordering.Less : Ordering.Equal
}

function main() {
  println("Guess the number!");

  let secret_number = rand.thread_rng().gen_range(1, 100);

  loop(() => {
    println("Please input your guess.");

    let guess = String_New()

    io.stdin()
      .read_line(guess)
      .expect("Failed to read line");

    guess = match(guess.trim().parse(), {
      Ok: (num) => num,
      Err: (_) => Continue()
    });

    println(`You guessed: ${guess}`);

    match(guess.cmp(secret_number), {
      [Ordering.Less]: () => println("Too small!"),
      [Ordering.Greater]: () => println("Too big!"),
      [Ordering.Equal]: () => {
        println("You win!");
        Break();
      }
    })
  })
}
main()

入力データを変換する際に作ったmatch関数を少しだけ改変し、最後の結果判定時にも利用した。個人的には、非常にうまくできなのではないだろうか。再現のために追加した関数を見えないところに定義してしまうと、瞬間的になんの言語か判別つかなくなるような気がする。
そして、この再現をしてみて思ったのは、Rustのmatchはすごい便利な代物なんだなと感じた。他言語でもパターンマッチングはよく出てくるが、JavaScriptではSwitch文程度しかないため、やはり、このような明確でわかりやすい仕組みは非常に便利だと実感した。しかし、この再現コードが若干Go言語に錯覚してきたのは気の所為だろうか。

Discussion