[Function][Easy] Composition
🔸 題目描述
給定一個函式陣列 [f1, f2, f3, ... fn]
,回傳一個新的函式 fn
,它是該陣列中函式的組合。
函式組合的定義是,如果函式陣列為 [f(x), g(x), h(x)]
,則 fn(x) = f(g(h(x)))
。空函式陣列的函式組合則為恆等函式 f(x) = x
。
你可以假設陣列中的每個函式都接受一個整數作為輸入,並回傳一個整數作為輸出。
// 範例一
輸入: functions = [x => x + 1, x => x * x, x => 2 * x], x = 4
輸出: 65
解說:
從右到左
初始的 x = 4.
2 * (4) = 8
(8) * (8) = 64
(64) + 1 = 65
// 範例二
輸入: functions = [x => 10 * x, x => 10 * x, x => 10 * x], x = 1
輸出: 1000
解說:
從右到左
10 * (1) = 10
10 * (10) = 100
10 * (100) = 1000
💭 分析與思路
問題釐清
- x 是否需要考慮 number 外的型別?
- 是否需要處理在過程中輸出不滿足下一個 function 輸入的處理
- function array 中的每個輸入參數是否都只有一個
提出測試案例
- 為了讓問題簡化,這邊先只允許每個 function 都只有一個 number 參數,且初始 x 需為 number
提出思路
// 定義型別
type FunctionItem = (x: number) => number;
const compose = (functions: FunctionItem[]): FunctionItem => {
// 複製一份 functions 陣列並做 reverse
// 輸出能回傳一個以 (x: number) ⇒ number 形式的 function
// 對已反轉的函式陣列去取 reduce 迭代執行相加的結果
};
實作
const compose = (functions: FunctionItem[]): FunctionItem => {
/**
* 對 functions 複製一份並用 reverse 反轉,方便後面可以從頭取出
* 注意 reverse 會改變原陣列
* 所以為了避免 input 中的 functions 被更動
* 這裡需要複製一份
*/
const reversedFunList = [...functions].reverse();
// 輸出能回傳一個以 (x: number) ⇒ number 形式的 function
// 對已反轉的函式陣列去取 reduce 迭代執行相加的結果
return (x: number) =>
reversedFunList.reduce<number>((sum, fn) => {
sum = fn(sum);
return sum;
}, x);
};
整理一下後這樣更簡潔:
const compose = (functions: FunctionItem[]): FunctionItem => {
const reversedFunList = [...functions].reverse();
return (x: number) => reversedFunList.reduce<number>((sum, fn) => fn(sum), x);
};
如果去掉型別與縮寫後其實可以變成一行 code,但其實沒有很好讀就是 😆:
const compose = (functions) => (x) => [...functions].reverse().reduce((sum, fn) => fn(sum), x);
進階
如果放寬不只是 number 的測資呢?其實最後發現也沒什麼差,就是改一下型別成泛型而已:
type FunctionItem<T> = (x: T) => T;
const compose = <T>(functions: FunctionItem<T>[]) => {
const reversedFunList = [...functions].reverse();
return (x: T) => reversedFunList.reduce<T>((sum, fn) => fn(sum), x);
};
也補了一些測資:
import { describe, expect, test } from 'vitest';
import compose from './compose';
const basicCases = [
{
input: [(x: number) => x + 1, (x: number) => x * x, (x: number) => 2 * x],
x: 4,
expected: 65,
},
{
input: [(x: number) => 10 * x, (x: number) => 10 * x, (x: number) => 10 * x],
x: 1,
expected: 1000,
},
{
input: [],
x: 5,
expected: 5,
},
];
const advancedCases = [
{
input: [
(x: string) => `${x} - ${x}`,
(x: string) => x.toUpperCase(),
(x: string) => x + 12345,
],
x: 'hello',
expected: 'HELLO12345 - HELLO12345',
},
{
input: [
(x: { name: string; age: number }) => ({
name: `${x.name} is so old!`,
age: x.age * 2,
}),
(x: { name: string; age: number }) => ({
name: `HELLO, ${x.name}`,
age: x.age * 2,
}),
(x: { name: string; age: number }) => ({
name: x.name.toUpperCase(),
age: x.age * 2,
}),
],
x: { name: 'codefarmer', age: 18 },
expected: {
name: 'HELLO, CODEFARMER is so old!',
age: 144,
},
},
];
const edgeCases = [
{
input: [(x: number) => x * x, (x: string) => `${x}${x}${x}`, (x: number) => x + 1],
x: 4,
expected: 308025,
},
{
input: [(x: number) => x * x, () => 987, (x: number) => x + 100],
x: 5,
expected: 974169,
},
{
input: [() => 987, (x: number) => x + 100, (x: number) => x * x],
x: 'hello world',
expected: 987,
},
{
input: [(x: number) => x * x, (x: string) => x.toUpperCase(), (x: null) => typeof x],
x: null,
expected: NaN,
},
];
describe('function composition', () => {
test.each(basicCases)('should pass basic test cases - %s', ({ input, x, expected }) => {
const result = compose(input)(x);
expect(result).toEqual(expected);
});
test.each(advancedCases)(
'should pass advanced test cases - %s',
({ input, x, expected }) => {
const result = compose(input)(x);
expect(result).toEqual(expected);
}
);
test.each(edgeCases)('should pass edge cases - %s', ({ input, x, expected }) => {
const result = compose(input)(x);
expect(result).toEqual(expected);
});
});