TIP

※ この記事は苫小牧高専アドベントカレンダー (opens new window) 3 日目の記事です。

# はじめに

どうも、おおやまです。 今年もアドベントカレンダーの時期ですね。某男 (opens new window)が初日に記事を書いていないので、多分僕が苫小牧高専アドベントカレンダーのトップバッターになっていると思います...

彼も数時間後に書くと信じて、はりきって書いていきます!!()

# FizzBuzz って?

おさらいとして FizzBuzz ゲームのルールはこんな感じです

  • 1 から任意の数字まで数を数え上げていく
  • 3 で割り切れる数字には Fizz と宣言
  • 5 で割り切れる数字には Buzz と宣言
  • 3 と 5 で割り切れる数字には FizzBuzz と宣言
  • それ以外はその数字を宣言する

実際にこのゲームを遊んでいる人を見たことないので、 一人で部屋でブツブツ「1,2,Fizz,4,Buzz...」と 100 くらいまでまで数えて遊んでみました。




...何が楽しいんだ...このゲーム...

20 を超えたあたりから心を殺して FizzBuzz を数え上げるだけの機械と化し、淡々と口から異音を発する異常男性になっていました。 盛り上がりにかける分 「世界のナベアツゲーム」の完全下位互換 です...


このつまらない FizzBuzz ゲームはプログラミングのチュートリアル的なテーマとしてよく実装されています。TypeScript で実装するとこんなかんじですね

const fizzBuzz = (n: number): string | number => {
  if (n % 15 === 0) {
    return `FizzBuzz`
  }
  if (n % 3 === 0) {
    return `Fizz`
  }
  if (n % 5 === 0) {
    return `Buzz`
  }
  return n
}
1
2
3
4
5
6
7
8
9
10
11
12

これを for なりで回すと FizzBuzz ゲームを機械と化した異常男性ではなく、本物の機械に遊ばせることができます

# 本題

しかし、機械に遊ばせても何も面白くありません...

では型で実装するのはどうでしょう...?
.....
.....
.....
....
とたんに面白そうに感じてきましたね!!!!!

というわけで、今回はこの FizzBuzz を型レベルで実装してみます。ゴールとしてはこんな感じの型を目指します

type fizzBuzz2 = FizzBuzz<7> // 7
type fizzBuzz1 = FizzBuzz<30> // "FizzBuzz"
1
2

# FizzBuzz の構成要素

上記の fizzBuzz 関数を見ながらFizzBuzz型に必要な構成要素はこんな感じです

  • 数値のリテラル型同士の剰余(あまり)を示すMod型
  • "Fizz"型"Buzz"型"FizzBuzz"型への型の条件分岐

# Mod型

あまりをシンプルに考えてみます。A と B の A から B から引き続け、引けなくなったらそれがあまりです。 となると、Mod型を作るには引き算を示すSub型も必要そうです

# Repeat型

Mod型Sub型のようなような数値リテラル型を扱う際に必要な型を先に作っておきましょう。

TypeScript でリテラル型はタプル型を使ってTuple["length"]で取得することができるので、基本的にリテラル型をタプル型に変換してタプルをこねこねする形になります。

type tuple = [any, any, any]
type literal = tuple[`length`] // 3
1
2

とりあえず、リテラル型からタプル型に変換するRepeat型を実装します。

type Repeater<T, N extends number, R extends T[]> = R[`length`] extends N
  ? R
  : Repeater<T, N, [T, ...R]>
type Repeat<T, N extends number> = Repeater<T, N, []>
type hogeTuple = Repeat<`hoge`, 3> // ["hoge", "hoge", "hoge"]
type tuple = Repeat<any, 3> // [any,any,any]
1
2
3
4
5
6
7
8

Repeat 型では再帰をつかって T 型を N 個並べています。

# Sub型

では引き算を示すSub型を作ります

export type Sub<A extends number, B extends number> = Repeat<any, A> extends [
  ...Repeat<any, B>,
  ...infer R
]
  ? R[`length`]
  : never
1
2
3
4
5
6

ジェネリクスとして受け取った A と B をタプル型に変換し、スプレッド構文による分割代入っぽい書き方で A の要素から B の要素を除いたタプル型である R を抽出します。あとは length プロパティを使って R をリテラル型に変換します。

また、B が A より大きい場合には never 型を返します。つまり負の数の計算はできないことになりますが、剰余の計算では問題ありません。

テキストエディタの型表示で確認してみるとうまく動いてそうです

# Mod型

引き算を示すSub型を作ることができたので、剰余を示すMod型を作ってみましょう

export type Mod<A extends number, B extends number> = Sub<A, B> extends never
  ? A
  : Mod<Sub<A, B>, B>
1
2
3

引けなくなるまで(=Sub型が never を吐くまで)A から B を引き続け、最終的な残りかすをあまりとして返す感じです。

よさげですね

# FizzBuzz型の実装

Mod型が完成したのであとは剰余の値に合わせて型を分岐させるだけです

type FizzBuzz<T extends number> = Mod<T, 15> extends 0
  ? `FizzBuzz`
  : Mod<T, 5> extends 0
  ? `Buzz`
  : Mod<T, 3> extends 0
  ? `Fizz`
  : T
1
2
3
4
5
6
7

シンプルですね!

エディタでも確認してみましょう

うまくいってそうです!!

これで型安全に FizzBuzz を遊べまね!!

# おわり

型レベルプログラミングをちょっと触りたくて実装してみましたが、パズルみたいで結構楽しくできました!

FizzBuzz ゲームを現実でやるより楽しいのでオススメです()