简体   繁体   English

TypeScript 中的类型级 Catalan 函数

[英]Type-level Catalan function in TypeScript

Consider the following Catalan function in JavaScript.考虑 JavaScript 中的以下 Catalan 函数。

 class Pair { constructor(fst, snd) { this.fst = fst; this.snd = snd; } } const catalan = (x, xs) => { if (xs.length === 0) return [x]; const result = []; for (let i = 0; i < xs.length; i++) { const ys = catalan(x, xs.slice(0, i)); const zs = catalan(xs[i], xs.slice(i + 1)); for (const y of ys) for (const z of zs) result.push(new Pair(y, z)); } return result; }; const show = (x) => x instanceof Pair ? `(${show(x.fst)} <> ${show(x.snd)})` : JSON.stringify(x); const log = (x) => console.log(x); catalan(1, []).map(show).forEach(log); catalan(1, [2]).map(show).forEach(log); catalan(1, [2, 3]).map(show).forEach(log); catalan(1, [2, 3, 4]).map(show).forEach(log);

It returns all the possible ways of associating n applications of a binary operator, where n = xs.length .它返回关联二元运算符的n应用程序的所有可能方式,其中n = xs.length

I want to do something similar, but with types in TypeScript.我想做类似的事情,但使用 TypeScript 中的类型。 However, I don't know how to implement the “else” branch.但是,我不知道如何实现“else”分支。

class Pair<A, B> {
    constructor(public fst: A, public snd: B) {}
}

type Catalan<X, XS extends unknown[]> = XS extends []
    ? X
    : /* how to define this “else” branch? */;

type C0 = Catalan<1, []>; // 1

type C1 = Catalan<1, [2]>; // Pair<1, 2>

type C2 = Catalan<1, [2, 3]>; // Pair<1, Pair<2, 3>> | Pair<Pair<1, 2>, 3>

