From 8d3b2e3b735af416b2b063909088536c49c0967b Mon Sep 17 00:00:00 2001 From: Lee Yi Date: Fri, 11 Aug 2023 13:23:38 +0800 Subject: [PATCH] Fix list accumulate (#1463) * Fix accumulate and add tests for it * Update gitignore to ignore yarn errors --- .gitignore | 2 + .../__tests__/__snapshots__/list.ts.snap | 54 +++++++++++++++++++ src/stdlib/__tests__/list.ts | 41 ++++++++++++++ src/stdlib/list.ts | 26 +++++---- tsconfig.json | 2 +- 5 files changed, 114 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 7bb67a701..a2a7602a5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ coverage/ .vscode/ tsconfig.tsbuildinfo test-report.html + +yarn-error.log \ No newline at end of file diff --git a/src/stdlib/__tests__/__snapshots__/list.ts.snap b/src/stdlib/__tests__/__snapshots__/list.ts.snap index 7ba394d08..e0d1de59a 100644 --- a/src/stdlib/__tests__/__snapshots__/list.ts.snap +++ b/src/stdlib/__tests__/__snapshots__/list.ts.snap @@ -286,6 +286,32 @@ Object { } `; +exports[`accumulate works from right to left: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": "accumulate((curr, acc) => curr + acc, '1', list('4','3','2'));", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": "4321", + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`accumulate works properly: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": "accumulate((curr, acc) => curr + acc, 0, list(2, 3, 4, 1));", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": 10, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + exports[`accumulate: expectResult 1`] = ` Object { "alertResult": Array [], @@ -789,6 +815,34 @@ Object { } `; +exports[`length works with empty lists: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": "const xs = list(); +length(xs);", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": 0, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`length works with populated lists: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": "const xs = list(1,2,3,4); +length(xs);", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": 4, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + exports[`list creates list: expectResult 1`] = ` Object { "alertResult": Array [], diff --git a/src/stdlib/__tests__/list.ts b/src/stdlib/__tests__/list.ts index ef0cd364b..e915b3678 100644 --- a/src/stdlib/__tests__/list.ts +++ b/src/stdlib/__tests__/list.ts @@ -246,6 +246,47 @@ test('list_to_string', () => { ).toMatchInlineSnapshot(`"[1,[2,[3,null]]]"`) }) +describe('accumulate', () => { + test('works properly', () => { + return expectResult( + stripIndent` + accumulate((curr, acc) => curr + acc, 0, list(2, 3, 4, 1)); + `, + { chapter: Chapter.SOURCE_2, native: true } + ).toMatchInlineSnapshot(`10`) + }) + + it('works from right to left', () => { + return expectResult( + stripIndent` + accumulate((curr, acc) => curr + acc, '1', list('4','3','2'));`, + { chapter: Chapter.SOURCE_2, native: true } + ).toMatchInlineSnapshot('"4321"') + }) +}) + +describe('length', () => { + test('works with populated lists', () => { + return expectResult( + stripIndent` + const xs = list(1,2,3,4); + length(xs); + `, + { chapter: Chapter.SOURCE_2, native: true } + ).toMatchInlineSnapshot('4') + }) + + test('works with empty lists', () => { + return expectResult( + stripIndent` + const xs = list(); + length(xs); + `, + { chapter: Chapter.SOURCE_2, native: true } + ).toMatchInlineSnapshot('0') + }) +}) + // assoc removed from Source test.skip('assoc', () => { return expectResult( diff --git a/src/stdlib/list.ts b/src/stdlib/list.ts index af91e2f3b..3b917196e 100644 --- a/src/stdlib/list.ts +++ b/src/stdlib/list.ts @@ -27,7 +27,7 @@ export function pair(x: H, xs: T): Pair { // is_pair returns true iff arg is a two-element array // LOW-LEVEL FUNCTION, NOT SOURCE -export function is_pair(x: any) { +export function is_pair(x: any): x is Pair { return array_test(x) && x.length === 2 } @@ -55,7 +55,7 @@ export function tail(xs: any) { // is_null returns true if arg is exactly null // LOW-LEVEL FUNCTION, NOT SOURCE -export function is_null(xs: List) { +export function is_null(xs: List): xs is null { return xs === null } @@ -71,7 +71,7 @@ export function list(...elements: any[]): List { // recurses down the list and checks that it ends with the empty list null // LOW-LEVEL FUNCTION, NOT SOURCE -export function is_list(xs: List) { +export function is_list(xs: List): xs is List { while (is_pair(xs)) { xs = tail(xs) } @@ -129,14 +129,20 @@ export function set_tail(xs: any, x: any) { } } -export function accumulate(acc: (each: T, result: U) => any, init: U, xs: List): U { - const recurser = (xs: List, result: U): U => { - if (is_null(xs)) return result - - return recurser(tail(xs), acc(head(xs), result)) +/** + * Accumulate applies given operation op to elements of a list + * in a right-to-left order, first apply op to the last element + * and an initial element, resulting in r1, then to the second-last + * element and r1, resulting in r2, etc, and finally to the first element + * and r_n-1, where n is the length of the list. `accumulate(op,zero,list(1,2,3))` + * results in `op(1, op(2, op(3, zero)))` + */ +export function accumulate(op: (each: T, result: U) => U, initial: U, sequence: List): U { + // Use CPS to prevent stack overflow + function $accumulate(xs: typeof sequence, cont: (each: U) => U): U { + return is_null(xs) ? cont(initial) : $accumulate(tail(xs), x => cont(op(head(xs), x))) } - - return recurser(xs, init) + return $accumulate(sequence, x => x) } export function length(xs: List): number { diff --git a/tsconfig.json b/tsconfig.json index 2bd9aa318..5cf50d62c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,7 @@ "src/stdlib/**/*.js", "node_modules", "dist", - "sicp_publish" + "sicp_publish", ], "types": [ "typePatches",