ค้นหาเว็บไซต์

ประเภท TypeScript สำหรับอาร์เรย์ที่สับอย่างสมบูรณ์


คำถามนี้เพื่อความสนุกสนานเท่านั้น ไม่ใช่สิ่งที่น่าจะต้องใช้ในโลกจริง แต่รู้สึกว่าไม่ถูกต้องที่จะเอาไปลงใน Code Golf เพราะมันเฉพาะเจาะจงมาก

สมมติว่าฉันมี JavaScript:

function shuffle(array) {
    if (array.length === 0 || array.length === 1) {
        return array;
    }

    const rest = array.slice();
    const middle = rest.splice(Math.ceil(array.length / 2), 1)[0];
    const first = rest.shift();

    return [first, middle, ...shuffle(rest)];
}

const array = [1, 2, 3, 4, 5, 6, 7, 8];
const shuffled = shuffle(array); // [ 1, 5, 2, 6, 3, 7, 4, 8 ]
console.log(shuffled);

ฟังก์ชันนี้โดยพื้นฐานแล้วจะสับ array เหมือนกับเป็นสำรับไพ่ที่สับอย่างสมบูรณ์ เริ่มจากไพ่บนสุด จากนั้นไพ่กลาง จากนั้นไพ่ที่สอง แล้วไพ่ที่อยู่หลังไพ่กลาง เป็นต้น อีกวิธีหนึ่งที่จะทำได้คือการแบ่ง array ออกเป็นสองส่วน แล้วรวมสองส่วนเข้าด้วยกันแบบ zip จากนั้นทำให้ array แบนลง

เป็นไปได้ไหมที่จะสร้าง type ใน TypeScript ที่จะรู้วิธีการเปลี่ยน array ให้เป็น shuffled?

นี่คือความพยายามที่ดีที่สุดของฉัน

type Shuffle<T extends readonly [...any[]]> = 
    T['length'] extends 0 | 1 ? T : 
    T extends [...infer A, ...infer B]
        ? A['length'] extends B['length'] 
            ? [A, B] extends [[infer AH, ...infer AR], [infer BH, ...infer BR]] 
                ? [AH, BH, ...Shuffle<[...AR, ...BR]>]
                : never
            : never
        : never;

function shuffle<T extends readonly any[]>(array: T): Shuffle<T> {
    if (array.length === 0 || array.length === 1) {
        return array.slice();
    }

    const rest = array.slice();
    const middle = rest.splice(Math.ceil((array.length - 1) / 2), 1)[0];
    const first = rest.shift();

    return [first, middle, ...shuffle(rest)];
}

const array = [1, 2, 3, 4, 5, 6, 7, 8] as const;
const shuffled = shuffle(array);

type Shuffled = typeof shuffled; // never

console.log(shuffle(array));

สนามเด็กเล่น TypeScript

ชัดเจนว่าจุดสำคัญคือการหาวิธีแบ่งอาเรย์ครึ่งหนึ่ง

บางทีฉันอาจจะสร้างอาร์เรย์สองตัวแบบเรียกซ้ำได้ โดยแต่ละครั้งดึงสมาชิกออกจากจุดเริ่มต้นและจุดสิ้นสุด และวางมันในลำดับที่ถูกต้อง.... (เมื่อฉันมีไอเดียนี้แล้ว ฉันก็แก้ปัญหาได้ ดังนั้นฉันจะโพสต์คำตอบของฉัน)

คำตอบ #1

ประเภทเชิงเงื่อนซ้อนแบบเรียกตัวเองและประเภททูเปิลแบบแปรผันทำให้สามารถทำได้ แต่ก็ยังค่อนข้างยุ่งยาก ในการทำการจัดการประเภทแบบที่คุณพูดถึง

ฉันคิดว่าสิ่งต่อไปนี้เป็นแนวทางทั่วไปเดียวกับที่คุณใช้ในคำตอบของคุณ แม้ว่ารายละเอียดที่แน่นอนจะแตกต่างกันเล็กน้อย ฉันพยายามทำให้ประเภทต่าง ๆ เป็นแบบ "เรียบง่าย" ที่สุดเท่าที่ฉันทำได้ แต่อย่างไรก็ตามฉันจะปล่อยให้ผู้อื่นตัดสินว่าความพยายามนั้นประสบความสำเร็จมากน้อยแค่ไหน:


ฉันจะกำหนดฟังก์ชันประเภทที่เรียกว่า FirstHalf<T> ซึ่งจะส่งกลับครึ่งแรกของทูเพิล T หาก T มีจำนวนสมาชิกเป็นเลขคี่ FirstHalf<T> จะรวมถึงสมาชิกตรงกลางด้วย ดังนั้น FirstHalf<[1,2,3,4]> ควรเป็น [1,2] และ FirstHalf<[1,2,3,4,5]> ควรเป็น [1,2,3]:

type FirstHalf<T extends any[]> = T extends [infer F, ...infer M, any] ?
    [F, ...FirstHalf<M>] : T

และจากนั้น LastHalf<T> จะคืนค่าเศษครึ่งหลังของชนิดทูเพิล T โดยการสร้างส่วนเติมเต็มของ FirstHalf<T> ซึ่งหมายความว่าถ้ามีจำนวนสมาชิกเป็นเลขคี่ใน T สมาชิกตรงกลางจะ ไม่ อยู่ใน LastHalf<T> ดังนั้น LastHalf<[1,2,3,4]> ควรเป็น [3,4] และ LastHalf<[1,2,3,4,5]> ควรเป็น [4,5]:

