函数的前推和拉回
前推(Pushforward)和拉回(Pullback)是函数复合的两个方向
- 前推:后复合(post-composition),将用于前推的函数作用在另一个函数的输出端
- 拉回:前复合(pre-composition),将用于拉回的函数作用在另一个函数的输入端
对于函数
其诱导的前推为
前推改变原函数的输出类型
其诱导的拉回为
拉回改变原函数的输入类型
这里使用 TypeScript 实现对函数的前推和拉回
// 函数
type Fn<A, B> = (a: A) => B;
// 前推:后复合
function pushforward<A, B>(f: Fn<A, B>) {
return <X>(g: Fn<X, A>): Fn<X, B> =>
(x) =>
f(g(x));
}
// 拉回:前复合
function pullback<A, B>(f: Fn<A, B>) {
return <Y>(g: Fn<B, Y>): Fn<A, Y> =>
(a) =>
g(f(a));
}查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { pushforward, pullback } from "./utils.ts";
// number => boolean
const f = (x: number) => x > 4;
// (string => number) -> (string => boolean)
const pushed = pushforward(f)(
(s: string) => s.length
);
// (boolean => string) -> (number => string)
const pulled = pullback(f)(
x => `value=${x}`
);
interface Param {
s: string
n: number
}
export function run({ s, n }: Param) {
return [
`pushed: ${pushed(s)}`,
`pulled: ${pulled(n)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
函数前推和函数拉回的结合律
复合映射也可以诱导出前推和拉回。
设两个函数为
二者的复合为
复合映射诱导的前推为
复合映射诱导的拉回为
把左侧的复合也写为前推和拉回,得到
因此,复合本身的结合律给出了前推和拉回的结合律。
我们用 TypeScript 实现一个二元运算表示函数的复合
// 函数复合
function compose<A, B, C>(f: Fn<B, C>, g: Fn<A, B>): Fn<A, C> {
return (a) => f(g(a));
}查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { pushforward, compose } from "./utils.ts";
const h = (x: number) => x + 1;
const g = (x: number) => x * 2;
const f = (x: number) => `Result: ${x}`;
// 复合映射诱导的前推
const comp = pushforward(
compose(f, g)
)(h)
// 前推左结合
const lhs = pushforward(
pushforward(f)(g)
)(h);
// 前推右结合
const rhs = pushforward(f)(
pushforward(g)(h)
);
interface Param {
x: number
}
export function run({ x }: Param) {
return [
`comp: ${comp(x)}`,
`lhs: ${lhs(x)}`,
`rhs: ${rhs(x)}`,
].join("\n")
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { pullback, compose } from "./utils.ts";
const f = (x: number) => x + 1;
const g = (x: number) => x * 2;
const h = (x: number) => `Result: ${x}`;
// 复合映射诱导的拉回
const comp = pullback(
compose(g, f)
)(h)
// 拉回左结合
const lhs = pullback(
pullback(f)(g)
)(h);
// 拉回右结合
const rhs = pullback(f)(
pullback(g)(h)
);
interface Param {
x: number
}
export function run({ x }: Param) {
return [
`comp: ${comp(x)}`,
`lhs: ${lhs(x)}`,
`rhs: ${rhs(x)}`,
].join("\n")
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
函数前推和函数拉回的交换律
设函数为
那么有
得到前推和拉回的交换律
即函数先前推再拉回等于先拉回再前推。
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { pushforward, pullback } from "./utils.ts";
const g = (x: number) => x + 1;
const h = (x: number) => x * 2;
const f = (x: number) => `Result: ${x}`;
// 先前推再拉回
const lhs = pullback(g)(pushforward(f)(h));
// 先拉回再前推
const rhs = pushforward(f)(pullback(g)(h));
interface Param {
x: number
}
export function run({ x }: Param) {
return [
`lhs: ${lhs(x)}`,
`rhs: ${rhs(x)}`,
].join("\n")
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
作为二元运算的前推和拉回
进一步把前推和拉回本身当作二元运算
结合律可以写为
交换律可以写为
用 TypeScript 实现两个二元运算分别表示前推和拉回
// 前推二元运算
function pushBin<A, B, C>(f: Fn<B, C>, g: Fn<A, B>): Fn<A, C> {
return pushforward(f)(g);
}
// 拉回二元运算
function pullBin<A, B, C>(f: Fn<A, B>, g: Fn<B, C>): Fn<A, C> {
return pullback(f)(g);
}查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { pullBin, pushBin } from "./utils.ts";
const f = (x: number) => x + 1;
const g = (x: number) => x * 2;
const h = (x: number) => `Result: ${x}`;
// 前推左结合
const pushLeft = pushBin(pushBin(h, g), f);
//前推右结合
const pushRight = pushBin(h, pushBin(g, f));
// 拉回左结合
const pullLeft = pullBin(pullBin(f, g), h);
// 拉回右结合
const pullRight = pullBin(f, pullBin(g, h));
interface Param {
x: number
}
export function run({ x }: Param) {
return [
`pushLeft: ${pushLeft(x)}`,
`pushRight: ${pushRight(x)}`,
`pullLeft: ${pullLeft(x)}`,
`pullRight: ${pullRight(x)}`,
].join("\n")
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
张量的前推和拉回
张量是线性代数里的术语,这里我们将其抽象为输入类型相同的多参数函数
或者简记为
函数 诱导出一个对张量的前推
即把用于前推的函数作用在张量的输出上。前推后张量的类型变为
函数 诱导出一个对张量的拉回
即把用于拉回的函数作用在张量的每个输入上。拉回后张量的类型变为
使用 TypeScript 实现张量。这里选择把张量作为一个普通对象,包含阶数和映射
// 张量类型
type Tensor<Args extends unknown[], Y> = {
readonly rank: Args["length"];
readonly apply: (...args: Args) => Y;
};
// 张量构造器
function tensor<Args extends unknown[], Y>(
rank: Args["length"],
apply: (...args: Args) => Y,
): Tensor<Args, Y> {
return { rank, apply };
}也可以把张量作为一个函数对象,通过 Object.assign 设置阶数。后者可读性会稍差一点,好处是可以像普通函数一样使用张量
// 张量类型
export type Tensor<Args extends unknown[], Y> = ((...args: Args) => Y) & {
readonly rank: Args["length"];
};
// 张量构造器
export function tensor<Args extends unknown[], Y>(
rank: Args["length"],
fn: (...args: Args) => Y,
): Tensor<Args, Y> {
return Object.assign(fn, { rank });
}我们使用可读性更好的普通对象版本,分别实现对张量的前推和拉回
// 张量前推:作用于输出
function tensorPushforward<Y, A>(f: (y: Y) => A) {
return <Args extends unknown[]>(T: Tensor<Args, Y>): Tensor<Args, A> =>
tensor(T.rank, (...xs: Args) => f(T.apply(...xs)));
}
// 张量拉回:作用于每个输入
function tensorPullback<B, X>(g: (b: B) => X) {
return <Args extends X[], Y>(T: Tensor<Args, Y>) => {
type Bs = { [K in keyof Args]: B };
return tensor(T.rank as Bs["length"], (...bs: Bs) =>
T.apply(...(bs.map(g) as Args)),
);
};
}查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { tensor, tensorPushforward, tensorPullback } from "./utils.ts";
// number^2 => number
const T = tensor(2, (x: number, y: number) => x*x + y*y);
// number^2 => string
const pushed = tensorPushforward(
(x: number) => `value=${x}`
)(T);
// boolean^2 => number
const pulled = tensorPullback(
(x: boolean) => x ? 2 : 3
)(T);
interface Param {
n1: number,
n2: number
b1: boolean,
b2: boolean,
}
export function run({ n1, n2, b1, b2 }: Param) {
return [
`pushed: ${pushed.apply(n1, n2)}`,
`pulled: ${pulled.apply(b1, b2)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
张量前推的结合律
设张量为
两个函数为
复合函数 诱导的前推为
因此
将左侧的函数复合写为前推,得到张量前推的结合律
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { tensor, tensorPushforward, pushforward } from "./utils.ts";
const T = tensor(2, (x: number, y: number) => x*x + y*y);
const f = (x: number) => x + 1;
const g = (x: number) => `value=${x}`;
// 张量前推左结合
const pushLeft = tensorPushforward(
pushforward(g)(f)
)(T);
// 张量前推右结合
const pushRight = tensorPushforward(g)(
tensorPushforward(f)(T)
);
interface Param {
n1: number,
n2: number
}
export function run({ n1, n2 }: Param) {
return [
`pushLeft: ${pushLeft.apply(n1, n2)}`,
`pushRight: ${pushRight.apply(n1, n2)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
张量拉回的结合律
设张量为
两个函数为
复合函数 诱导的拉回为
因此
将左侧的函数复合写为拉回,得到张量拉回的结合律
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { tensor, tensorPullback } from "./utils.ts";
const T = tensor(2, (x: number, y: number) => x*x + y*y);
const h = (x: string) => Number(x);
const k = (x: boolean) => x ? "2" : "3";
// 张量拉回左结合
const pullLeft = tensorPullback(
(x: boolean) => h(k(x))
)(T);
// 张量拉回右结合
const pullRight = tensorPullback(k)(
tensorPullback(h)(T)
);
interface Param {
b1: boolean,
b2: boolean
}
export function run({ b1, b2 }: Param) {
return [
`pullLeft: ${pullLeft.apply(b1, b2)}`,
`pullRight: ${pullRight.apply(b1, b2)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
张量前推和张量前推的交换律
设
那么有
得到张量前推和张量拉回的交换律
即对张量先前推再拉回等于先拉回再前推。
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { tensor, tensorPullback, tensorPushforward } from "./utils.ts";
const T = tensor(2, (x: number, y: number) => x*x + y*y);
const f = (x: number) => x + 1;
const g = (x: string) => x.length;
// 先拉回再前推
const lhs = tensorPushforward(f)(
tensorPullback(g)(T)
);
// 先前推再拉回
const rhs = tensorPullback(g)(
tensorPushforward(f)(T)
);
interface Param {
s1: string,
s2: string
}
export function run({ s1, s2 }: Param) {
return [
`lhs: ${lhs.apply(s1, s2)}`,
`rhs: ${rhs.apply(s1, s2)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
函数加法
假设函数的值位于一个具有加法结构的集合中,例如实数
定义函数的加法为逐点乘法
TypeScript 实现为
function add<X>(f: Fn<X, number>, g: Fn<X, number>): Fn<X, number> {
return (x) => f(x) + g(x);
}拉回保持加法
设
由 诱导的拉回满足
因此拉回保持函数加法
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { add, pullback } from "./utils";
const f = (x: number) => x + 1;
const g = (x: number) => x * x;
const h = (x: number) => x * 2;
// 先相加后拉回
const lhs = pullback(h)(
add(f, g)
);
// 先拉回后相加
const rhs = add(
pullback(h)(f),
pullback(h)(g)
);
interface Param {
n: number,
}
export function run({ n }: Param) {
return [
`lhs: ${lhs(n)}`,
`rhs: ${rhs(n)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
前推不一定保持加法
前推通常不保持加法。
函数先相加后前推
函数先前推后相加
若想
则要求
这并不是所有函数都满足的性质。一个简单的反例是
取
左侧为
右侧为
当 时二者就不相等。
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { add, pushforward } from "./utils";
const f = (x: number) => x * x;
const u = (x: number) => x;
const v = (x: number) => 1;
// 先相加后前推
const lhs = pushforward(f)(
add(u, v)
);
// 先前推后相加
const rhs = add(
pushforward(f)(u),
pushforward(f)(v)
);
interface Param {
n: number,
}
export function run({ n }: Param) {
return [
`lhs: ${lhs(n)}`,
`rhs: ${rhs(n)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
函数乘法
假设函数的值位于一个具有乘法结构的集合中,例如实数
定义函数的乘法为逐点乘法
TypeScript 实现为
function mul<X>(f: Fn<X, number>, g: Fn<X, number>): Fn<X, number> {
return (x) => f(x) * g(x);
}拉回保持乘法
设
由 诱导的拉回满足
因此拉回保持函数乘法
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { mul, pullback } from "./utils";
const f = (x: number) => x + 1;
const g = (x: number) => x * x;
const h = (x: number) => x * 2;
// 先相乘后拉回
const lhs = pullback(h)(
mul(f, g)
);
// 先拉回后相乘
const rhs = mul(
pullback(h)(f),
pullback(h)(g)
);
interface Param {
n: number,
}
export function run({ n }: Param) {
return [
`lhs: ${lhs(n)}`,
`rhs: ${rhs(n)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
前推不一定保持乘法
前推通常不保持乘法。
函数先相乘后前推
函数先前推后相乘
若想
则要求
这要求用于前推的函数本身满足满足这一性质——但显然并非函数都满足。一个简单的反例是
取
左侧为
右侧为
当 时二者就不相等。
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { mul, pushforward } from "./utils";
const f = (x: number) => x + 1;
const u = (x: number) => x;
const v = (x: number) => 1;
// 先相乘后前推
const lhs = pushforward(f)(
mul(u, v)
);
// 先前推后拉回
const rhs = mul(
pushforward(f)(u),
pushforward(f)(v)
);
interface Param {
n: number,
}
export function run({ n }: Param) {
return [
`lhs: ${lhs(n)}`,
`rhs: ${rhs(n)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
张量加法和张量乘法
对于张量,我们可以定义两种加法和两种乘法。
第一种张量加法和张量乘法
第一种加法和乘法类似于函数的加法和乘法,逐点定义为
这要求两个张量的类型相同,并且值位于一个具有加法结构和乘法结构的集合中,比如实数
因此,两个 阶张量相加或相乘,得到的是一个 阶张量。并且,这种张量加法和张量乘法与函数加法和函数乘法没有什么区别。
对于
拉回保持张量加法
拉回保持张量乘法
对于
前推不一定保持张量加法。 和 分别展开为
而 不是所有的 都满足。
前推不一定保持张量乘法。 和 分别展开为
而 不是所有的 都满足。
第二种张量加法和张量乘法
第二种张量加法和张量乘法允许两个张量的类型不同
第二种加法称为张量和,对 逐点定义为
第二种乘法称为张量积,对 逐点定义为
因此,一个 阶张量和一个 阶张量进行张量和或张量积,得到的是一个 阶张量。
TypeScript 实现为
// 辅助函数:按左侧张量的阶数切分输入
function splitTensorArgs<A extends unknown[], B extends unknown[]>(
args: [...A, ...B],
leftRank: A["length"],
): [A, B] {
return [args.slice(0, leftRank), args.slice(leftRank)] as [A, B];
}
// 张量和
function tensorPlus<A extends unknown[], B extends unknown[]>(
T: Tensor<A, number>,
S: Tensor<B, number>,
): Tensor<[...A, ...B], number> {
return tensor(T.rank + S.rank, (...args: [...A, ...B]) => {
const [left, right] = splitTensorArgs<A, B>(args, T.rank);
return T.apply(...left) + S.apply(...right);
});
}
// 张量积
function tensorTimes<A extends unknown[], B extends unknown[]>(
T: Tensor<A, number>,
S: Tensor<B, number>,
): Tensor<[...A, ...B], number> {
return tensor(T.rank + S.rank, (...args: [...A, ...B]) => {
const [left, right] = splitTensorArgs<A, B>(args, T.rank);
return T.apply(...left) * S.apply(...right);
});
}查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import { tensor, tensorPlus, tensorTimes } from "./utils.ts";
// (number, number) -> number
const T = tensor(2, (x: number, y: number) => x * x + y * y);
// string -> number
const S = tensor(1, (x: string) => x.length);
// (number, number, string) => number
const sum = tensorPlus(T, S);
const product = tensorTimes(T, S);
interface Param {
n1: number;
n2: number;
s: string;
}
export function run({ n1, n2, s }: Param) {
return [
`T(x, y) = ${T.apply(n1, n2)}`,
`S(s) = ${S.apply(s)}`,
`sum.rank = ${sum.rank}, sum(x, y, s) = ${sum.apply(n1, n1, s)}`,
`product.rank = ${product.rank}, product(x, y, s) = ${product.apply(n1, n2, s)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。
现假设两个张量的输入类型相同,只有阶数不同
对于
拉回保持张量和
拉回保持张量积
对于
前推不一定保持张量和。 和 分别展开为
而 不是所有的 都满足。
前推不一定保持张量积。 和 分别展开为
而 不是所有的 都满足。
查看详情
| 参数 | 变量名 | 类型 | 约束 |
|---|
源代码
import {
tensor,
tensorPlus,
tensorTimes,
tensorPullback,
tensorPushforward,
} from "./utils.ts";
const T = tensor(2, (x: number, y: number) => x + y);
const S = tensor(1, (x: number) => x);
const f = (x: number) => 2 * x;
const g = (x: number) => x + 1;
const pullPlusLeft = tensorPullback(f)(tensorPlus(T, S));
const pullPlusRight = tensorPlus(tensorPullback(f)(T), tensorPullback(f)(S));
const pullTimesLeft = tensorPullback(f)(tensorTimes(T, S));
const pullTimesRight = tensorTimes(tensorPullback(f)(T), tensorPullback(f)(S));
const pushPlusLeft = tensorPushforward(g)(tensorPlus(T, S));
const pushPlusRight = tensorPlus(tensorPushforward(g)(T), tensorPushforward(g)(S));
const pushTimesLeft = tensorPushforward(g)(tensorTimes(T, S));
const pushTimesRight = tensorTimes(tensorPushforward(g)(T), tensorPushforward(g)(S));
interface Param {
n1: number;
n2: number;
n3: number;
}
export function run({ n1, n2, n3 }: Param) {
return [
`pullPlusLeft: ${pullPlusLeft.apply(n1, n2, n3)}`,
`pullPlusRight: ${pullPlusRight.apply(n1, n2, n3)}`,
"",
`pullTimesLeft: ${pullTimesLeft.apply(n1, n2, n3)}`,
`pullTimesRight: ${pullTimesRight.apply(n1, n2, n3)}`,
"",
`pushPlusLeft: ${pushPlusLeft.apply(n1, n2, n3)}`,
`pushPlusRight: ${pushPlusRight.apply(n1, n2, n3)}`,
"",
`pushTimesLeft: ${pushTimesLeft.apply(n1, n2, n3)}`,
`pushTimesRight: ${pushTimesRight.apply(n1, n2, n3)}`,
].join("\n");
}
运行结果
点击运行查看结果演示正在加载,请确保浏览器已启用 JavaScript。