🐥

一日一処: TypeScriptで連番の数字を出力するコードをできるだけ長く書くする

2024/02/06に公開

できるだけ長く書く原動力

プログラムは、適切な考えのもと記述しなければ、冗長的になってしまうこともしばしば。ただ、その適切な考えとは何だと、再考したとき、短く記述するための手法だけではないのだろう、と至った。つまり、どれだけ簡単なプログラムであっても、限りあるだけ、冗長的に長く書くことができれば、それはある意味、短くするためのテクニックなのではないかという結論になった。我々はいかに短く書くかに囚われすぎて、わざと長く書くことから遠ざかっている。
つまり、冗長と簡素は紙一重ということだ。

(直接的な関連する意味はないです。ただ逆に長く書くことに興味を持っただけです。冗談半分の取り組みです。申し訳ないです。)

連番の数字を出力

まずは、0から99だ。言ってしまえ、TypeScriptでなくてもいいし、他のどの言語でも、大体同じような内容になってくる。これ以上の基本的な記述はないだろう。

for (let i = 0; i < 100; i++) {
  console.log(i)
}

一旦、短く書いてみたい

と思ったが、私の技能だと、2文字分程度しか短くならなかった。

let i=0;while(i<100)console.log(i++)

より長く、最適なプログラムに

ここから、緩やかに長いプログラムに変化させていく。道のりは長くないと祈りたい。
まずは、console.logを直接使用しており、これは万死に値する。そのため、ひとまず出力用のクラスに切り分け、これを使用する。

class Output {
  static println(value: string) {
    console.log(value)
  }
}

for (let i = 0; i < 100; i++) {
  Output.println(`${i}`)
}

これで、console.logを直接使用せずにすんだ。危ないところだった。
これだと、現状、文字列のみしか、出力に対応していないため、ジェネリックを用いて、文字列と数列を出力できるように修正する。

class Output {
  static println<T extends string|number>(value: T) {
    console.log(value)
  }
}
for (let i = 0; i < 100; i++) {
  Output.println<number>(i)
}

これで、数列を直接渡すことができた。現在、静的メソッドによる実行ではあるが、Outputをインスタンス化して使いたいため、修正を加える。

class Output<T extends string|number> {
  println(value: T) {
    console.log(value)
  }
}

for (let i = 0; i < 100; i++) {
  new Output<number>().println(i)
}

出力毎に、インスタンスを生成してしまうので、実際のところパフォーマンスがあまりよろしくないと思うが、今回は如何に冗長的にかけるかが論点のため、パフォーマンスという悪魔に飲み込まれないようにする。
せっかくインスタンス化できるクラスを定義できたので、コンストラクタを追加する。

class Output<T extends string|number> {
  #value

  constructor(value: T) {
    this.#value = value
  }

  println() {
    console.log(this.#value)
  }
}

for (let i = 0; i < 100; i++) {
  new Output<number>(i).println()
}

どうだろう。これで、Outputクラスは簡潔に成立したのではないだろうか。

続いて、繰り返し構文だ。こんなものを使用しているなんて、命がいくつあっても足りない。この名を言ってはいけない例の繰り返し構文を新しく作ったクラスに置き換える。

type LoopCallback = (index: number) => void
class Loop {
  #callback: LoopCallback

  constructor(callback: LoopCallback) {
    this.#callback = callback
  }
}

new Loop(
  (i) => new Output<number>(i).println()
)

繰り返しにはLoopクラスを用いる。コンストラクタには、繰り返し毎に実行されるコールバックを設定する。コールバックには常に繰り返しで加算される値が引数として渡される。
次に、開始と終了を設定できるようにしたい。

type LoopCallback = (index: number) => void
class Loop {
  #callback: LoopCallback
  #start: number = 0
  #end: number = 0
  #current: number = 0

  constructor(callback: LoopCallback) {
    this.#callback = callback
  }

