typescript 怎么表示当前时间减一个月_TypeScript类型元编程:实现8位数的算术运算...
失業(yè)中在 github 閑逛,看到有人用類型實現(xiàn)的一個4位虛擬機(jī),為什么是4位呢,因為 TypeScript 的類型實例化深度有限制,沒法實現(xiàn)太大的數(shù)字計算。說到用類型實現(xiàn)數(shù)字計算,一大堆邱奇數(shù)就冒出來了,但是因為這個限制,這種方法通常只能用于很小的數(shù)字,今天我們嘗試一種不同的思路,用二進(jìn)制實現(xiàn)8位的數(shù)字計算。
為了實現(xiàn)計算我們要先把數(shù)字類型轉(zhuǎn)換成二進(jìn)制,比如一個0和1的數(shù)組類型,但是進(jìn)制轉(zhuǎn)換又需要數(shù)學(xué)計算,顯然我們是沒辦法實現(xiàn)的,那么怎么辦呢? 很簡單,硬編碼一個映射就好了。但是對于8位數(shù)來說,255個長度為8的數(shù)組類型是一段很長的代碼,為了減少代碼長度,我們可以只編碼一個二進(jìn)制 trie 結(jié)構(gòu),然后通過搜索路徑得到二進(jìn)制編碼。
我們用數(shù)組來表示二進(jìn)制 trie,以3位數(shù)(0-7)為例,就是這個樣子:
type binaryTrie = [[[0, 1], [2, 3]], [[4, 5], [6, 7]]];數(shù)字的訪問路徑就是對應(yīng)的二進(jìn)制,比如binaryTrie[0][1][1]就是3,也就是二進(jìn)制的 011。然后我們寫一個簡單的腳本來生成8位的 trie:
JSON.stringify((function it(n, acc) {return n > 0? [it(n - 1, acc), it(n - 1, acc + 2 ** n)]: [acc + 0, acc + 1];})(7, 0) );看一下在編輯器里的樣子:
還好,不算太長,但如果是16位數(shù)字的話那出來的就是1M多字節(jié)了,所以我們只實現(xiàn)8位數(shù)的就好了。然后我們來定義一個用來查找 trie 的類型:
type SearchInTrie<Num, Node, Digits> = {1: Node extends [infer A, infer B]? Num extends A ? Push<Digits, 0>: Num extends B ? Push<Digits, 1>: never : never;0: Node extends [infer A, infer B]? SearchInTrie<Num, A, Push<Digits, 0>>| SearchInTrie<Num, B, Push<Digits, 1>>: never; }[Node extends [number, number] ? 1 : 0];這是一個遞歸的類型,三個參數(shù)分別是 要查找的數(shù)字,當(dāng)前查找節(jié)點(diǎn),當(dāng)前查找路徑。我們先判斷當(dāng)前節(jié)點(diǎn):如果是葉子節(jié)點(diǎn)(1)判斷是左右節(jié)點(diǎn)的哪一個,把相應(yīng)的二進(jìn)制值加入路徑并返回,都不是返回never。如果不是葉子節(jié)點(diǎn)(0),分別對左右節(jié)點(diǎn)進(jìn)行查找,將結(jié)果 union 在一起,由于其中一邊肯定查找不到,結(jié)果會是never,所以 union 的結(jié)果將會是最終查找到的路徑。
這里用到了一個Push類型來將二進(jìn)制位加入數(shù)組,它的完整定義如下:
type Copy<T, S extends any> = { [P in keyof T]: P extends keyof S ? S[P] : never };type Unshift<T, A> = ((a: A, ...b: T extends any[] ? T : never) => void ) extends (...a: infer R) => void ? R : never;type Push<T, A> =Copy<Unshift<T, any>, T & Record<string, A>>;你們可能已經(jīng)見過Unshift這種用法了,它就是用 conditional type 和函數(shù)類型的可變參數(shù)去推斷出一個在開頭插入了新元素的數(shù)組類型。目前只有這種辦法可以向元組類型中加入元素,而且只能在開頭加入。
那么這個Push是怎么實現(xiàn)在末尾加入元素的呢,這就涉及到另一個東西了,就是 mapped type,這個東西有個沒有在文檔中說明的特性,當(dāng)用于元組類型時,具體來說,in關(guān)鍵字后面是一個元組類型的 key 時,mapped type的結(jié)果也是一個相同長度的元組類型。TS 的隱式規(guī)則越來越多了,很多特性都是糊出來的。
所以我們先定義一個Copy類型將T上的屬性值覆蓋為S的,然后在Push中,我們先往數(shù)組開頭隨意插入一個元素,然后從原來的數(shù)組T復(fù)制屬性過來,但是因為多了一個元素,最后一個元素在T上是沒有的,所以我們加一個& Record<string, A>,無論這最后一個元素的 key 是多少,最后肯定會從這個Record上取到A,也就是我們要加入的元素。
回到主題上來,我們現(xiàn)在可以把數(shù)字轉(zhuǎn)成二進(jìn)制了,同時這個 trie 也可以由二進(jìn)制得到數(shù)字:
type Digit = 0 | 1; type Bits = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; type Uint8 = Record<Bits, Digit>; // 也可以定義成8個Digit的數(shù)組,這樣寫比較簡短// 數(shù)字轉(zhuǎn)二進(jìn)制表示 type ToUint8<A extends number> =SearchInTrie<A, BinaryTrie, []>;// 二進(jìn)制表示轉(zhuǎn)數(shù)字 type ToNumber<A extends Uint8> =BinaryTrie[A[0]][A[1]][A[2]][A[3]][A[4]][A[5]][A[6]][A[7]];那么我們就可以開始實現(xiàn)計算了,最簡單的也是最基礎(chǔ)的就是加法了,我們要用類型實現(xiàn)一個全加器。讓我們先來實現(xiàn)1位的加法器:
// 兩個1 bit數(shù)相加,C 表示進(jìn)位 type BitAdd<A extends Digit, B extends Digit, C extends Digit> = [[[[0, 0], [1, 0]], [[1, 0], [0, 1]]],[[[1, 0], [0, 1]], [[0, 1], [1, 1]]] ][A][B][C];非常簡單,A和B是相加的兩個1位數(shù)字,C是進(jìn)位標(biāo)志,返回的類型是兩個值的數(shù)組,第一個值是和,第二個值是進(jìn)位標(biāo)志。
然后把8個1位加法器級聯(lián)起來就組成了一個8位全加器:
type AsDigit<T> = T extends Digit ? T : never; type AsUint8<T> = T extends Uint8 ? T : never;type Uint8Add<A extends Uint8, B extends Uint8> =BitAdd< A[7], B[7], 0> extends [infer S7, infer C]? BitAdd<A[6], B[6], AsDigit<C>> extends [infer S6, infer C]? BitAdd<A[5], B[5], AsDigit<C>> extends [infer S5, infer C]? BitAdd<A[4], B[4], AsDigit<C>> extends [infer S4, infer C]? BitAdd<A[3], B[3], AsDigit<C>> extends [infer S3, infer C]? BitAdd<A[2], B[2], AsDigit<C>> extends [infer S2, infer C]? BitAdd<A[1], B[1], AsDigit<C>> extends [infer S1, infer C]? BitAdd<A[0], B[0], AsDigit<C>> extends [infer S0, infer C]// ? C extends 1 ? "overflow" :? AsUint8<[S0, S1, S2, S3, S4, S5, S6, S7]>: never : never : never : never : never : never : never : never;這里我們用 infer 語法當(dāng)作變量保存每一步的結(jié)果,這個語法直接把“函數(shù)式”變成了“過程式”(笑),不過 infer 出來的類型變量是沒有類型約束的,所以我們額外定義了兩個類型AsDigit和AsUint8來把結(jié)果 assert 成期望的類型,主要是為了滿足泛型參數(shù)的約束檢查。從中間的注釋可以看到,我們沒有對加法溢出進(jìn)行處理,溢出會得到循環(huán)后的結(jié)果,這也是實現(xiàn)減法的原理。
我們來看一下,8 位二進(jìn)制數(shù)能表示的最大數(shù)字,8 位全是 1,十進(jìn)制是 255,如果我們對它加 1 會怎樣?因為每位都是 1,所以會不斷的進(jìn)位最終得到全 0。如果加 2 呢,其實就相當(dāng)于加 1 再加 1,第一次加 1 得到 0,第二次加 1 就得到 1。所以我們可以簡單的認(rèn)識到,有限位的二進(jìn)制數(shù)字是循環(huán)的,因為最高位溢出后就回歸到 0 了。再舉一個例子,如果 255 加 256 呢?由于 256 是超出 8 位數(shù)范圍的,我們拆開成加 1 + 255。加 1 得到 0,再加 255 又得到 255。因為 0-255 共 256 個數(shù),所以任何數(shù)加 256 相當(dāng)于循環(huán)一圈,得到的還是原來的數(shù)。
我們再來看對一個數(shù)字減 1 會如何。以0 - 1為例,根據(jù)前面的推論我們知道,任何 8 位數(shù)字加 256 結(jié)果不變,我們把0 - 1表示為0 + 256 - 1,就變成了計算0 + (256 - 1),再進(jìn)一步地,由于 256 超出了 8 位數(shù)范圍,我們改寫為0 + (255 - 1 + 1),我們知道 255 的二進(jìn)制 8 位全是 1,它減任何一個 8 位數(shù),我們逐位相減可以發(fā)現(xiàn),如果被減數(shù)字的某一位為 0,那么結(jié)果為1 - 0 = 1,如果為 1 結(jié)果為1 - 1 = 0,所以我們可以簡單地把被減數(shù)每一位取反,得到的就是 255 減去它的結(jié)果。所以0 + (255 - 1 + 1)就變成了0 + (對1按位取反 + 1),所以0 - 1就等于“對 1 按位取反再加 1”(這個1是我們從256里拿出來的,不是減數(shù)1,不要搞混了),結(jié)果是 8 位全 1 也就是 255。和加法溢出類似,減法也產(chǎn)生了溢出循環(huán)的效果。這其實就是-1 的二進(jìn)制表示,“按位取反再加 1”就是所謂的“補(bǔ)碼”。在有符號的數(shù)字系統(tǒng)里,數(shù)字表示范圍的后一半就是用來表示負(fù)數(shù)(這就是為什么有符號數(shù)上溢會產(chǎn)生負(fù)數(shù),但實際上-1 + 1 = 0 才是二進(jìn)制溢出的結(jié)果),對于計算機(jī)來說正數(shù)負(fù)數(shù)都是一樣的,都是在一個環(huán)上,是我們通過指令的語義或語言的類型賦予了數(shù)字正負(fù)的含義。
再回到我們的主題,要實現(xiàn)A - B的計算,其實就是實現(xiàn)A + (-B),而負(fù)數(shù)我們上面已經(jīng)說了就是補(bǔ)碼,只不過我們的數(shù)字系統(tǒng)里沒有負(fù)數(shù),但是對于二進(jìn)制來說都是一樣的。
我們先來實現(xiàn)取反:
type Reverse = [1, 0];type Uint8Reverse<A extends Uint8> = [Reverse[A[0]],Reverse[A[1]],Reverse[A[2]],Reverse[A[3]],Reverse[A[4]],Reverse[A[5]],Reverse[A[6]],Reverse[A[7]] ];然后實現(xiàn)補(bǔ)碼,先取反再加 1:
type ONE = [0, 0, 0, 0, 0, 0, 0, 1];type Uint8Negate<A extends Uint8> =Uint8Add<Uint8Reverse<A>, ONE>;減法就是加上被減數(shù)的補(bǔ)碼:
type Uint8Sub<A extends Uint8, B extends Uint8> = Uint8Add<A, Uint8Negate<B>>;回顧一下,我們通過 1 位數(shù)的加法實現(xiàn)了 8 位數(shù)的加法,又通過加法實現(xiàn)減法。接下來我們又會用加法和減法實現(xiàn)除法,不過讓我們先來看一下如何實現(xiàn)乘法。根據(jù)小學(xué)的數(shù)學(xué)知識,只要把被乘數(shù)和乘數(shù)的每一位相乘再相加就可以了,這里面其實還會有進(jìn)位,還有與被乘數(shù)每一位相乘的操作其實還乘了位權(quán),比如乘以 21,這個 2 其實是 20,要補(bǔ)上與位權(quán)相應(yīng)的 0。
示例:
二進(jìn)制和十進(jìn)制的算法是一樣的,只是更簡單,因為被乘數(shù)字只會有 0 或 1 兩種,為 0 時結(jié)果也是 0,為 1 時數(shù)字不變,就只要補(bǔ) 0 就可以了,也就是左移操作。
示例:
我們先來實現(xiàn)左移,為了方便使用,我們用一個額外的參數(shù)指定左移時填補(bǔ)的數(shù)字:
type LShift<A extends Uint8, B extends number, P extends Digit> =B extends 1 ? [A[1], A[2], A[3], A[4], A[5], A[6], A[7], P]: B extends 2 ? [A[2], A[3], A[4], A[5], A[6], A[7], P, P]: B extends 3 ? [A[3], A[4], A[5], A[6], A[7], P, P, P]: B extends 4 ? [A[4], A[5], A[6], A[7], P, P, P, P]: B extends 5 ? [A[5], A[6], A[7], P, P, P, P, P]: B extends 6 ? [A[6], A[7], P, P, P, P, P, P]: B extends 7 ? [A[7], P, P, P, P, P, P, P]: B extends 0 ? A : [P, P, P, P, P, P, P, P];然后實現(xiàn)逐位的乘法,額外提供一個參數(shù)指示位移長度,這里左移填充0,但是后面我們還會用到第三個參數(shù):
type ZERO = [0, 0, 0, 0, 0, 0, 0, 0];type BitMul<A extends Uint8, B extends Digit, C extends Bits> =B extends 1 ? LShift<A, C, 0> : ZERO;最后實現(xiàn)完整的乘法,對每一位運(yùn)算的結(jié)果進(jìn)行累加就是乘積了:
type Uint8Mul<A extends Uint8, B extends Uint8> = Uint8Add<ZERO, BitMul<A, B[7], 0>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[6], 1>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[5], 2>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[4], 3>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[3], 4>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[2], 5>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[1], 6>> extends infer S? Uint8Add<AsUint8<S>, BitMul<A, B[0], 7>>: never : never : never : never : never : never : never;除法的算法也不過是小學(xué)知識,就是從被除數(shù)的最高位開始與除數(shù)比較,大于就相除并記錄商和余數(shù),不斷地將被除數(shù)的下一位補(bǔ)到余數(shù)的末位來,直到除完所有數(shù)位。先來看 10 進(jìn)制的:
二進(jìn)制的更簡單了,因為每次求余的結(jié)果,商要么為 0 要么為 1,也就是說不會超過 2 倍,只要比較大小相減即可:
先來實現(xiàn)一個比較器,從高位到低位比較,不相等就返回比較結(jié)果,相等就繼續(xù)比較下一位:
type EQ = 0; type GT = 1; type LT = 2;type BitCMP<A extends Digit, B extends Digit> =[[EQ, LT], [GT, EQ]][A][B];type Uint8CMP<A extends Uint8, B extends Uint8> =BitCMP<A[0], B[0]> extends GT | LT ? BitCMP<A[0], B[0]>: BitCMP<A[1], B[1]> extends GT | LT ? BitCMP<A[1], B[1]>: BitCMP<A[2], B[2]> extends GT | LT ? BitCMP<A[2], B[2]>: BitCMP<A[3], B[3]> extends GT | LT ? BitCMP<A[3], B[3]>: BitCMP<A[4], B[4]> extends GT | LT ? BitCMP<A[4], B[4]>: BitCMP<A[5], B[5]> extends GT | LT ? BitCMP<A[5], B[5]>: BitCMP<A[6], B[6]> extends GT | LT ? BitCMP<A[6], B[6]>: BitCMP<A[7], B[7]>;再實現(xiàn)用于迭代的簡單求余器,返回商和余數(shù)兩個值,被除數(shù)小于除數(shù)則商為 0 余數(shù)為被除數(shù),否則商為 1 余數(shù)為兩數(shù)之差,這里用到了我們前面實現(xiàn)的減法:
type Remainder<A extends Uint8, B extends Uint8> =Uint8CMP<A, B> extends LT ? [0, A] : [1, Uint8Sub<A, B>];最后,讓我們來實現(xiàn)完整的除法運(yùn)算:
type Uint8Div<A extends Uint8, B extends Uint8> =Remainder<LShift<ZERO, 1, A[0]>, B> extends [infer Q0, infer R]? Remainder<LShift<AsUint8<R>, 1, A[1]>, B> extends [infer Q1, infer R]? Remainder<LShift<AsUint8<R>, 1, A[2]>, B> extends [infer Q2, infer R]? Remainder<LShift<AsUint8<R>, 1, A[3]>, B> extends [infer Q3, infer R]? Remainder<LShift<AsUint8<R>, 1, A[4]>, B> extends [infer Q4, infer R]? Remainder<LShift<AsUint8<R>, 1, A[5]>, B> extends [infer Q5, infer R]? Remainder<LShift<AsUint8<R>, 1, A[6]>, B> extends [infer Q6, infer R]? Remainder<LShift<AsUint8<R>, 1, A[7]>, B> extends [infer Q7, infer R]? [AsUint8<[Q0, Q1, Q2, Q3, Q4, Q5, Q6, Q7]>, AsUint8<R>]: never : never : never : never : never : never : never : never;我們把被除數(shù)從高位到低位逐位后綴到每一步的余數(shù)后面,這里用到了我們左移操作的第三個參數(shù)。然后不斷對新的數(shù)求余,并保存每一位得到的商,然后返回最終的商和最終的余數(shù)。
最后,我們來定義幾個便于使用的類型:
// 加 type Add<A extends number, B extends number> =ToNumber<Uint8Add<ToUint8<A>, ToUint8<B>>>; // 減 type Sub<A extends number, B extends number> =ToNumber<Uint8Sub<ToUint8<A>, ToUint8<B>>>; // 乘 type Mul<A extends number, B extends number> =ToNumber<Uint8Mul<ToUint8<A>, ToUint8<B>>>; // 除 type Div<A extends number, B extends number> =B extends 0 ? never :ToNumber<Uint8Div<ToUint8<A>, ToUint8<B>>[0]>; // 取余 type Mod<A extends number, B extends number> =B extends 0 ? never :ToNumber<Uint8Div<ToUint8<A>, ToUint8<B>>[1]>;然后我們簡單的測試一下:
type case1_ShouldBe99 = Add<33, 66>; // 33 + 66 = 99 type case2_ShouldBe0 = Add<255, 1>; // 255 + 1 = 0 (overflow)type case3_ShouldBe99 = Sub<123, 24>; // 123 - 24 = 99 type case4_ShouldBe255 = Sub<0, 1>; // 0 - 1 = 255 (overflow)type case5_ShouldBe153 = Mul<17, 9>; // 17 x 9 = 153 type case6_ShouldBe253 = Mul<255, 3>; // 255 x 3 = 253 (overflow)type case7_ShouldBe33 = Div<100, 3>; // 100 / 3 = 33 type case8_ShouldBeNever = Div<1, 0>; // 1 / 0 = error (divide by 0)type case9_ShouldBe1 = Mod<100, 3>; // 100 % 3 = 1 type case10_ShouldBeNever = Mod<1, 0>; // 1 % 0 = error (divide by 0)最后放上 Playground 可以在線試一下(更新到了TS 4.0,和文章略有出入)。下一篇我再說一下如何用類型實現(xiàn)一個 parser 來從一個 token 數(shù)組中解析表達(dá)式和語法,會用到更多奇技淫巧,如果有人想看的話。
總結(jié)
以上是生活随笔為你收集整理的typescript 怎么表示当前时间减一个月_TypeScript类型元编程:实现8位数的算术运算...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双拼输入法键位图_谈谈小鹤双拼入门(1)
- 下一篇: 内存分段分页机制理解_深入理解虚拟机,J