type LastHalf<T extends any[]> = T extends [...FirstHalf<T>, ...infer E] ? E : never;

จากนั้น, Interleave<T, U> จะสลับองค์ประกอบจาก T กับองค์ประกอบจาก U, ดังนั้น Interleave<[1,2,3],["A","B","C"]> ควรเป็น [1,"A",2,"B",3,"C"]. ถ้า T หรือ U มีองค์ประกอบเหลืออยู่ ส่วนที่เหลือจะอยู่ด้านท้าย ดังนั้น Interleave<[1,2],["A","B","C","D","E"]> ควรเป็น [1,"A",2,"B","C","D","E"]:

type Interleave<T extends any[], U extends any[]> =
    T extends [infer H, ...infer R] ? [H, ...Interleave<U, R>] : U

(โปรดทราบว่านี่เกิดขึ้นโดยการสลับบทบาทของ T และ U ในการเรียกแบบเรียกซ้ำ)

สุดท้าย, Shuffle<T> จะแยก T ออกเป็น FirstHalf และ LastHalf และ Interleave ครึ่งสองส่วนเข้าด้วยกัน:

type Shuffle<T extends any[]> = Interleave<FirstHalf<[...T]>, LastHalf<[...T]>>;   

ฉันไม่ได้เปลี่ยนแปลงการใช้งานของฟังก์ชัน shuffle() ของคุณ; ใช่ คุณต้องใช้การยืนยันชนิดข้อมูลหรือลักษณะอื่นเพื่อโน้มน้าวคอมไพเลอร์ว่าการใช้งานนี้ปลอดภัย แทบไม่มีโอกาสที่คอมไพเลอร์จะสามารถติดตามได้ว่าการปรับเปลี่ยนอาร์เรย์ใด ๆ จะเปลี่ยน T เป็น Shuffle<T> ในตัวอย่างต่อไปนี้ ฉันใช้ฟังก์ชันโอเวอร์โหลดที่มีลายเซ็นเรียกใช้เพียงครั้งเดียวเพื่อหลีกเลี่ยงการต้องใช้การยืนยันภายในตัวฟังก์ชัน:

function shuffle<T extends readonly any[]>(array: T): Shuffle<[...T]>;
function shuffle(array: any[]) {
    if (array.length === 0 || array.length === 1) {
        return array.slice();
    }
    const rest = array.slice();
    const middle = rest.splice(Math.ceil(array.length / 2), 1)[0];
    const first = rest.shift();
    return [first, middle, ...shuffle(rest)];
};

แล้วมาทดลองมันกันเถอะ:

const shuffled = shuffle([1, 2, 3, 4, 5, 6, 7, 8] as const);
// const shuffled: [1, 5, 2, 6, 3, 7, 4, 8]
console.log(shuffled); // [1, 5, 2, 6, 3, 7, 4, 8]

const oddShuffled = shuffle([1, 2, 3, 4, 5, 6, 7] as const);
// const oddShuffled: [1, 5, 2, 6, 3, 7, 4]
console.log(oddShuffled); // [1, 5, 2, 6, 3, 7, 4] 

ใช่ ดูดีนะ!

ลิงก์ Playground ไปยังโค้ด

คำตอบ #2

ตามที่สัญญาไว้ นี่คือวิธีแก้ปัญหาที่ฉันคิดออก

type SplitEven<T extends readonly any[], Start extends any[] = [], End extends any[] = []> =
    T['length'] extends 0 | 1 ? [[...Start, ...T], End] :
    T extends readonly [infer S, ...infer M, infer E] ? SplitEven<M, [...Start, S], [E, ...End]> :
    never;

type Shuffle<T extends readonly any[]> =
    T['length'] extends 0 | 1
        ? T
        : SplitEven<T> extends infer A
            ? A extends readonly [readonly [infer AH, ...infer AR], readonly [infer BH, ...infer BR]]
                ? [AH, BH, ...Shuffle<[...AR, ...BR]>]
                : never
            : never;

function shuffle<T extends readonly any[]>(array: T): Shuffle<T> {
    if (array.length === 0 || array.length === 1) {
        return array.slice() as Shuffle<T>;
    }

    const rest = array.slice();
    const middle = rest.splice(Math.ceil(array.length / 2), 1)[0];
    const first = rest.shift();

    return [first, middle, ...shuffle(rest)] as Shuffle<T>;
}

const array = [1, 2, 3, 4, 5, 6, 7, 8] as const;
const shuffled = shuffle(array);

type Shuffled = typeof shuffled; // [1, 5, 2, 6, 3, 7, 4, 8]

console.log(shuffled);

สนามเด็กเล่น TypeScript

ทั้งฟังก์ชันและประเภทก็ทำงานกับอาร์เรย์ที่มีความยาวเป็นคี่ด้วยเช่นกัน

สิ่งเดียวที่เหลือคือหาวิธีว่าฉันสามารถหลีกเลี่ยง as Shuffle<T> ในคำสั่ง return ได้หรือไม่ ฉันสงสัยว่าคำสั่งที่สองเกินความสามารถของ TSC เพราะมันจะต้องรู้ว่า rest, middle และ first ถูกกำหนดอย่างไร แต่ฉันก็แปลกใจที่คำสั่งแรกไม่ได้รับการอนุมาน