  set start(value: number) {
    this.#start = value
  }

  set end(value: number) {
    this.#end = value
  }
}

const loop = new Loop(
  (i) => new Output<number>(i).println()
)
loop.start = 0
loop.end = 99

セッターを用いて、開始と終了の値を設定できるようにした。この時点では、繰り返しが行われることはない。そもそも、記述したそばから繰り返しが実行されるというのは、恐ろしくないだろうか。もし、鋭利なブーメランの回転であれば、命はなかったでしょう。自動で繰り返しが実行されないことで、命拾いしました。
次に、繰り返しの実行を行う前に、開始と終了の数値をまとめて指定できるようにオプションを追加しよう。

type LoopCallback = (index: number) => void
type LoopOptions = { start?: number, end?: number }
class Loop {
  #callback: LoopCallback
  #start: number
  #end: number
  #current: number = 0

  constructor(callback: LoopCallback, options: LoopOptions = {}) {
    this.#callback = callback
    this.#start = options?.start ?? 0
    this.#end = options?.end ?? 0
  }

  set start(value: number) {
    this.#start = value
  }

  set end(value: number) {
    this.#end = value
  }
}

new Loop(
  (i) => new Output<number>(i).println(),
  { start: 0, end: 99 },
)

やはり、オプションのように設定ができるとスタイリッシュに見える。これだけで、素晴らしいプログラムを書いたという錯覚さえあるように思える。
次が最後だ。等々、この忌まわしき繰り返しを実行させてあげよう。

type LoopCallback = (index: number) => void
type LoopOptions = { start?: number, end?: number }
class Loop {
  #callback: LoopCallback
  #start: number
  #end: number
  #current: number

  constructor(callback: LoopCallback, options: LoopOptions = {}) {
    this.#callback = callback
    this.#start = options?.start ?? 0
    this.#end = options?.end ?? 0
    this.#current = this.#start
  }

  set start(value: number) {
    this.#start = value
  }

  set end(value: number) {
    this.#end = value
  }

  open() {
    this.#current = this.#start
    while(this.#current <= this.#end) {
      this.#callback(this.#current)
      this.#current = this.#current + 1
    }
  }
}

const loop = new Loop(
  (i) => new Output<number>(i).println(),
  { start: 0, end: 99 },
).open()

openメソッドでは、whileを用いた。本当はもう少し煩雑なものを考えたが、本質からズレすぎてしまうと感じたので、今回はこの程度だ。
改めて全文を確認しよう。

class Output<T extends string|number> {
  #value

  constructor(value: T) {
    this.#value = value
  }

  println() {
    console.log(this.#value)
  }
}

type LoopCallback = (index: number) => void
type LoopOptions = { start?: number, end?: number }
class Loop {
  #callback: LoopCallback
  #start: number
  #end: number
  #current: number

  constructor(callback: LoopCallback, options: LoopOptions = {}) {
    this.#callback = callback
    this.#start = options?.start ?? 0
    this.#end = options?.end ?? 0
    this.#current = this.#start
  }

  set start(value: number) {
    this.#start = value
  }

  set end(value: number) {
    this.#end = value
  }

  open() {
    this.#current = this.#start
    while(this.#current <= this.#end) {
      this.#callback(this.#current)
      this.#current = this.#current + 1
    }
  }
}

new Loop(
  (i) => new Output<number>(i).println(),
  { start: 0, end: 99 },
).open()

改行も含まれているが、48行の大作となった。冗長的に書くことで、新しい発見があるかもしれない、ライブラリを作り出すヒントになるかもしれない。このような処理だと、どのような表現のパターンがあるのか、こういった頭の体操で思考を広げてみるのも楽しいかもしれない。
もし、関心があれば、現状のコードでは、プラス方向への繰り返ししかできないため、マイナス方面への繰り返しや、倍数によるスキップ、数値の設定ミスによる例外対応なども加えてもらえると嬉しい。

Discussion