type C3 = Catalan<1, [2, 3, 4]>; /* Pair<1, Pair<2, Pair<3, 4>>> |
                                  * Pair<1, Pair<Pair<2, 3>, 4>> |
                                  * Pair<Pair<1, 2>, Pair<3, 4>> |
                                  * Pair<Pair<1, Pair<2, 3>>, 4> |
                                  * Pair<Pair<Pair<1, 2>, 3>, 4>
                                  * /

Any help will be greatly appreciated.任何帮助将不胜感激。 By the way, I want to use this Catalan type to define the following function.顺便说一句,我想用这个Catalan类型来定义下面的函数。

declare const flatten: <X, XS extends unknown[]>(
    x: Catalan<X, XS>
) => [X, ...XS];

Here's how the flatten function is implemented in JavaScript.下面是如何在 JavaScript 中实现flatten函数。

 class Pair { constructor(fst, snd) { this.fst = fst; this.snd = snd; } } const catalan = (x, xs) => { if (xs.length === 0) return [x]; const result = []; for (let i = 0; i < xs.length; i++) { const ys = catalan(x, xs.slice(0, i)); const zs = catalan(xs[i], xs.slice(i + 1)); for (const y of ys) for (const z of zs) result.push(new Pair(y, z)); } return result; }; const flatten = (x) => x instanceof Pair ? [...flatten(x.fst), ...flatten(x.snd)] : [x]; const log = (x) => console.log(JSON.stringify(x)); catalan(1, []).map(flatten).forEach(log); catalan(1, [2]).map(flatten).forEach(log); catalan(1, [2, 3]).map(flatten).forEach(log); catalan(1, [2, 3, 4]).map(flatten).forEach(log);


Edit: If it helps, here's an implementation of the value-level catalan function in Haskell.编辑:如果有帮助,这里是 Haskell 中值级catalan函数的实现。

import Data.List (inits, tails)

data Catalan a = Catalan a :<>: Catalan a | Lift a deriving Show

split :: [a] -> [([a], [a])]
split = init . (zipWith (,) <$> inits <*> tails)

catalan :: a -> [a] -> [Catalan a]
catalan x [] = [Lift x]
catalan x xs = do
    (ys, z:zs) <- split xs
    y <- catalan x ys
    z <- catalan z zs
    return $ y :<>: z

main :: IO ()
main = do
    mapM_ print $ catalan 1 []
    mapM_ print $ catalan 1 [2]
    mapM_ print $ catalan 1 [2, 3]
    mapM_ print $ catalan 1 [2, 3, 4]

Here's the output of the above Haskell program.这是上述 Haskell 程序的输出。

Lift 1
Lift 1 :<>: Lift 2
Lift 1 :<>: (Lift 2 :<>: Lift 3)
(Lift 1 :<>: Lift 2) :<>: Lift 3
Lift 1 :<>: (Lift 2 :<>: (Lift 3 :<>: Lift 4))
Lift 1 :<>: ((Lift 2 :<>: Lift 3) :<>: Lift 4)
(Lift 1 :<>: Lift 2) :<>: (Lift 3 :<>: Lift 4)
(Lift 1 :<>: (Lift 2 :<>: Lift 3)) :<>: Lift 4
((Lift 1 :<>: Lift 2) :<>: Lift 3) :<>: Lift 4

updated may 19 5月19日更新

Oh boy, we aren't done yet.哦,男孩,我们还没有完成。 We can make this thing even faster!我们可以让这件事变得更快!

The first thing you can do is transform the extends in Catalan to only:您可以做的第一件事是将Catalan中的 extends 转换为:

type Catalan<X, XS extends List> = ({
    "0": X;
    "1": Pair<X, XS[0]>;
} & {
    [_: `${number}`]: CatalanLoop<X, XS>;
})[`${XS["length"]}`];

This makes it extremely fast.这使它非常快。 It's only a lookup table now.它现在只是一个查找表。

Then instead of big clunky loop for CatalanLoop , we can use distributive conditional types!然后,我们可以使用分布式条件类型,而不是CatalanLoop的大笨重循环!

type CatalanLoop<X, XS extends List, K extends keyof XS & `${bigint}` = keyof XS & `${bigint}`> =
        K extends K
            ? Partition<XS, K> extends infer P
                ? P extends [List, List]
                    ? P extends P
                        ? CatalanPairs<X, XS, P, K>
                        : never
                    : never
                : never
            : never

And you'll notice a new type to help with the distributing:你会注意到一种新的类型来帮助分发:

type CatalanPairs<X, XS extends List, P extends [List, List], K extends keyof XS> = K extends K ? Pair<Catalan<X, P[0]>, Catalan<XS[K], P[1]>> : never;

Try this new playground to see the effects of these changes.试试这个新的游乐场,看看这些变化的效果。


When encountering type-level problems like these, it's best to look at the original code and look for patterns, or anything that the type system can do for you.当遇到此类类型级别的问题时,最好查看原始代码并寻找模式,或者类型系统可以为您做的任何事情。

So let's begin:那么让我们开始吧:

const catalan = (x, xs) => {
    if (xs.length === 0) return [x];
    const result = [];
    for (let i = 0; i < xs.length; i++) {
        const ys = catalan(x, xs.slice(0, i));
        const zs = catalan(xs[i], xs.slice(i + 1));
        for (const y of ys) for (const z of zs) result.push(new Pair(y, z));
    }
    return result;
};

First we notice that if xs is empty, then we directly return x .首先我们注意到如果xs为空,那么我们直接返回x We make a mental note to use XS["length"] extends 0 ? X : ...我们记下使用XS["length"] extends 0 ? X : ... XS["length"] extends 0 ? X : ... later. XS["length"] extends 0 ? X : ...稍后。

Then we see that this:然后我们看到:

const ys = catalan(x, xs.slice(0, i));
const zs = catalan(xs[i], xs.slice(i + 1));

is really just partitioning xs in such a way that:实际上只是以这样的方式对xs进行分区:

partition [1, 2, 3, 4, 5] at 3 => [1, 2, 3] [5]

In other words, we split the tuple at index 3 and return the two halves.换句话说,我们在索引 3 处拆分元组并返回两半。 This will be much faster than slicing the tuple two times individually and can be implemented without much trouble.这将比单独对元组切片两次要快得多,并且可以毫不费力地实现。

Finally we notice this nested loop:最后我们注意到这个嵌套循环:

for (const y of ys) for (const z of zs) result.push(new Pair(y, z));

No need for this in the type system, we can simply do:在类型系统中不需要这个,我们可以简单地做:

Pair<YS, ZS>

and have it generate all the possible pairs for us from the unions.并让它从工会中为我们生成所有可能的配对。

Alright, time to get cracking away at the solution.好吧,是时候破解解决方案了。

Recall that x is returned if xs is empty:回想一下,如果xs为空,则返回x

type Catalan<X, XS extends ReadonlyArray<unknown>> = 
  XS["length"] extends 0 ? X : 

And also when XS is only one element then we return that pair.而且当XS只是一个元素时,我们返回那对。 If XS has more than one element, we enter the loop instead:如果XS有多个元素,我们进入循环:

... : XS["length"] extends 1 ? Pair<X, XS[0]> : CatalanLoop<X, XS>;

Let's see the loop now:现在让我们看看循环:

type CatalanLoop<X, XS extends ReadonlyArray<unknown>> = {
  [K in keyof XS & `${bigint}`]: ...
}[keyof XS & `${bigint}`];

Now, what's this funny looking thing:现在,这个看起来很有趣的东西是什么:

keyof XS & `${bigint}`

keyof XS would give us something in the form of number | "0" | "1" | "2" | "at" | "concat" | "..." keyof XS会给我们一些number | "0" | "1" | "2" | "at" | "concat" | "..." number | "0" | "1" | "2" | "at" | "concat" | "..." number | "0" | "1" | "2" | "at" | "concat" | "..." , but we only want the indices of XS . number | "0" | "1" | "2" | "at" | "concat" | "..." ,但我们只想要XS的索引。 If we intersect keyof XS with the interpolated bigint , we get the desired "0" | "1" | "2"如果我们keyof XS与插值bigint相交,我们会得到所需的"0" | "1" | "2" "0" | "1" | "2" "0" | "1" | "2" only."0" | "1" | "2"

That means this is just like the loop in the original code!这意味着这就像原始代码中的循环一样! We loop over each index using a mapped type.我们使用映射类型遍历每个索引。

Inside the loop body we partition XS at index K :在循环体内,我们在索引K处对XS进行分区:

type CatalanLoop<X, XS extends ReadonlyArray<unknown>> = {
  [K in keyof XS & `${bigint}`]:
    Partition<XS, K> extends [infer Left, infer Right]
      ? ...
      : ...
}[keyof XS & `${bigint}`];

But we have to assert to TypeScript that our partitioning type will definitely give us tuples like this first:但是我们必须向 TypeScript 断言,我们的分区类型肯定会首先给我们这样的元组:

    Partition<XS, K> extends [infer Left, infer Right]
      ? Left extends ReadonlyArray<unknown>
        ? Right extends ReadonlyArray<unknown>

Then we call Catalan and make our pairs:然后我们调用Catalan并进行配对:

          ? Catalan<X, Left> extends infer YS
            ? Catalan<XS[K], Right> extends infer ZS 
              ? Pair<YS, ZS>

This is doing what this original code does:这是做这个原始代码所做的事情:

const ys = catalan(x, xs.slice(0, i));
const zs = catalan(xs[i], xs.slice(i + 1));
for (const y of ys) for (const z of zs) result.push(new Pair(y, z));

And let's close off all our ternaries/conditionals with never (because these clauses should never be reached anyways):让我们用never关闭所有的三元/条件语句(因为无论如何都不应该达到这些子句):

              : never
            : never
          : never
        : never
      : never

Finally, we need to make our partitioning type.最后,我们需要创建我们的分区类型。

To do that, we need a type to increment a number.为此,我们需要一个类型来增加一个数字。 This can be done with a tuple like this:这可以通过这样的元组来完成:

type Increment = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33];

Increment[0]  // => 1
Increment[15] // => 16
Increment[32] // => 33

Now that we can increment a number, we define Partition :现在我们可以增加一个数字,我们定义Partition

type Partition<
  XS extends ReadonlyArray<unknown>,
  At extends string,
  Index extends number = 0,
  Left extends ReadonlyArray<unknown> = [],
> = XS extends [infer First, ...infer Rest]
    ? `${Index}` extends At
      ? [Left, Rest]
      : Partition<Rest, At, Increment[Index], [...Left, First]>
    : never

This type loops over XS until it hits At , the index to partition at.这种类型在XS上循环,直到它At ,即要分区的索引。 It excludes the element at At and stops, giving us [Left, Rest] , the two halves.它排除了At和停止处的元素,给了我们[Left, Rest] ,两半。 Partition is the type that replaces xs.slice(0, i) and xs.slice(i + 1) . Partition是替换xs.slice(0, i)xs.slice(i + 1)的类型。

Lastly, just for kicks, let's also make a type to mimic the original show function:最后,为了好玩,让我们也创建一个类型来模仿原始的show功能:

type Show<Pairs> = Pairs extends Pair<infer A, infer B> ? `(${Show<A>} <> ${Show<B>})` : `${Pairs & number}`;

And wow!哇! It really does work!它确实有效!

type ShowFifth = Show<Catalan<1, [2, 3, 4, 5]>>;
// =>
// | "(1 <> (2 <> (3 <> (4 <> 5))))"
// | "(1 <> (2 <> ((3 <> 4) <> 5)))"
// | "(1 <> ((2 <> 3) <> (4 <> 5)))"
// | "(1 <> ((2 <> (3 <> 4)) <> 5))"
// | "(1 <> (((2 <> 3) <> 4) <> 5))"
// | "((1 <> 2) <> (3 <> (4 <> 5)))"
// | "((1 <> 2) <> ((3 <> 4) <> 5))"
// | "((1 <> (2 <> 3)) <> (4 <> 5))"
// | "((1 <> (2 <> (3 <> 4))) <> 5)"
// | "((1 <> ((2 <> 3) <> 4)) <> 5)"
// | "(((1 <> 2) <> 3) <> (4 <> 5))"
// | "(((1 <> 2) <> (3 <> 4)) <> 5)"
// | "(((1 <> (2 <> 3)) <> 4) <> 5)"
// | "((((1 <> 2) <> 3) <> 4) <> 5)"

To end off this little adventure, a playground where you can play around with this yourself.为了结束这个小冒险, 一个您可以自己玩耍的游乐场

So here is my attempt:所以这是我的尝试:

First of all, I am not sure if I understood the Catalan algorithm correctly.首先,我不确定我是否正确理解了加泰罗尼亚语算法。 I created this type purely by looking at the examples you gave.我纯粹是通过查看您提供的示例来创建这种类型的。 You need to test if this works for bigger tuples as well.您需要测试这是否也适用于更大的元组。

Explanation解释

I used some utilities to solve this problem.我使用了一些实用程序来解决这个问题。 I needed a type to splice arrays, so I used the type Splice from here .我需要一个类型来splice数组,所以我使用了 这里Splice类型。

type Absolute<T extends string | number | bigint> = T extends string
  ? T extends `-${infer R}`
    ? R
    : T
  : Absolute<`${T}`>;

type isNegative<T extends number> = `${T}` extends `-${infer _}` ? true : false;

type SliceLeft<
  Arr extends any[],
  Index extends number,
  Tail extends any[] = []
> = Tail["length"] extends Index
  ? [Tail, Arr]
  : Arr extends [infer Head, ...infer Rest]
  ? SliceLeft<Rest, Index, [...Tail, Head]>
  : [Tail, []];

type SliceRight<
  Arr extends any[],
  Index extends string,
  Tail extends any[] = []
> = `${Tail["length"]}` extends Index
  ? [Arr, Tail]
  : unknown extends Arr[0]
  ? [[], Tail]
  : Arr extends [...infer Rest, infer Last]
  ? SliceRight<Rest, Index, [Last, ...Tail]>
  : [[], Tail];

type SliceIndex<
  Arr extends any[],
  Index extends number
> = isNegative<Index> extends false
  ? SliceLeft<Arr, Index>
  : SliceRight<Arr, Absolute<Index>>;

type Slice<
  Arr extends any[],
  Start extends number = 0,
  End extends number = Arr["length"]
> = SliceIndex<SliceIndex<Arr, End>[0], SliceIndex<Arr, Start>[0]["length"]>[1];

I also needed a way to convert the indizes of the array produzed by keyof X (which are strings) back to numbers.我还需要一种方法将由keyof X (它们是字符串)产生的数组的 indizes 转换回数字。 I used this helper type:我使用了这个助手类型:

type Indizes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

Now to the algorithm itself.现在到算法本身。

It was not clear to me why X and XS needed to be seperate things.我不清楚为什么XXS需要分开。 For the first step I took X and XS and merged them into a single array.第一步,我采用XXS并将它们合并到一个数组中。

type Catalan<X, XS extends unknown[]> = _Catalan<[X, ...XS]>

To evaluate the result I created the recursive type _Catalan which takes a tuple.为了评估结果,我创建了递归类型_Catalan ,它采用一个元组。 The first two steps are simple.前两个步骤很简单。 If the tuple is of length 1, return the element inside the array.如果元组的length 1,则返回数组内的元素。 If it is of length 2 ,return a Pair of the first two elements.如果它的length 2 ,则返回前两个元素的Pair

X["length"] extends 1 
  ? X[0]
  : X["length"] extends 2 
    ? Pair<X[0], X[1]>
    : /* ... */

