阅读视图
特朗普施压美联储将利率降至1%
特朗普称不考虑延长关税谈判最后期限
Remark完成1600万美元A轮融资
波音公司将收购Spirit位于北爱尔兰的工厂
特斯拉6月份在法国新车注册量同比下降10%
华纳音乐集团与贝恩资本宣布成立合资企业,计划投资高达12亿美元收购经典音乐曲库
城堡投资据悉从高盛和Pimco挖角高管担任投资组合经理
贝佐斯出售价值7.37亿美元亚马逊股票
美股三大指数收盘涨跌不一,大型科技股多数下跌
每日一题-找到初始输入字符串 II🔴
Alice 正在她的电脑上输入一个字符串。但是她打字技术比较笨拙,她 可能 在一个按键上按太久,导致一个字符被输入 多次 。
给你一个字符串 word
,它表示 最终 显示在 Alice 显示屏上的结果。同时给你一个 正 整数 k
,表示一开始 Alice 输入字符串的长度 至少 为 k
。
请你返回 Alice 一开始可能想要输入字符串的总方案数。
由于答案可能很大,请你将它对 109 + 7
取余 后返回。
示例 1:
输入:word = "aabbccdd", k = 7
输出:5
解释:
可能的字符串包括:"aabbccdd"
,"aabbccd"
,"aabbcdd"
,"aabccdd"
和 "abbccdd"
。
示例 2:
输入:word = "aabbccdd", k = 8
输出:1
解释:
唯一可能的字符串是 "aabbccdd"
。
示例 3:
输入:word = "aaabbb", k = 3
输出:8
提示:
1 <= word.length <= 5 * 105
-
word
只包含小写英文字母。 1 <= k <= 2000
JavaScript 函数式编程思想与柯里化的深度剖析
JavaScript 作为一门多范式语言,既支持面向对象编程,也为函数式编程(Functional Programming, FP)提供了强大的支持。函数式编程以其声明式、不可变性和高阶函数等特性,正在现代前端开发中占据越来越重要的地位。而柯里化(Currying)作为函数式编程的核心技术之一,通过将多参数函数转换为单参数函数链,极大地提升了代码的灵活性和复用性。
1. 函数式编程的基础
1.1 什么是函数式编程?
函数式编程是一种编程范式,强调将计算过程建模为数学函数的求值,避免状态变化和可变数据。其核心思想包括:
- 纯函数:函数的输出仅依赖于输入,且无副作用。
- 不可变性:数据一旦创建不可修改,变化通过创建新数据实现。
- 高阶函数:函数可以作为参数传递或作为返回值返回。
- 声明式:关注“做什么”而非“怎么做”,代码更简洁。
在 JavaScript 中,函数是一等公民(First-Class Citizen),支持高阶函数、闭包等特性,使其天然适合函数式编程。
1.2 函数式编程的核心原则
-
纯函数:相同的输入始终产生相同的输出,无副作用。例如:
function add(a, b) { return a + b; } console.log(add(2, 3)); // 5
反例(非纯函数):
let total = 0; function addToTotal(value) { total += value; return total; }
-
不可变性:避免直接修改数据:
const numbers = [1, 2, 3]; const doubled = numbers.map(n => n * 2); // [2, 4, 6] console.log(numbers); // [1, 2, 3](原数组未变)
-
避免副作用:函数不应修改外部状态:
const log = console.log; function greet(name) { log(`Hello, ${name}`); // 副作用 return `Hello, ${name}`; }
-
函数组合:通过组合小函数实现复杂逻辑:
const compose = (f, g) => x => f(g(x)); const addOne = x => x + 1; const double = x => x * 2; const addOneThenDouble = compose(double, addOne); console.log(addOneThenDouble(5)); // 12
1.3 为什么在 JavaScript 中使用函数式编程?
- 可预测性:纯函数和不可变性降低调试难度。
- 模块化:高阶函数和函数组合提升代码复用性。
- 并发友好:无状态操作更适合异步和并发场景。
- 现代框架支持:React、Redux 等框架大量采用函数式思想。
2. 核心函数式编程概念
2.1 纯函数与副作用
纯函数是函数式编程的基石。实现纯函数需遵循:
- 输入决定输出:不依赖外部变量。
- 无外部修改:不更改全局状态或 DOM。
示例:
function filterEvens(numbers) {
return numbers.filter(n => n % 2 === 0);
}
console.log(filterEvens([1, 2, 3, 4])); // [2, 4]
避免副作用:
// 非纯函数
let counter = 0;
function increment() {
counter++;
return counter;
}
// 纯函数
function incrementCounter(current) {
return current + 1;
}
2.2 高阶函数
高阶函数接受函数作为参数或返回函数。JavaScript 的数组方法(如 map
、filter
、reduce
)是典型的高阶函数。
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8]
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0); // 10
自定义高阶函数:
function withLogging(fn) {
return (...args) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// Calling add with [2, 3]
// Result: 5
2.3 闭包与函数式编程
闭包允许函数访问其定义时的词法作用域,是实现函数式编程的重要机制。
function createCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count,
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.get()); // 1
闭包实现私有状态:
function createUser(name) {
let _name = name;
return {
getName: () => _name,
setName: newName => (_name = newName),
};
}
const user = createUser('Alice');
console.log(user.getName()); // Alice
user.setName('Bob');
console.log(user.getName()); // Bob
2.4 不可变性
JavaScript 中的数组和对象是可变的,需通过复制实现不可变性。
数组不可变操作:
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4]; // [1, 2, 3, 4]
console.log(numbers); // [1, 2, 3]
对象不可变操作:
const user = { name: 'Alice', age: 30 };
const updatedUser = { ...user, age: 31 };
console.log(user); // { name: 'Alice', age: 30 }
console.log(updatedUser); // { name: 'Alice', age: 31 }
使用 Object.freeze
:
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
});
config.apiUrl = 'new-url'; // 无效果
console.log(config.apiUrl); // https://api.example.com
2.5 函数组合与管道
函数组合通过将多个函数串联实现复杂逻辑。
组合(compose
):
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const addOne = x => x + 1;
const double = x => x * 2;
const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(5)); // 12
管道(pipe
):
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const addOneThenDouble = pipe(addOne, double);
console.log(addOneThenDouble(5)); // 12
3. 柯里化的核心概念
3.1 什么是柯里化?
柯里化是将一个多参数函数转换为一系列单参数函数的过程。形式上:
// 非柯里化
function add(a, b) {
return a + b;
}
// 柯里化
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
const addFive = curriedAdd(5);
console.log(addFive(3)); // 8
3.2 柯里化的优势
- 参数复用:固定部分参数,生成专用函数。
- 延迟执行:只有提供所有参数时才执行计算。
- 函数组合:柯里化函数易于组合。
- 模块化:将复杂逻辑拆分为小函数。
3.3 手动实现柯里化
简单柯里化:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
3.4 处理任意参数
支持动态参数:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const join = (...args) => args.join('');
const curriedJoin = curry(join);
console.log(curriedJoin('a')('b')('c')); // abc
console.log(curriedJoin('a', 'b')('c')); // abc
3.5 占位符支持
实现带占位符的柯里化(如 Lodash 的 _.curry
):
const _ = Symbol('placeholder');
function curry(fn) {
const arity = fn.length;
return function curried(...args) {
const actualArgs = args.filter(arg => arg !== _);
if (actualArgs.length >= arity) {
return fn(...args.slice(0, arity));
}
return (...nextArgs) => {
const combined = [];
let argIdx = 0;
let nextIdx = 0;
for (let i = 0; i < args.length; i++) {
if (args[i] === _ && nextIdx < nextArgs.length) {
combined.push(nextArgs[nextIdx++]);
} else {
combined.push(args[i]);
}
}
while (nextIdx < nextArgs.length) {
combined.push(nextArgs[nextIdx++]);
}
return curried(...combined);
};
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1, _, 3)(2)); // 6
console.log(curriedAdd(_, 2, _)(1, 3)); // 6
4. 柯里化在函数式编程中的应用
4.1 参数复用
柯里化通过固定参数生成专用函数:
const multiply = curry((a, b) => a * b);
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
4.2 事件处理
为 DOM 事件创建专用处理器:
const addEventListener = curry((event, handler, element) =>
element.addEventListener(event, handler)
);
const onClick = addEventListener('click');
const logClick = () => console.log('Clicked');
const button = document.querySelector('#myButton');
onClick(logClick)(button);
4.3 数据转换
处理数据管道:
const map = curry((fn, arr) => arr.map(fn));
const filter = curry((fn, arr) => arr.filter(fn));
const addOne = x => x + 1;
const isEven = x => x % 2 === 0;
const processNumbers = pipe(
map(addOne),
filter(isEven)
);
console.log(processNumbers([1, 2, 3, 4])); // [2, 4]
4.4 配置化函数
为函数注入配置:
const fetchData = curry((baseUrl, endpoint, params) =>
fetch(`${baseUrl}/${endpoint}?${new URLSearchParams(params)}`).then(res =>
res.json()
)
);
const api = fetchData('https://api.example.com');
const getUsers = api('users');
getUsers({ limit: 10 }).then(console.log);
5. 函数式编程与柯里化在前端框架中的应用
5.1 React 中的函数式编程
React 的函数组件和 Hooks 天然契合函数式编程。
纯组件:
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
高阶组件(HOC):
const withLoading = Component => ({ isLoading, ...props }) =>
isLoading ? <div>Loading...</div> : <Component {...props} />;
const UserListWithLoading = withLoading(UserList);
function App() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setIsLoading(false);
});
}, []);
return <UserListWithLoading isLoading={isLoading} users={users} />;
}
柯里化在 React:
const useFetch = curry((url, options, setState) =>
useEffect(() => {
fetch(url, options)
.then(res => res.json())
.then(setState);
}, [url, options])
);
function UserList() {
const [users, setUsers] = useState([]);
const fetchUsers = useFetch('/api/users', { method: 'GET' });
fetchUsers(setUsers);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
5.2 Vue 中的函数式编程
Vue 3 的组合式 API 支持函数式风格。
纯函数组件:
import { defineComponent } from 'vue';
export default defineComponent({
props: ['users'],
setup({ users }) {
return () => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
},
});
高阶组件:
const withLoading = Component => ({
props: ['isLoading'],
setup(props) {
return () => (props.isLoading ? <div>Loading...</div> : <Component {...props} />);
},
});
const UserListWithLoading = withLoading(UserList);
柯里化:
const useFetch = curry((url, options, setState) => {
onMounted(() => {
fetch(url, options)
.then(res => res.json())
.then(setState);
});
});
export default defineComponent({
setup() {
const users = ref([]);
const fetchUsers = useFetch('/api/users', { method: 'GET' });
fetchUsers(users.value);
return () => (
<ul>
{users.value.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
},
});
6. 函数式编程工具库
6.1 Ramda
Ramda 是一个专注于函数式编程的库,内置柯里化支持。
安装:
npm install ramda
使用:
import * as R from 'ramda';
const addOne = R.map(R.add(1));
const filterEvens = R.filter(x => x % 2 === 0);
const processNumbers = R.pipe(addOne, filterEvens);
console.log(processNumbers([1, 2, 3, 4])); // [2, 4]
柯里化:
const add = R.curry((a, b) => a + b);
const addFive = add(5);
console.log(addFive(3)); // 8
6.2 Lodash/fp
Lodash 的函数式模块提供柯里化支持。
安装:
npm install lodash
使用:
import { flow, map, filter, curry } from 'lodash/fp';
const addOne = map(x => x + 1);
const filterEvens = filter(x => x % 2 === 0);
const processNumbers = flow([addOne, filterEvens]);
console.log(processNumbers([1, 2, 3, 4])); // [2, 4]
const add = curry((a, b) => a + b);
const addFive = add(5);
console.log(addFive(3)); // 8
7. 性能优化
7.1 记忆化
通过缓存结果优化纯函数性能:
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const factorial = memoize(n => (n <= 1 ? 1 : n * factorial(n - 1)));
console.log(factorial(5)); // 120
console.log(factorial(5)); // 120(从缓存获取)
7.2 惰性求值
延迟计算以提升性能:
function lazyMap(fn, arr) {
return {
next: () => {
const result = arr.map(fn);
this.next = () => result;
return result;
},
};
}
const numbers = [1, 2, 3, 4];
const lazyDouble = lazyMap(x => x * 2, numbers);
console.log(lazyDouble.next()); // [2, 4, 6, 8]
console.log(lazyDouble.next()); // [2, 4, 6, 8](缓存结果)
7.3 柯里化性能优化
缓存柯里化函数:
function curry(fn) {
const cache = new Map();
return function curried(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
if (args.length >= fn.length) {
const result = fn(...args);
cache.set(key, result);
return result;
}
const next = (...nextArgs) => curried(...args, ...nextArgs);
cache.set(key, next);
return next;
};
}
const add = curry((a, b, c) => a + b + c);
const addFive = add(5);
console.log(addFive(2)(3)); // 10
console.log(addFive(2)(3)); // 10(缓存)
8. 函数式编程与 TypeScript
8.1 纯函数
const add = (a: number, b: number): number => a + b;
console.log(add(2, 3)); // 5
8.2 高阶函数
type Fn<T, R> = (arg: T) => R;
function withLogging<T, R>(fn: Fn<T, R>): Fn<T, R> {
return (...args: T[]) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
8.3 柯里化
function curry<T extends any[], R>(fn: (...args: T) => R) {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs: any[]) => curried(...args, ...nextArgs);
};
}
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
8.4 不可变性
interface User {
readonly name: string;
readonly age: number;
}
const user: User = { name: 'Alice', age: 30 };
const updatedUser: User = { ...user, age: 31 };
console.log(user); // { name: 'Alice', age: 30 }
console.log(updatedUser); // { name: 'Alice', age: 31 }
9. 函数式编程与异步编程
9.1 Promise 链
const fetchData = url =>
fetch(url)
.then(res => res.json())
.then(data => data.results);
const processData = pipe(
map(item => ({ ...item, processed: true })),
filter(item => item.id > 0)
);
fetchData('/api/users')
.then(processData)
.then(console.log);
9.2 异步柯里化
const asyncCurry = fn => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
};
const fetchData = async (url, params) =>
fetch(`${url}?${new URLSearchParams(params)}`).then(res => res.json());
const curriedFetch = asyncCurry(fetchData);
const getUsers = curriedFetch('/api/users');
getUsers({ limit: 10 }).then(console.log);
10. 函数式编程与状态管理
10.1 Redux
Redux 使用纯函数(Reducer)管理状态:
const initialState = { count: 0 };
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
const increment = () => ({ type: 'INCREMENT' });
const decrement = () => ({ type: 'DECREMENT' });
柯里化 Action Creator:
const createAction = curry((type, payload) => ({ type, payload }));
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');
console.log(increment({ value: 1 })); // { type: 'INCREMENT', payload: { value: 1 } }
10.2 Zustand
Zustand 支持函数式状态管理:
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));
const increment = useStore(state => state.increment);
const decrement = useStore(state => state.decrement);
柯里化:
const createAction = curry((fn, set) => (...args) => set(state => fn(state, ...args)));
const increment = createAction((state, value) => ({
count: state.count + value,
}));
const decrement = createAction((state, value) => ({
count: state.count - value,
}));
const useStore = create(set => ({
count: 0,
increment: increment(set),
decrement: decrement(set),
}));
11. 函数式编程与测试
11.1 测试纯函数
describe('add', () => {
it('should add two numbers', () => {
const add = (a, b) => a + b;
expect(add(2, 3)).toBe(5);
});
});
11.2 测试柯里化函数
describe('curriedAdd', () => {
const add = curry((a, b, c) => a + b + c);
it('should add three numbers', () => {
expect(add(1)(2)(3)).toBe(6);
expect(add(1, 2)(3)).toBe(6);
expect(add(1)(2, 3)).toBe(6);
});
});
11.3 Mock 高阶函数
jest.mock('./utils', () => ({
withLogging: jest.fn(fn => (...args) => fn(...args)),
}));
describe('withLogging', () => {
it('should wrap function', () => {
const add = (a, b) => a + b;
const loggedAdd = require('./utils').withLogging(add);
expect(loggedAdd(2, 3)).toBe(5);
expect(require('./utils').withLogging).toHaveBeenCalledWith(add);
});
});
12. 函数式编程与模块化
12.1 CommonJS
// utils.js
module.exports = {
curry: fn => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
},
};
// app.js
const { curry } = require('./utils');
const add = curry((a, b) => a + b);
console.log(add(2)(3)); // 5
12.2 ES Modules
// utils.mjs
export const curry = fn => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
};
// app.mjs
import { curry } from './utils.mjs';
const add = curry((a, b) => a + b);
console.log(add(2)(3)); // 5
13. 函数式编程与微前端
13.1 模块化状态管理
const createModuleStore = curry((moduleName, initialState, reducers) =>
create(set => ({
...initialState,
...Object.keys(reducers).reduce(
(acc, action) => ({
...acc,
[action]: (...args) =>
set(state => reducers[action](state, ...args)),
}),
{}
),
}))
);
const userStore = createModuleStore('user', { users: [] }, {
addUser: (state, user) => ({ users: [...state.users, user] }),
});
userStore.getState().addUser({ name: 'Alice' });
console.log(userStore.getState().users); // [{ name: 'Alice' }]
13.2 Qiankun 微前端
import { registerMicroApps, start } from 'qiankun';
const createMicroApp = curry((name, config) => ({
name,
...config,
}));
const registerApp = curry((apps, app) => {
apps.push(app);
return apps;
});
const apps = [];
const registerReactApp = registerApp(apps);
const createReactApp = createMicroApp('reactApp');
registerReactApp(
createReactApp({
entry: '//localhost:3001',
container: '#reactContainer',
activeRule: '/react',
})
);
registerMicroApps(apps);
start();
14. 函数式编程与错误处理
14.1 Either 容器
实现简化的 Either 容器处理错误:
class Either {
constructor(value, isRight = true) {
this.value = value;
this.isRight = isRight;
}
static Right(value) {
return new Either(value, true);
}
static Left(value) {
return new Either(value, false);
}
map(fn) {
return this.isRight ? Either.Right(fn(this.value)) : this;
}
getOrElse(defaultValue) {
return this.isRight ? this.value : defaultValue;
}
}
const safeDivide = curry((a, b) =>
b === 0 ? Either.Left('Division by zero') : Either.Right(a / b)
);
console.log(safeDivide(10)(2).getOrElse(0)); // 5
console.log(safeDivide(10)(0).getOrElse(0)); // 0
14.2 Try-Catch
const tryCatch = curry((fn, ...args) => {
try {
return Either.Right(fn(...args));
} catch (error) {
return Either.Left(error.message);
}
});
const parseJSON = tryCatch(JSON.parse);
console.log(parseJSON('{"name":"Alice"}').getOrElse({})); // { name: 'Alice' }
console.log(parseJSON('invalid').getOrElse({})); // {}
15. 函数式编程与性能分析
15.1 性能测试
const numbers = Array.from({ length: 1000 }, (_, i) => i);
const start = performance.now();
numbers.map(x => x * 2).filter(x => x % 2 === 0);
const end = performance.now();
console.log(`Map and filter took ${end - start}ms`);
15.2 优化循环
使用 reduce
减少多次遍历:
const processNumbers = numbers =>
numbers.reduce((acc, x) => {
const doubled = x * 2;
if (doubled % 2 === 0) {
acc.push(doubled);
}
return acc;
}, []);
const start = performance.now();
processNumbers(numbers);
const end = performance.now();
console.log(`Reduce took ${end - start}ms`);
16. 函数式编程与 Node.js
16.1 文件操作
const fs = require('fs').promises;
const readFile = curry((encoding, path) => fs.readFile(path, encoding));
const writeFile = curry((path, data) => fs.writeFile(path, data));
const processFile = pipe(
readFile('utf8'),
map(line => line.toUpperCase()),
writeFile('output.txt')
);
processFile('input.txt');
16.2 HTTP 服务器
const http = require('http');
const createRoute = curry((method, path, handler) => ({
method,
path,
handler,
}));
const handleRequest = curry((routes, req, res) => {
const route = routes.find(
r => r.method === req.method && r.path === req.url
);
if (route) {
route.handler(req, res);
} else {
res.writeHead(404);
res.end('Not Found');
}
});
const routes = [
createRoute('GET', '/users', (req, res) => {
res.writeHead(200);
res.end(JSON.stringify([{ name: 'Alice' }]));
}),
];
const server = http.createServer(handleRequest(routes));
server.listen(3000);
17. 函数式编程与事件处理
const createEventHandler = curry((event, handler, element) =>
element.addEventListener(event, handler)
);
const onClick = createEventHandler('click');
const logClick = () => console.log('Clicked');
const button = document.querySelector('#myButton');
onClick(logClick)(button);
18. 函数式编程与数据模型
const createModel = curry((transform, data) => transform(data));
const userModel = createModel(data => ({
id: data.id,
name: data.name,
getFullName: () => data.name,
}));
const user = userModel({ id: 1, name: 'Alice' });
console.log(user.getFullName()); // Alice
19. 函数式编程与插件系统
const createPlugin = curry((name, fn) => ({
name,
apply: fn,
}));
const loggerPlugin = createPlugin('logger', config => message =>
console.log(`[${config.level}] ${message}`)
);
const logger = loggerPlugin({ level: 'INFO' });
logger('System started'); // [INFO] System started
20. 函数式编程与配置管理
const createConfig = curry((env, config) => ({
...config,
env,
}));
const devConfig = createConfig('development', {
apiUrl: 'http://localhost:3000',
debug: true,
});
console.log(devConfig); // { env: 'development', apiUrl: 'http://localhost:3000', debug: true }
生成函数解法 O(klogk)
这题和前几天的 每日一题 几乎是一样的,那道题下标 $i$ 处的元素能贡献 $0,1,\cdots,i$ 个逆序对,本题将连续的相同字符分块,具有 $l$ 个字符的块在初始字符串中可能有 $1,2,\cdots,l$ 个字符。
和 3193 题类似的动态规划解法可以达到 $O(k^2)$ 复杂度,3193 题的 生成函数解法 也同样适合本题。
具有 $l$ 个字符的块的生成函数为 $x+x^2+\cdots+x^l=x\dfrac{1-x^l}{1-x}$,假设总共有 $n$ 个块,生成函数即为 $\dfrac{x^n}{(1-x)^n}\prod_{i=0}^{n-1}(1-x^{l_i})$,去掉 $x^n$ 项取对数后为
$$
\sum_{i=0}^{n-1}\log(1-x^{l_i})-n\log(1-x)
$$
用泰勒展开计算 $\log$ 再取 $\exp$ 即可,和 3193 题不同的是要将长度相同的块合并处理,这样才能保证复杂度是调和级数部分和,达到 $O(k \log k)$ 的复杂度。
代码
###Python3
import numpy as np
def polymul(lhs, rhs, MOD):
n_lhs = len(lhs)
n_rhs = len(rhs)
n = n_lhs + n_rhs - 1
if n_lhs <= 64:
rhs = rhs.astype(np.uint64)
total = np.zeros(n, dtype = np.uint64)
for i, e in enumerate(lhs):
total[i: i + n_rhs] += np.uint64(e) * rhs % MOD
return total % MOD
elif n_rhs <= 64:
lhs = lhs.astype(np.uint64)
total = np.zeros(n, dtype = np.uint64)
for i, e in enumerate(rhs):
total[i: i + n_lhs] += np.uint64(e) * lhs % MOD
return total % MOD
else:
p = (lhs & 65535) + 1j * (lhs >> 16)
f_p = np.fft.fft(p, n)
f_cp = np.conj(np.append(f_p[0:1], f_p[-1:0:-1]))
q = (rhs & 65535) + 1j * (rhs >> 16)
f_q = np.fft.fft(q, n)
pq = np.fft.ifft(f_p * f_q, n)
cpq = np.fft.ifft(f_cp * f_q, n)
s = np.round(0.5 * (pq + cpq))
d = np.round(-0.5j * (pq - cpq))
ans00 = s.real.astype(np.uint64)
ans01 = s.imag.astype(np.uint64)
ans10 = d.real.astype(np.uint64)
ans11 = d.imag.astype(np.uint64)
return (ans00 % MOD + (((ans01 + ans10) % MOD) << 16) + ((ans11 % MOD) << 32)) % MOD
def polyninv(x, n, MOD):
y = np.array([pow(MOD - int(x[0]), MOD - 2, MOD)], dtype = np.uint64)
l = 1
while l < n:
l = min(2 * l, n)
t = polymul(x[:l], y, MOD)[:l]
t[0] += 2
y = polymul(t, y, MOD)[:l]
return y
def polyinv(x, n, MOD):
return polyninv(MOD - x, n, MOD)
def polyder(x, MOD):
return x[1:] * np.arange(1, len(x), dtype = np.uint64) % MOD
def polyint(x, INV, MOD):
return np.append([np.uint64(0)], x * INV[:len(x)] % MOD)
def polylog(x, n, INV, MOD):
return polyint(polymul(polyder(x[:n + 1], MOD), polyinv(x, n, MOD), MOD)[:n - 1], INV, MOD)
def polyexp(x, n, INV, MOD):
y = np.array([1, 0], dtype = np.uint64)
l = 1
while l < n:
l = min(2 * l, n)
t = (MOD - polylog(y, l, INV, MOD))
t[:len(x)] += x[:l]
t[0] += 1
y = polymul(t, y, MOD)[:l]
return y
MOD = 1000000007
K = 2000
INV = [1] * (K + 1)
for i in range(2, K + 1):
INV[i] = (MOD - MOD // i) * INV[MOD % i] % MOD
NP_INV = np.array(INV[1:], dtype = np.uint64)
class Solution:
def possibleStringCount(self, word: str, k: int) -> int:
log_f = [0] * k
pre_ch = word[0]
l = 0
n = 1
total = 1
for ch in word:
if ch == pre_ch:
l += 1
else:
n += 1
if l < k:
log_f[l] += 1
total = total * l % MOD
pre_ch = ch
l = 1
total = total * l % MOD
if k <= n:
return total
if l < k:
log_f[l] += 1
for i in range(k - n - 1, 0, -1):
if log_f[i] > 0:
c = log_f[i]
log_f[i] = 0
for j in range(1, (k - n - 1) // i + 1):
log_f[i * j] -= c * INV[j]
for i in range(1, k - n):
log_f[i] = (log_f[i] + n * INV[i]) % MOD
f = polyexp(np.array(log_f[:k - n], dtype = np.uint64), k - n, NP_INV, MOD)
return (total - int(np.sum(f))) % MOD
前缀和优化多重背包(Python/Java/C++/Go)
总体思路
把一开始想要输入的字符串叫做原串。为方便计算,考虑长度小于 $k$ 的原串。
- 计算不考虑 $k$ 的情况下,有多少个原串。
- 计算长度小于 $k$ 的原串个数。
- 二者相减,即为长度大于等于 $k$ 的原串个数。
不考虑 k 的原串个数
比如 $\textit{word}=\texttt{aabcccdd}$,分成 $4$ 段连续相同子串:$\texttt{aa},\texttt{b},\texttt{ccc},\texttt{dd}$,长度分别为 $2,1,3,2$。
在原串中,比如 $\texttt{ccc}$ 这一段可能是 $\texttt{c}$、$\texttt{cc}$ 或 $\texttt{ccc}$,有 $3$ 种可能。每一段的可能情况数,等于这一段的长度。由于每一段的长度互相独立,根据乘法原理,原串个数为
$$
2\times 1\times 3\times 2 = 12
$$
注:本题与 3330. 找到初始输入字符串 I 是不同的,那题至多犯错一次,本题每一段都可能会犯错。
长度小于 k 的原串个数
寻找子问题
假设字符串分为 $4$ 组,要用这 $4$ 组构造的原串的长度是 $6$。
由于每组的长度至少是 $1$,为方便计算,先从每组各选 $1$ 个字母,问题转换成从 $4$ 组中额外再选 $6-4=2$ 个字母的方案数。
枚举从最后一组中选多少个字母:
- 选 $0$ 个,问题变成从前 $3$ 组中选 $2-0=2$ 个字母的方案数。
- 选 $1$ 个,问题变成从前 $3$ 组中选 $2-1=1$ 个字母的方案数。
状态定义与状态转移方程
这是一个多重背包方案数问题。在上面的例子中,有 $m=4$ 种物品,第 $i$ 种物品有「第 $i$ 组的大小减一」个。
我们至多选 $k-1$ 个物品($<k$ 即 $\le k-1$),其中每组都提前选了 $1$ 个物品,最终,我们需要计算的是:至多选 $(k-1)-m$ 个物品的方案数。
根据「寻找子问题」中的讨论,定义 $f[i][j]$ 表示从前 $i$ 种物品中选至多 $j$ 个物品的方案数。
初始值 $f[0][j]=1$,只能什么也不选,算一种方案。在示例 1 中,这对应字符串 $\texttt{abcd}$。
答案为 $f[m][k-1-m]$。
假设第 $i$ 种物品有 $c$ 个,枚举选 $L=0,1,2,\ldots,c$ 个物品,问题变成从前 $i-1$ 种物品中选至多 $j-L$ 个物品的方案数,即 $f[i-1][j-L]$。
累加得
$$
f[i][j] = \sum_{L=0}^{c} f[i-1][j-L]
$$
注意要保证 $j-L\ge 0$。用 $p$ 替换 $j-L$,上式为
$$
f[i][j] = \sum_{p=\max(j-c, 0)}^{j} f[i-1][p]
$$
和式是 $f[i-1]$ 的子数组和。定义 $f[i-1]$ 的 前缀和 数组为 $s$,上式简化为
$$
f[i][j] = s[j+1] - s[\max(j-c, 0)]
$$
细节
如果 $n<k$($n$ 为 $\textit{word}$ 的长度),无法满足「长度至少为 $k$」的要求,直接返回 $0$。
如果 $m\ge k$,那么长度小于 $k$ 的原串个数为 $0$,直接返回「不考虑 $k$ 的原串个数」。
注意计算 DP 时,下标 $i$ 是从 $0$ 开始的,状态定义中的 $i$ 是从 $1$ 开始的。$i=0$ 表示第 $1$ 组,$i=1$ 表示第 $2$ 组,依此类推。所以 $f$ 第一维的下标要加一,实际计算的是 $f[i+1][j]$。
代码中用到了一些取模的细节,原理见 模运算的世界:当加减乘除遇上取模。
具体请看 视频讲解,欢迎点赞关注~
###py
class Solution:
def possibleStringCount(self, word: str, k: int) -> int:
n = len(word)
if n < k: # 无法满足要求
return 0
MOD = 1_000_000_007
cnts = []
ans = 1
cnt = 0
for i in range(n):
cnt += 1
if i == n - 1 or word[i] != word[i + 1]:
# 如果 cnt = 1,这组字符串必选,无需参与计算
if cnt > 1:
if k > 0:
cnts.append(cnt - 1)
ans = ans * cnt % MOD
k -= 1 # 注意这里把 k 减小了
cnt = 0
if k <= 0:
return ans
f = [[0] * k for _ in range(len(cnts) + 1)]
f[0] = [1] * k
for i, c in enumerate(cnts):
# 计算 f[i] 的前缀和数组 s
s = list(accumulate(f[i], initial=0))
# 计算子数组和
for j in range(k):
f[i + 1][j] = (s[j + 1] - s[max(j - c, 0)]) % MOD
return (ans - f[-1][-1]) % MOD
###java
class Solution {
public int possibleStringCount(String word, int k) {
int n = word.length();
if (n < k) { // 无法满足要求
return 0;
}
final int MOD = 1_000_000_007;
List<Integer> cnts = new ArrayList<>();
long ans = 1;
int cnt = 0;
for (int i = 0; i < n; i++) {
cnt++;
if (i == n - 1 || word.charAt(i) != word.charAt(i + 1)) {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if (cnt > 1) {
if (k > 0) {
cnts.add(cnt - 1);
}
ans = ans * cnt % MOD;
}
k--; // 注意这里把 k 减小了
cnt = 0;
}
}
if (k <= 0) {
return (int) ans;
}
int m = cnts.size();
int[][] f = new int[m + 1][k];
Arrays.fill(f[0], 1);
int[] s = new int[k + 1];
for (int i = 0; i < m; i++) {
// 计算 f[i] 的前缀和数组 s
for (int j = 0; j < k; j++) {
s[j + 1] = (s[j] + f[i][j]) % MOD;
}
int c = cnts.get(i);
// 计算子数组和
for (int j = 0; j < k; j++) {
f[i + 1][j] = (s[j + 1] - s[Math.max(j - c, 0)]) % MOD;
}
}
return (int) ((ans - f[m][k - 1] + MOD) % MOD); // 保证结果非负
}
}
###cpp
class Solution {
public:
int possibleStringCount(string word, int k) {
int n = word.size();
if (n < k) { // 无法满足要求
return 0;
}
const int MOD = 1'000'000'007;
vector<int> cnts;
long long ans = 1;
int cnt = 0;
for (int i = 0; i < n; i++) {
cnt++;
if (i == n - 1 || word[i] != word[i + 1]) {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if (cnt > 1) {
if (k > 0) {
cnts.push_back(cnt - 1);
}
ans = ans * cnt % MOD;
}
k--; // 注意这里把 k 减小了
cnt = 0;
}
}
if (k <= 0) {
return ans;
}
int m = cnts.size();
vector f(m + 1, vector<int>(k));
ranges::fill(f[0], 1);
vector<int> s(k + 1);
for (int i = 0; i < m; i++) {
// 计算 f[i] 的前缀和数组 s
for (int j = 0; j < k; j++) {
s[j + 1] = (s[j] + f[i][j]) % MOD;
}
// 计算子数组和
for (int j = 0; j < k; j++) {
f[i + 1][j] = (s[j + 1] - s[max(j - cnts[i], 0)]) % MOD;
}
}
return (ans - f[m][k - 1] + MOD) % MOD; // 保证结果非负
}
};
###go
func possibleStringCount(word string, k int) int {
if len(word) < k { // 无法满足要求
return 0
}
const mod = 1_000_000_007
cnts := []int{}
ans := 1
cnt := 0
for i := range word {
cnt++
if i == len(word)-1 || word[i] != word[i+1] {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if cnt > 1 {
if k > 0 {
cnts = append(cnts, cnt-1)
}
ans = ans * cnt % mod
}
k-- // 注意这里把 k 减小了
cnt = 0
}
}
if k <= 0 {
return ans
}
m := len(cnts)
f := make([][]int, m+1)
for i := range f {
f[i] = make([]int, k)
}
for i := range f[0] {
f[0][i] = 1
}
s := make([]int, k+1)
for i, c := range cnts {
// 计算 f[i] 的前缀和数组 s
for j, v := range f[i] {
s[j+1] = s[j] + v
}
// 计算子数组和
for j := range f[i+1] {
f[i+1][j] = (s[j+1] - s[max(j-c, 0)]) % mod
}
}
return (ans - f[m][k-1] + mod) % mod // 保证结果非负
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n + k^2)$,其中 $n$ 是 $\textit{word}$ 的长度。
- 空间复杂度:$\mathcal{O}(k^2)$。
空间优化
去掉 $f$ 的第一个维度。
前缀和直接计算到 $f$ 数组中。
然后和 0-1 背包 一样,倒序计算 $f[j] = s[j] - s[j-c-1]$。减一是因为原来前缀和中的 $s[0]=0$ 去掉了,$s$ 的长度不是 $k+1$ 而是 $k$。
###py
class Solution:
def possibleStringCount(self, word: str, k: int) -> int:
n = len(word)
if n < k: # 无法满足要求
return 0
MOD = 1_000_000_007
cnts = []
ans = 1
cnt = 0
for i in range(n):
cnt += 1
if i == n - 1 or word[i] != word[i + 1]:
# 如果 cnt = 1,这组字符串必选,无需参与计算
if cnt > 1:
if k > 0: # 保证空间复杂度为 O(k)
cnts.append(cnt - 1)
ans = ans * cnt % MOD
k -= 1 # 注意这里把 k 减小了
cnt = 0
if k <= 0:
return ans
f = [1] * k
for c in cnts:
# 原地计算 f 的前缀和
for j in range(1, k):
f[j] = (f[j] + f[j - 1]) % MOD
# 计算子数组和
for j in range(k - 1, c, -1):
f[j] -= f[j - c - 1]
return (ans - f[-1]) % MOD
###java
class Solution {
public int possibleStringCount(String word, int k) {
int n = word.length();
if (n < k) { // 无法满足要求
return 0;
}
final int MOD = 1_000_000_007;
List<Integer> cnts = new ArrayList<>();
long ans = 1;
int cnt = 0;
for (int i = 0; i < n; i++) {
cnt++;
if (i == n - 1 || word.charAt(i) != word.charAt(i + 1)) {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if (cnt > 1) {
if (k > 0) { // 保证空间复杂度为 O(k)
cnts.add(cnt - 1);
}
ans = ans * cnt % MOD;
}
k--; // 注意这里把 k 减小了
cnt = 0;
}
}
if (k <= 0) {
return (int) ans;
}
int[] f = new int[k];
Arrays.fill(f, 1);
for (int c : cnts) {
// 原地计算 f 的前缀和
for (int j = 1; j < k; j++) {
f[j] = (f[j] + f[j - 1]) % MOD;
}
// 计算子数组和
for (int j = k - 1; j > c; j--) {
f[j] = (f[j] - f[j - c - 1]) % MOD;
}
}
return (int) ((ans - f[k - 1] + MOD) % MOD); // 保证结果非负
}
}
###cpp
class Solution {
public:
int possibleStringCount(string word, int k) {
int n = word.size();
if (n < k) { // 无法满足要求
return 0;
}
const int MOD = 1'000'000'007;
vector<int> cnts;
long long ans = 1;
int cnt = 0;
for (int i = 0; i < n; i++) {
cnt++;
if (i == n - 1 || word[i] != word[i + 1]) {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if (cnt > 1) {
if (k > 0) { // 保证空间复杂度为 O(k)
cnts.push_back(cnt - 1);
}
ans = ans * cnt % MOD;
}
k--; // 注意这里把 k 减小了
cnt = 0;
}
}
if (k <= 0) {
return ans;
}
vector<int> f(k, 1);
for (int c : cnts) {
// 原地计算 f 的前缀和
for (int j = 1; j < k; j++) {
f[j] = (f[j] + f[j - 1]) % MOD;
}
// 计算子数组和
for (int j = k - 1; j > c; j--) {
f[j] = (f[j] - f[j - c - 1]) % MOD;
}
}
return (ans - f[k - 1] + MOD) % MOD; // 保证结果非负
}
};
###go
func possibleStringCount(word string, k int) int {
if len(word) < k { // 无法满足要求
return 0
}
const mod = 1_000_000_007
cnts := []int{}
ans := 1
cnt := 0
for i := range word {
cnt++
if i == len(word)-1 || word[i] != word[i+1] {
// 如果 cnt = 1,这组字符串必选,无需参与计算
if cnt > 1 {
if k > 0 { // 保证空间复杂度为 O(k)
cnts = append(cnts, cnt-1)
}
ans = ans * cnt % mod
}
k-- // 注意这里把 k 减小了
cnt = 0
}
}
if k <= 0 {
return ans
}
f := make([]int, k)
for i := range f {
f[i] = 1
}
for _, c := range cnts {
// 原地计算 f 的前缀和
for j := 1; j < k; j++ {
f[j] = (f[j] + f[j-1]) % mod
}
// 计算子数组和
for j := k - 1; j > c; j-- {
f[j] -= f[j-c-1]
}
}
return (ans - f[k-1] + mod*2) % mod // 保证结果非负
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n + k^2)$,其中 $n$ 是 $\textit{word}$ 的长度。
- 空间复杂度:$\mathcal{O}(k)$。
相似题目
更多相似题目,见下面动态规划题单中的「§11.1 前缀和优化 DP」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与一般树(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
容斥 & 递推
解法:容斥 & 递推
我们把连续相同的字符视为一段。首先根据题意,原字符串中,每一段至少会保留一个字符。因此如果段数 $m$ 至少为 $k$,那么任何保留的方案都是合法的,答案就是 $\prod l$,其中 $l$ 是段长。
如果段数 $m$ 少于 $k$,我们可以扣掉总长不足 $k$ 的方案就是答案。维护 $f(i, j)$ 表示前 $i$ 段留下的总长是 $j$ 的方案数,枚举第 $i$ 段要留下多少字符,则递推方程为
$$
f(i, j) = \sum\limits_{t = 1}^{l_i} f(i - 1, j - t)
$$
用前缀和即可将复杂度优化成 $\mathcal{O}(k^2)$。答案就是 $\prod l - \sum\limits_{j = 1}^{k - 1} f(m, j)$。
参考代码(c++)
###cpp
class Solution {
public:
int possibleStringCount(string word, int K) {
int n = word.size();
const int MOD = 1e9 + 7;
vector<int> vec;
for (int i = 0, j = 0; i < n; i++) {
if (i == n - 1 || word[i] != word[i + 1]) {
vec.push_back(i - j + 1);
j = i + 1;
}
}
int m = vec.size();
long long ans = 1;
for (int x : vec) ans = ans * x % MOD;
if (m >= K) return ans;
long long f[m + 1][K], g[m + 1][K];
memset(f, 0, sizeof(f)); memset(g, 0, sizeof(g));
f[0][0] = 1;
for (int j = 0; j < K; j++) g[0][j] = 1;
for (int i = 1; i <= m; i++) {
for (int j = 1; j < K; j++) {
long long v = 0;
int t = j - vec[i - 1] - 1;
if (t >= 0) v = g[i - 1][t];
f[i][j] = (g[i - 1][j - 1] - v + MOD) % MOD;
}
for (int j = 1; j < K; j++) g[i][j] = (g[i][j - 1] + f[i][j]) % MOD;
}
return (ans - g[m][K - 1] + MOD) % MOD;
}
};
在Swift中运行Silero VAD
最近又开始学习Swift了,前段时间在AI的帮助下做了一个可以和大模型聊天的软件,当时VAD的功能很头痛,搜了下有一个付费的Cobra VAD,另外就只有靠音频能量判断了,这种方式不准。
最近做的东西又有VAD需求了,研究了很久后可以在Swift里跑Silero VAD了,直接把代码丢出来。
由于我不知道如何把ONNX模型转成Core ML的,官方ONNX Runtime只有Pods的包,我用的是另一个Swift Packags版本的ONNX Runtime,用Pods的包要把import OnnxRuntimeBindings
换一下。
//
// SileroVAD.swift
// Real-time Captions
//
// Created by yu on 2025/6/30.
//
import AVFoundation
import Foundation
import OnnxRuntimeBindings
/// 说话起止事件回调
protocol SileroVADDelegate: AnyObject {
/// 检测到"开始说话"
/// - Parameter probability: 触发时那一帧的 VAD 概率
func vadDidStartSpeech(probability: Float)
/// 检测到"结束说话"
/// - Parameter probability: 触发时那一帧的 VAD 概率
func vadDidEndSpeech(probability: Float)
}
final class SileroVAD {
// MARK: - 可调参数
public struct Config {
/// 进入说话的高阈值
public var threshold: Float = 0.5
/// 退出说话的低阈值(自动与 threshold 保持 0.15 差值)
public var negThreshold: Float { max(threshold - 0.15, 0.01) }
/// 连续多长时间高于 threshold 才算"开始说话"(秒)
public var startSecs: Float = 0.20
/// 连续多长时间低于 negThreshold 才算"结束说话"(秒)
public var stopSecs: Float = 0.80
/// 采样率,仅支持 8 kHz / 16 kHz
public var sampleRate: Int = 16000
public init() {}
}
// MARK: - 内部状态
private enum VADState {
case silence // 静音状态
case speechCandidate // 可能开始说话
case speech // 正在说话
case silenceCandidate // 可能结束说话
}
private enum VADError: Error {
case modelLoadFailed(String)
case invalidAudioFormat(String)
case inferenceError(String)
case tensorCreationFailed(String)
}
// MARK: - 核心属性
private let session: ORTSession
private var state: ORTValue
private let config: Config
public weak var delegate: SileroVADDelegate?
// 状态机相关
private var vadState: VADState = .silence
private var speechFrameCount = 0
private var silenceFrameCount = 0
private var lastProbability: Float = 0.0
// 阈值(基于配置计算的帧数)
private let speechFrameThreshold: Int
private let silenceFrameThreshold: Int
// 音频缓冲
private var sampleBuffer: [Float] = []
private let bufferSize = 512
// MARK: - 公有方法
public init(config: Config = Config(), delegate: SileroVADDelegate? = nil) {
self.config = config
self.delegate = delegate
// 计算帧数阈值(基于配置动态计算窗口时长)
let windowDurationSecs = Float(bufferSize) / Float(config.sampleRate)
speechFrameThreshold = Int(config.startSecs / windowDurationSecs)
silenceFrameThreshold = Int(config.stopSecs / windowDurationSecs)
guard let modelPath = Bundle.main.path(forResource: "silero_vad", ofType: "onnx") else {
fatalError("SileroVAD: Model file not found in bundle")
}
do {
let env = try ORTEnv(loggingLevel: .warning)
let sessionOptions = try ORTSessionOptions()
// 性能优化配置
try sessionOptions.setGraphOptimizationLevel(.all)
try sessionOptions.setIntraOpNumThreads(Int32(ProcessInfo.processInfo.processorCount))
// 尝试启用Core ML硬件加速
do {
let coreMLOptions = ORTCoreMLExecutionProviderOptions()
try sessionOptions.appendCoreMLExecutionProvider(with: coreMLOptions)
print("SileroVAD: Using Core ML Execution Provider (Neural Engine/NPU)")
} catch {
print("SileroVAD: Using optimized CPU execution with \(ProcessInfo.processInfo.processorCount) cores")
}
session = try ORTSession(env: env, modelPath: modelPath, sessionOptions: sessionOptions)
} catch {
fatalError("SileroVAD: Failed to create ONNX session: \(error)")
}
// 初始化RNN状态 (shape: 2, 1, 128)
let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
do {
state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
elementType: .float,
shape: [2, 1, 128])
} catch {
fatalError("SileroVAD: Failed to create initial state tensor: \(error)")
}
}
/// 输入音频样本,自动处理状态检测
public func feed(_ samples: [Float]) {
sampleBuffer.append(contentsOf: samples)
// 当有足够样本时自动检测
while sampleBuffer.count >= bufferSize {
if let probability = performDetection() {
updateVADState(probability: probability)
}
}
}
/// 重置内部状态机 & RNN 隐状态
public func reset() {
// 重置状态机
vadState = .silence
speechFrameCount = 0
silenceFrameCount = 0
lastProbability = 0.0
// 清空缓冲区
sampleBuffer.removeAll()
// 重置RNN状态
let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
do {
state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
elementType: .float,
shape: [2, 1, 128])
} catch {
print("SileroVAD: Failed to reset state tensor: \(error)")
}
}
// MARK: - 私有方法
private func performDetection() -> Float? {
guard sampleBuffer.count >= bufferSize else {
return nil
}
// 取出一个窗口的样本
let vadInput = Array(sampleBuffer.prefix(bufferSize))
sampleBuffer.removeFirst(bufferSize)
do {
let probability = try runInference(audioData: vadInput)
lastProbability = probability
return probability
} catch {
print("SileroVAD: Detection error: \(error)")
return nil
}
}
private func runInference(audioData: [Float]) throws -> Float {
guard audioData.count == 512 else {
throw VADError.invalidAudioFormat("Audio data must be exactly 512 samples")
}
// 创建输入张量
let inputTensor = try ORTValue(
tensorData: NSMutableData(data: Data(bytes: audioData, count: audioData.count * 4)),
elementType: .float,
shape: [1, 512]
)
// 创建采样率张量
var srData = Int64(config.sampleRate)
let srTensor = try ORTValue(
tensorData: NSMutableData(data: Data(bytes: &srData, count: 8)),
elementType: .int64,
shape: [1]
)
// 准备输入
let inputs: [String: ORTValue] = [
"input": inputTensor,
"state": state,
"sr": srTensor,
]
// 执行推理
let allOutputNames = try session.outputNames()
let outputs = try session.run(withInputs: inputs, outputNames: Set(allOutputNames), runOptions: nil)
// 提取结果
guard let outputTensor = outputs["output"] else {
throw VADError.inferenceError("Missing 'output' tensor")
}
guard let newStateTensor = outputs["stateN"] else {
throw VADError.inferenceError("Missing 'stateN' tensor")
}
// 更新状态
state = newStateTensor
// 提取概率值
let tensorData = try outputTensor.tensorData() as Data
let probability = tensorData.withUnsafeBytes { bytes in
bytes.load(as: Float.self)
}
return probability
}
private func updateVADState(probability: Float) {
let isHighProbability = probability >= config.threshold
let isLowProbability = probability <= config.negThreshold
switch vadState {
case .silence:
if isHighProbability {
vadState = .speechCandidate
speechFrameCount = 1
silenceFrameCount = 0
}
case .speechCandidate:
if isHighProbability {
speechFrameCount += 1
if speechFrameCount >= speechFrameThreshold {
vadState = .speech
delegate?.vadDidStartSpeech(probability: probability)
}
} else {
vadState = .silence
speechFrameCount = 0
}
case .speech:
if isLowProbability {
vadState = .silenceCandidate
silenceFrameCount = 1
speechFrameCount = 0
} else if isHighProbability {
// 继续说话,重置静音计数
silenceFrameCount = 0
}
case .silenceCandidate:
if isLowProbability {
silenceFrameCount += 1
if silenceFrameCount >= silenceFrameThreshold {
vadState = .silence
delegate?.vadDidEndSpeech(probability: probability)
}
} else if isHighProbability {
vadState = .speech
silenceFrameCount = 0
}
}
}
}
要下载模型silero_vad.onnx
丢进项目。
当然这个代码也是Claude帮我写的。
Swift 的多平台策略,需要我们大家一起来建设 | 肘子的 Swift 周报 #091
在 weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。
Swift 的多平台策略,需要我们大家一起来建设
继 2025 年 2 月 Swift 社区论坛发布关于启动 Android Community Workgroup 的消息数月后,Swift.org 于上周正式宣布成立官方 Android 工作组。这标志着由官方主导的 Swift 安卓平台支持正式启动,未来 Swift 开发者有望获得更完善的安卓适配工具链与开发体验。
不过,在欣喜之余,我们也应正视一个现实:对于绝大多数 Swift 开发者来说,长期以来的开发工作深度依赖苹果生态,日常所用 API 多与系统框架强耦合。尽管 Swift 社区和苹果已着手推进 Foundation 的纯 Swift 化改造,并陆续提供更多跨平台基础库,但这距离满足实际跨平台开发的需求仍有相当差距。
不久前,Swift Package Index 在原有对苹果平台和 Linux 的兼容性标识基础上,新增了对 Android 与 Wasm 平台的支持,侧面反映出社区对多平台适配的重视。我也借此机会让自己的两个库完成了对 Linux 的兼容。不过在适配过程中也深刻体会到,目前还缺乏一个便捷、统一的跨平台开发环境。虽然这两个库的适配较为简单,仅通过 GitHub Actions 就完成了编译测试和修复,但若将来需要支持更多平台,社区能否构建一个便利、安全的适配机制将变得至关重要。
近年来,Swift 在多平台战略上的推进明显提速,但若想真正成为跨平台开发者的主流选择,仅靠官方与苹果的努力还远远不够。我们每一位 Swift 开发者的参与同样不可或缺。Swift 越强大,Swift 开发者越受益。Swift 的多平台生态,需要我们共同建设!
原创
NotificationCenter.Message:Swift 6.2 并发安全通知的全新体验
NotificationCenter 作为 iOS 开发中的经典组件,为开发者提供了灵活的广播——订阅机制。然而,随着 Swift 并发模型的不断演进,传统基于字符串标识和 userInfo
字典的通知方式暴露出了诸多问题。为了彻底解决这些痛点,Swift 6.2 在 Foundation 中引入了全新的并发安全通知协议:NotificationCenter.MainActorMessage
和 NotificationCenter.AsyncMessage
。它们充分利用 Swift 的类型系统和并发隔离特性,让消息的发布与订阅在编译期就能得到验证,从根本上杜绝了“线程冲突”和“数据类型错误”等常见问题。
近期推荐
Xcode Coding Intelligence 逆向解析简报 (Reverse-Engineering Xcode's Coding Intelligence Prompt)
在 Xcode 26 中,苹果正式推出了备受期待的 AI 编码助手 —— Coding Intelligence。相较于市面上已有的 AI 编程工具,苹果在系统提示词(system prompt)的设计上是否有自己的哲学?Peter Friese 借助 Proxyman 对其进行了深入逆向分析。通过这些解析出的提示词内容,我们不仅可以了解 Coding Intelligence 的工作机制,也能窥见苹果对现代开发实践的倾向性,比如:强烈推荐使用 Swift Concurrency(async/await、actor)而非 Combine,测试建议使用 Swift Testing 框架与宏。这些设计细节,是苹果开发范式的重要指标。
SwiftUI 设计系统中的语义颜色设计 (SwiftUI Design System Considerations: Semantic Colors)
在构建 SwiftUI 设计系统 API 时,如何优雅地处理 语义颜色(Semantic Colors) 始终是一个令人头疼的问题。Magnus Jensen 在本文中系统梳理了常见方案的优缺点,并提出了一种基于宏(macro)的解决路径,力求实现 可读性强、类型安全、上下文感知 的色彩系统。如果你正打算为自己的 SwiftUI 项目设计一套结构清晰、可维护的风格体系,这篇文章值得一读。
iOS 内存效率指南系列 (Memory Efficiency in iOS)
随着项目复杂度的提升,开发者终将面对内存相关的问题:内存泄漏、系统警告,甚至因资源占用过高被系统强制终止。在这种情况下,如何诊断问题、控制内存占用,是对开发者经验与体系理解的深度考验。Anton Gubarenko 在两篇文章(内存优化篇)中,系统梳理了 iOS 应用内存使用的评估方式、诊断工具以及优化手段,构建出一套完整、实用的内存管理知识体系。
What is @concurrent in Swift 6.2?
从 Swift 最近的几个版本更新和 Xcode 26 的表现可以看出,Swift 团队正有意识地优化并发编程的开发体验。通过启用新的默认行为,开发者无需在一开始就理解所有细节,便能写出更安全的并发代码。@concurrent
的引入,正是这一策略下的产物之一。在 Donny Wals 的这篇文章中,他详细介绍了 @concurrent
的背景与用途。简单来说,@concurrent
是 Swift 6.2 引入的显式并发标记,主要用于在启用 NonIsolatedNonSendingByDefault
特性时,明确指定函数运行在全局执行器上,从而在需要时将工作负载转移到后台线程,避免阻塞调用者所在的 actor(如主线程)。
或许有人会质疑 Swift 是否又在“用新关键字补旧洞”,但从语言设计趋势来看,随着并发模型逐步完善,许多旧关键字的使用将逐渐被默认机制吸收、简化甚至隐藏。
Swift 与 Java 互操作 (Swift 6.2 Java interoperability in Practice)
Swift 与 Java 的互操作并非新鲜事物,但过往的解决方案往往过程复杂且容易出错。Swift 6.2 引入的 swift-java
包具有划时代意义——这是首次提供官方支持、与工具链深度集成、开发体验接近一等公民的互操作方案,标志着 Swift 和 Java 之间真正意义上的“无缝互通”正式到来。Artur Gruchała 通过一个完整的示例项目,详细演示了如何从 Swift 端调用 Java 方法、构建双语言协作的 CLI 应用,并深入分析了实际开发中容易踩坑的关键细节——特别是 classpath 配置等看似简单却至关重要的环节。
Kodeco 教程:迁移到 Swift 6 (Migrating to Swift 6 Tutorial)
Swift 6 引入了更严格的并发规则与更加结构化的编程范式。在迁移过程中,理解隔离域、Sendable 类型、默认行为,以及 @concurrent
的使用变得尤为重要。Audrey Tam 通过一个完整的 SwiftUI 示例项目(附项目源码),系统演示了从 Swift 5 迁移至 Swift 6.2 的全过程,涵盖 Xcode 设置、并发语义调整与数据隔离等核心环节,是一篇很具实用价值的迁移教程。
Modern Concurrency - Swift 6.2 Suite of Examples
如何在 async/await 中实现类似 Combine 的 throttle 操作?如何持续追踪 @Observable
属性的变化?如何构建支持多消费者的异步流?Lucas van Dongen 在这个开源项目中给出了系统性的实践示例。他汇集了 Swift 6.2 并发模型下的多种模式,演示了如何在实际项目中逐步替代 Combine,迁移到更现代、类型安全的并发范式。
是否升级应用的最低支持版本?(Considerations for New iOS Versions)
WWDC 25 中 Liquid Glass 的登场令人惊艳,但要同时支持两种视觉风格,对开发资源是一大考验。这也让很多开发者开始思考是否应放弃对旧系统的支持。David Smith 建议从两个角度判断:现有用户影响与新用户流失。以他的 Widgetsmith 应用为例,当前仍有约 9% 的新增用户来自旧系统,一旦抬高最低支持版本将直接失去这部分潜在用户。他认为,只有当旧系统用户占比降至个位数时,再做版本升级才更合理——简化技术负担,不应以牺牲业务增长为代价。
活动
AdventureX 25 游客指南
AdventureX 25 将于 2025 年 7 月 23 日至 27 日在杭州市湖畔创研中心与未来科技城学术交流中心举行。本指南包含活动行程介绍、参与方式、群聊福利、出行与住宿建议及注意事项等内容。不论你是来逛展、互动,还是寻找志同道合的伙伴,这份指南都将帮助你轻松规划行程~
往期内容
THANK YOU
如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。
在 weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。
Re: 0x01. 从零开始的光线追踪实现-光线、相机及背景
目标
书接上文,之前已经实现一个铺满整个窗口的红色填充,这趟来实现光线、相机及背景。
本节最终效果
计算物体在窗口坐标的位置
其实这个光追的思维模式很简单,就是从相机处开始发射一束射线,射线撞到哪些“物体”,就计算跟该“物体”相交的颜色。如图所示,从相机处发射射线,以左上角开始逐像素扫一遍,计算对应像素的颜色
我们再看看 viewport 的坐标,假设一个窗口大小是 (没错,PSP 的分辨率😁)的宽高,那么 的区间就是 , 的区间就是
现在我们要来处理一个标准化的像素坐标,处理像素在屏幕中的 2D 位置
struct Vertex {
float4 position [[position]];
};
fragment float4 fragmentFn(Vertex in [[stage_in]]) {
auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
// ...
}
上面这一步的作用是把像素级的屏幕坐标转成区间 的归一化坐标。
假设现在有一个物体,它的坐标是 ,通过上面的计算式子可以得出
,说明它在屏幕的中间
接着我们假定相机的位置是原点 ,相机距离 viewport 。我们计算出宽高的比例再套进这个计算 (2 * uv - float2(1))
,等于讲把 映射成 的范围,其实就是
原始 uv | 变换后 |
---|---|
(0, 0) | (-1, -1) 左下角 |
(1, 0) | (1, -1) 右下角 |
(0.5, 0.5) | (0, 0) 居中 |
(1, 1) | (1, 1) 右上角 |
再把 (2 * uv - float2(1))
跟 float2(aspect_ratio, -1)
相乘等于讲横向乘以 aspect_ratio
用来做等比例变换
至于纵向乘以 -1
,那是因为在 Metal 中, 轴是向下为正,乘一下 -1
就可以把 轴翻转变成向上为正,接下来计算方向就简单多了,因为 轴面向相机,其实就是相机距离取反,上面假定相机距离为 1
,所以取反再跟 放一块就是方向,同时我们又假定相机的位置是原点 ,那么求光线就很容易了
struct Ray {
float3 origin;
float3 direction;
};
fragment float4 fragmentFn(Vertex in [[stage_in]]) {
// ...
const auto focus_distance = 1.0;
// ...
const auto direction = float3(uv, -focus_distance);
Ray ray = { origin, direction };
}
现在既然有了光线,再就是要计算一下光线的颜色,因为目前场景中没有物体,所以就默认计算背景色,我们先把光线从 映射回 ,然后再线性插值计算渐变天空颜色,所以先要让光线经过归一化操作到
// [-1, 1]
normalize(ray.direction)
然后再给该向量加
// [-1, 1] + 1 = [0, 2]
normalize(ray.direction) + 1
然后把 乘以 就转成 了,之后再代入线性插值公式计算结果,具体渐变色值可以根据自己的需求调整,我这里直接使用 Ray Tracing in One Weekend 的色值 float3(0.5, 0.7, 1)
float3 sky_color(Ray ray) {
const auto a = 0.5 * (normalize(ray.direction).y + 1);
return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}
最后总结一下代码
struct Ray {
float3 origin;
float3 direction;
};
float3 sky_color(Ray ray) {
const auto a = 0.5 * (normalize(ray.direction).y + 1);
return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}
fragment float4 fragmentFn(Vertex in [[stage_in]]) {
const auto origin = float3(0);
const auto focus_distance = 1.0;
const auto aspect_ratio = 480 / 272;
auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
uv = (2 * uv - float2(1)) * float2(aspect_ratio, -1);
const auto direction = float3(uv, -focus_distance);
Ray ray = { origin, direction };
return float4(sky_color(ray), 1);
}
browser-tools-mcp前端开发调试利器
如果你有过前端项目开发的经历,那么一定会经常打开浏览器自带开发者工具,查看网络请求或者控制台日志等等。遇到问题还会复制粘贴里面信息去搜索引擎搜索信息。即使当前ai非常强大,你也不得不手动告知ai你遇到的上下文情景。来来回回操作会非常繁琐,幸运的是这个mcp工具——browser-tools-mcp转为解决上面的问题而生。
怎么用呢?
1. 前提条件
由于是javascript开发
,确保电脑上有安装node
2. step1: 浏览器安装插件
需要注意的是,如果你直接在chrome扩展商店是搜不到的,应该没上架。所以要手动去github.com/AgentDeskAI… 下载扩展安装包,解压后;在浏览器中通过打开开发者模式手动安装。
3. step2: 使用工具中添加mcp
假设你使用的是cursor
,那么进入设置界面
添加添加New Mcp server配置如下信息
{
"mcpServers": {
"browser-tools": {
"command": "npx",
"args": ["@agentdeskai/browser-tools-mcp@latest"]
}
}
}
4. step3: 终端启动工具服务
这一步必不可少,它是一个中间服务,用于与你浏览器中的插件通信;启动后你也能看到些日志信息
5. step4: 直接使用
- 首先打开我们安装的插件
- 打开页面的开发者工具窗口
3. 在你的IDE中调用mcp即可获取到相关的调试信息
可以看到成功获取了浏览器开发工具中的信息。
最后
它的主要好处是打通了ai和浏览器调试的鸿沟,ai能直接获取到调试信息,大大加快代码调试速度。