TypeScriptの型で素因数分解する
こちらは LAPRAS Advent Calendar 2022の25日目の記事です。
概要
本記事では、与えられた自然数を素因数分解する型を作っていきます。
以下のPrimeFactorList
型が完成イメージです。
const foo: PrimeFactorList<15> = [3, 5]
const bar: PrimeFactorList<16> = [2, 2, 2, 2]
バージョン情報
- TypeScript: v4.9.4
素因数分解のために必要なこと
最も素朴な「試し割り法」で行う場合、以下の 3 種類の計算が必要です。
-
number
リテラル型I
が与えられたとき、その次の数I + 1
の型を作れる(ループを回すため) - 素因数分解したい数
N
が、I
で割り切れるかどうかを判定できる - 割り切れる場合にその商が計算できる
型レベルで自然数を操作する
上記 3 種類の計算を型システム上で行うためには、まずはnumber
リテラル型を計算可能な形に変換する必要があります。
今回は「タプルの長さを介して自然数の計算を行う方法(en)」を採用しました。
まずはその方法を、簡単な例をつかって解説します。
足し算を行う例
number
リテラル型1
と2
の足し算を行う場合は以下のようになります。
- 型
1
を長さ1
のタプル型T
に変換する(方法は後述) - 型
2
を長さ2
のタプル型U
に変換する -
T
とU
を連結させ、長さ3
のタプル型[...T, ...U]
を得る - そのタプルの長さ
[...T, ...U]['length']
を取ることで、型3
を得る
つまり、
- 自然数
N
の型を、長さN
のタプル型に変換 - そのタプルを好きなように操作
- 操作後のタプルの
length
を取得することで数値型に戻す
ということです。
このような方法をとるのは、number
リテラル型よりもタプル型の方が色々と操作がしやすいためです。
実装
「数N
を長さN
のタプルに変換する型」は以下のように作れます。
type NumToTuple<
N extends number,
Result extends unknown[] = []
> = Result['length'] extends N
? Result
: NumToTuple<N, [...Result, unknown]>
let tuple: NumToTuple<3> // [unknown, unknown, unknown]
一見ややこしく見えますが、やっていることは単純な再帰です。
-
Result
タプルの長さがN
未満なら、Result
に要素を一つ追加した上でNumToTuple
を再度呼び出す -
Result
タプルの長さがN
になったらResult
を返す
Result
はデフォルトで[]
型なので、N回ほど再帰すれば長さN
のタプルが得られることになります。
「次の数」の型を作る
これで「数I
の次の数を返す型」が作れるようになりました。
type PlusOne<I extends number> = [...NumToTuple<I>, unknown]['length']
let four: PlusOne<3> // 4
数I
をタプルに変換し、要素を一つ追加してからそのlength
を取っているだけで簡単ですね。
引き算
次に「割り切れるかどうかを判定する型」を作っていきたいところですが、それは、
- 引き算の繰り返し
- 大小判定
から作れるので、まずは引き算を行う型から作成して行きます。
type Subtraction<
N extends number,
M extends number
> = NumToTuple<N> extends [
...NumToTuple<M>,
...infer U
]
? U['length']
: never
let two: Subtraction<3, 1> // 2
解説
infer
を用いて以下を行なっています。
- 長さ
M
のタプルと、何らかのタプルU
を繋げて長さN
のタプルが作れそうなら、U
の長さを返す - そのような
U
が存在しなそうなら(M
の方がN
より大きいということなので)never
を返す
例えばN = 3
でM = 1
の場合は、TS君が「U
として長さ2
のタプルを取ってくれば長さN
のタプルが作れそうだな」と勝手に推論してくれます。賢いですね。
大小判定
次は大小判定です。これは引き算から簡単に作れます。
type IsStrictlySmaller<
N extends number,
M extends number
> = Subtraction<N, M> extends never ? true : false
let is1SmallerThan2: IsStrictlySmaller<1, 2> // true
割り切れるかどうかの判定
これでようやく「割り切れるかどうかを判定する型」が作れるようになりました。
type IsDivisible<
N extends number,
M extends number
> = N extends 0
? true
: isStrictlySmaller<N, M> extends true
? false
: IsDivisible<Subtraction<N, M>, M>
let is8DivisibleBy2: <8, 2> // true
解説
-
N < M
になるまで、N
からM
を引いていく - 最終的に
N
が0
になった場合、N
はM
の自然数倍ということなのでtrue
-
0
より大きい数になった場合は、それが余りなのでfalse
割り算
この節で下準備は最後です!
割り算もシンプルな引き算の再帰で作れます。
type Division<
N extends number,
M extends number,
Q extends unknown[] = [] // 商
> = isStrictlySmaller<N, M> extends true
? Q['length']
: Division<Subtraction<N, M>, M, [unknown, ...Q]>
let eightDividedByTwo: <8, 2> // 4
解説
-
N < M
になるまで「N
からM
を引いて商Q
を1
増やす」を繰り返す。
(つまりQ
は「N
をM
で何回引けたか」を表す) -
N < M
になったらQ
の長さを返す。
素因数分解
とうとう素因数分解までたどり着きました!
少し複雑ですが、基本的なテクニックはこれまでと同じです。
type PrimeFactorList<
N extends number,
I extends unknown[] = [unknown, unknown],
Result extends number[] = []
> = N extends 1
? Result
: I['length'] extends N
? [...Result, N]
: IsDivisible<N, I['length']> extends true
? PrimeFactorList<Division<N, I['length']>, I, [...Result, I['length']]>
: PrimeFactorList<N, [0, ...I], Result> // Iをインクリメントして再帰
解説
-
N = 1
なら、素因数がないので[]
を返す -
N > 1
なら、I
を長さ2
から開始して、N
がI
の長さと等しくなるまで以下を繰り返す-
N
がI
の長さで割り切れるなら、N
をI
の長さで割って、Result
にI
を追加して再帰 -
N
がI
の長さで割り切れないなら、I
をインクリメントして再帰
-
結果は以下のようになります。
let pf1: PrimeFactorList<1> // []
let pf2: PrimeFactorList<2> //[2]
let pf3: PrimeFactorList<3> // [3]
let pf4: PrimeFactorList<4> // [2, 2]
let pf5: PrimeFactorList<5> // [5]
let pf6: PrimeFactorList<6> // [2, 3]
let pf7: PrimeFactorList<7> // [7]
let pf8: PrimeFactorList<8> // [2, 2, 2]
let pf9: PrimeFactorList<9> // [3, 3]
let pf10: PrimeFactorList<10> // [2, 5]
let pf11: PrimeFactorList<11> // [11]
let pf12: PrimeFactorList<12> // [2, 2, 3]
let pf13: PrimeFactorList<13> // [13]
let pf14: PrimeFactorList<14> // [2, 7]
let pf15: PrimeFactorList<15> // [3, 5]
let pf16: PrimeFactorList<16> // [2, 2, 2, 2]
let pf17: PrimeFactorList<17> // [17]
let pf18: PrimeFactorList<18> // [2, 3, 3]
let pf19: PrimeFactorList<19> // [19]
let pf20: PrimeFactorList<20> // [2, 2, 5]
let pf21: PrimeFactorList<21> // [3, 7]
let pf22: PrimeFactorList<22> // [2, 11]
let pf23: PrimeFactorList<23> // [23]
let pf24: PrimeFactorList<24> // [2, 2, 2, 3]
let pf25: PrimeFactorList<25> // [5, 5]
let pf26: PrimeFactorList<26> // [2, 13]
let pf27: PrimeFactorList<27> // [3, 3, 3]
let pf28: PrimeFactorList<28> // [2, 2, 7]
let pf29: PrimeFactorList<29> // [29]
let pf30: PrimeFactorList<30> // [2, 3, 5]
.
.
.
let pf999: PrimeFactorList<999> // [3, 3, 3, 37]
型のみを使って正しく素因数分解することができています🎉
全体のコードはこちらにあります。
さらに工夫を加えれば、より大きな数の素因数分解もできるかと思います。
興味のある方は是非試してみてください!
🎅🎁🎄𝕄𝕖𝕣𝕣𝕪 𝕏𝕞𝕒𝕤...🎄🎁🎅
Discussion