The rest is a bit more complicated.其余的有点复杂。 My approach was to "cut" the array once at every possible index and recursively call _Catalan with both resulting sub-arrays.我的方法是在每个可能的索引处“剪切”一次数组,并使用两个结果子数组递归调用_Catalan

[ 1 , 2 , 3 , 4 ]

    |                <- first cut here

[ 1 ] [ 2 , 3 , 4 ]

          |          <- next cut here

[ 1 ] [ 2 ] [ 3 , 4 ]

=> Pair<1, Pair<2, Pair<3,4>>

So on the highest level I iterate through each element and convert all iterations to a union with [keyof X & '${bigint}'] .因此,在最高级别上,我遍历每个元素并将所有迭代转换为带有[keyof X & '${bigint}']的联合。

{
  [I in keyof X & `${bigint}`]: /* ... */
}[keyof X & `${bigint}`]

I then convert the index to the corresponding number and also skip the first iteration since we only need to cut the array n - 1 times.然后我将索引转换为相应的数字并跳过第一次迭代,因为我们只需要切割数组n - 1次。

I extends keyof Indizes 
  ? Indizes[I] extends 0
    ? never
    : /* ... */
  : never

And last of all I create both sub-arrays with Slice and create a Pair of both sub-arrays by calling _Catalan with them.最后,我使用Slice创建两个子数组,并通过调用_Catalan来创建两个子数组的一Pair

