Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion src/bundles/repeat/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
runAsyncGenerator
} from '@sourceacademy/modules-testplugin';
import { describe, expect, it } from 'vitest';
import { repeat, thrice, twice } from '../functions';
import { repeat, repeat_apply, thrice, twice } from '../functions';

async function makePlusOne(handler: TestDataHandler) {
return closureFromFunction(
Expand Down Expand Up @@ -78,3 +78,36 @@ describe(repeat, () => {
.rejects.toThrow('repeat: Expected integer ≥ 0, got 1.5.');
});
});

describe(repeat_apply, () => {
it('applies a closure to a value n times', async () => {
const handler = new TestDataHandler();
const plusOne = await makePlusOne(handler);
const result = await runAsyncGenerator(
repeat_apply(handler, plusOne, numberValue(5), numberValue(0))
);

expect(result.value).toEqual(5);
});

it('returns x unchanged when n = 0', async () => {
const handler = new TestDataHandler();
const plusOne = await makePlusOne(handler);
const result = await runAsyncGenerator(
repeat_apply(handler, plusOne, numberValue(0), numberValue(5))
);

expect(result.value).toEqual(5);
});

it('throws an error when provided a negative or non-integer n', async () => {
const handler = new TestDataHandler();
const plusOne = await makePlusOne(handler);

await expect(runAsyncGenerator(repeat_apply(handler, plusOne, numberValue(-1), numberValue(0))))
.rejects.toThrow('repeat_apply: Expected integer ≥ 0, got -1.');

await expect(runAsyncGenerator(repeat_apply(handler, plusOne, numberValue(1.5), numberValue(0))))
.rejects.toThrow('repeat_apply: Expected integer ≥ 0, got 1.5.');
});
});
16 changes: 16 additions & 0 deletions src/bundles/repeat/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ export function repeat_internal<T>(f: UnaryFunction<T>, n: number): UnaryFunctio
return n === 0 ? x => x : x => func(repeat_internal(func, n - 1)(x));
}

/**
* Applies the specified closure to a value n times, threading the result of each
* application into the next.
*/
export async function* repeat_apply(evaluator: IDataHandler, func: TypedValue<DataType.CLOSURE>, n: TypedValue<DataType.NUMBER>, x: TypedValue<DataType>): AsyncGenerator<void, TypedValue<DataType>, undefined> {
if (!Number.isInteger(n.value) || n.value < 0) {
throw new EvaluatorRuntimeError(`repeat_apply: Expected integer ≥ 0, got ${n.value}.`);
}

let current = x;
for (let i = 0; i < n.value; i += 1) {
current = yield* evaluator.closure_call_unchecked(func, [current]);
}
return current;
}

/**
* Returns a new closure which when applied to an argument, has the same effect
* as applying the specified closure to the same argument n times.
Expand Down
21 changes: 19 additions & 2 deletions src/bundles/repeat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { BaseModulePlugin, moduleMethod } from '@sourceacademy/conductor/module'
import type { IInterfacableEvaluator } from '@sourceacademy/conductor/runner';
import { DataType, type TypedValue } from '@sourceacademy/conductor/types';

import { repeat as repeat_func, thrice as thrice_func, twice as twice_func } from './functions';
import { repeat as repeat_func, repeat_apply as repeat_apply_func, thrice as thrice_func, twice as twice_func } from './functions';

export default class RepeatModulePlugin extends BaseModulePlugin {
id = 'repeat';
override exportedNames = ['repeat', 'twice', 'thrice'] as const;
override exportedNames = ['repeat', 'repeat_apply', 'twice', 'thrice'] as const;
static override channelAttach = [];
constructor(conduit: IConduit, channels: IChannel<any>[], evaluator: IInterfacableEvaluator) {
super(conduit, channels, evaluator);
Expand All @@ -36,6 +36,23 @@ export default class RepeatModulePlugin extends BaseModulePlugin {
return yield* repeat_func(this.evaluator, func, n);
}

/**
* Applies the specified function to a value n times, threading the result of each
* application into the next.
* @example
* ```
* repeat_apply(x => x + 2, 5, 0); // Returns 10
* ```
* @param func the function to be applied
* @param n the number of times to apply the function
* @param x the initial value
* @returns the result of applying func to x n times
*/
@moduleMethod([DataType.CLOSURE, DataType.NUMBER, DataType.ANY], DataType.ANY)
async* repeat_apply(func: TypedValue<DataType.CLOSURE>, n: TypedValue<DataType.NUMBER>, x: TypedValue<DataType>): AsyncGenerator<void, TypedValue<DataType>, undefined> {
return yield* repeat_apply_func(this.evaluator, func, n, x);
}

/**
* Returns a new function which when applied to an argument, has the same effect
* as applying the specified function to the same argument 2 times.
Expand Down
Loading