Pair<_Catalan<Slice<X, 0, Indizes[I]>>, _Catalan<Slice<X, Indizes[I], X["length"]>>>

Result结果

The end result looks like this:最终结果如下所示:

type _Catalan<X extends unknown[]> = X["length"] extends 1 
  ? X[0]
  : X["length"] extends 2 
    ? Pair<X[0], X[1]>
    : {
        [I in keyof X & `${bigint}`]: I extends keyof Indizes 
          ? Indizes[I] extends 0
            ? never
            : Indizes[I] extends number 
              ? Pair<_Catalan<Slice<X, 0, Indizes[I]>>, _Catalan<Slice<X, Indizes[I], X["length"]>>>
              : never
          : never
    }[keyof X & `${bigint}`]

type Catalan<X, XS extends unknown[]> = _Catalan<[X, ...XS]>

Usage:用法:

class Pair<A, B> {
    constructor(public fst: A, public snd: B) {}
}

type _Catalan<X extends unknown[]> = X["length"] extends 1 
  ? X[0]
  : X["length"] extends 2 
    ? Pair<X[0], X[1]>
    : {
        [I in keyof X & `${bigint}`]: I extends keyof Indizes 
          ? Indizes[I] extends 0
            ? never
            : Indizes[I] extends number 
              ? Pair<_Catalan<Slice<X, 0, Indizes[I]>>, _Catalan<Slice<X, Indizes[I], X["length"]>>>
              : never
          : never
    }[keyof X & `${bigint}`]

type Catalan<X, XS extends unknown[]> = _Catalan<[X, ...XS]>

type C0 = Catalan<1, []>; // 1

type C1 = Catalan<1, [2]>; // Pair<1, 2>

type C2 = Catalan<1, [2, 3]>; // Pair<1, Pair<2, 3>> | Pair<Pair<1, 2>, 3>

type C3 = Catalan<1, [2, 3, 4]>; /* Pair<1, Pair<2, Pair<3, 4>>> |
                                  * Pair<1, Pair<Pair<2, 3>, 4>> |
                                  * Pair<Pair<1, 2>, Pair<3, 4>> |
                                  * Pair<Pair<1, Pair<2, 3>>, 4> |
                                  * Pair<Pair<Pair<1, 2>, 3>, 4>
                                  */

If you hover over the type C3 , you will notice that it looks different from what you specified.如果您将鼠标悬停在类型C3上,您会注意到它看起来与您指定的不同。 There are only 3 top-level unions and each member will have unions inside the Pair s.只有3顶级联合,每个成员在Pair中都有联合。

Pair<1, Pair<2, Pair<3, 4>> | Pair<Pair<2, 3>, 4>>
//instead of 
Pair<1, Pair<2, Pair<3, 4>>> | Pair<1, Pair<Pair<2, 3>, 4>>

But this is purely a visual thing and they should functionally not be different.但这纯粹是视觉上的东西,它们在功能上应该没有什么不同。

Some tests to validate the result:一些验证结果的测试:

type Test1 = Pair<1, Pair<2, Pair<3, 4>>> extends C3 ? true : false
type Test2 = Pair<1, Pair<Pair<2, 3>, 4>> extends C3 ? true : false
type Test3 = Pair<Pair<1, 2>, Pair<3, 4>> extends C3 ? true : false
type Test4 = Pair<Pair<1, Pair<2, 3>>, 4> extends C3 ? true : false
type Test5 = Pair<Pair<Pair<1, 2>, 3>, 4> extends C3 ? true : false

Playground 操场

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM