Typescript 能做简单的函数组合吗?
Posted
技术标签:
【中文标题】Typescript 能做简单的函数组合吗?【英文标题】:Is Typescript capable of doing simple function composition? 【发布时间】:2019-12-18 04:47:45 【问题描述】:我写了以下 compose、map 和 filter 的基本实现来测试。
下面设置类型和功能,然后实现。
javascript 似乎没问题,但是当使用compose
时,打字稿会显示误报。具体来说,它似乎了解第一个函数将返回什么,但它没有将该信息传递给第二个函数的输入。设置后在下面进一步解释。
import curry from './curry'
type f1<a, b> = (a: a) => b
type f2_2<a, b, c> = (a: a, b: b) => c
type f2_1<a, b, c> = (a: a) => (b: b) => c
type f2<a, b, c> = f2_2<a, b, c> & f2_1<a, b, c>
type p<a> = (a: a) => boolean
//====[compose]================================
type b3 = <a, b, c>(f: f1<b, c>, g: f1<a, b>, a: a) => c
type b2 = <a, b, c>(f: f1<b, c>, g: f1<a, b>) => f1<a, c>
type b1 = <a, b, c>(f: f1<b, c>) => f2<f1<a, b>, a, c>
type b = b1 & b2 & b3
// bluebird :: (b -> c) -> (a -> b) -> a -> c
const compose: b = curry((f, g, a) => f(g(a)))
//====[filter]=================================
type fil2 = <a>(p: p<a>, xs: a[]) => a[]
type fil1 = <a>(p: p<a>) => f1<a[], a[]>
type fil = fil1 & fil2
// filter :: (a -> Boolean) -> [a] -> [a]
const filter: fil = curry((p, xs) =>
const len = xs.length
const r = Array()
for (let [i, j] = [0, 0]; i < len; i++)
const v = xs[i]
if (p(v))
r[j++] = v
return r
)
//====[mapArr]=================================
type m2 = <a, b>(f1: f1<a, b>, xs: a[]) => b[]
type m1 = <a, b>(f: f1<a, b>) => f1<a[], b[]>
type m = m2 & m1
// map :: (a -> b) -> [a] -> [b]
const mapArr: m = curry((fn, xs) =>
const len = xs.length
const r = Array(len)
for (let i = 0; i < len; i++)
r[i] = fn(xs[i])
return r
)
//====[prop]===================================
type z2 = <o, k extends keyof o>(propName: k, source: o) => o[k]
type z1 = <o, k extends keyof o>(propName: k) => f1<o, o[k]>
type z = z2 & z1
// prop :: String -> a -> b
// prop :: Number -> a -> b
// prop :: Symbol -> a -> b
const prop: z = curry((propName, obj) => obj[propName])
当我将鼠标悬停在下面合成的过滤器函数上时,TS 知道它将返回data[]
;但是,如果我将鼠标悬停在 mappArr
函数上,TS 会显示输入是 unknown[]
,因此它会为 id
字段抛出误报。我做错了什么?
//====[typescript test]===================================
interface data
relationId: string
id: string
type isMine = p<data>
// isMine :: a -> Boolean
const isMine: isMine = x => x.relationId === '1'
type t = f1<data[], string[]>
const testFn: t = compose(
// @ts-ignore
mapArr(prop('id')),
//=> ^^^^^
// error TS2345: Argument of type '"id"' is not assignable to
// parameter of type 'never'.
filter(isMine)
)
//====[javascript test]================================
const r = testFn([
id: '3', relationId: '1' ,
id: '5', relationId: '3' ,
id: '8', relationId: '1' ,
])
test('javascript is correct', () =>
expect(r).toEqual(['3', '8'])
)
当我使用 curried 参数调用 compose 时,Typescript 的误报会变得更糟。
const testFn: t = compose (mapArr(prop('id')))
(filter(isMine))
//=> ^^^^^^^^^^^^^^^
// error TS2322: Type 'unknown[]' is not assignable to type 'f1<data[], string[]>'.
// Type 'unknown[]' provides no match for the signature '(a: data[]): string[]'.
//=============================================== =========================
@shanonjackson 想看看 curry 函数。我几乎可以肯定,这不可能是任何问题的根源,因为上述每个函数的类型都应用于 curry 函数的结果,而不是让它们通过,这似乎是不可能的。
再一次,下面的内容应该没必要复习了,这只是curry
的标准实现:
function isFunction (fn)
return typeof fn === 'function'
const CURRY_SYMB = '@@curried'
function applyCurry (fn, arg)
if (!isFunction(fn))
return fn
return fn.length > 1
? fn.bind(null, arg)
: fn.call(null, arg)
export function curry (fn)
if (fn[CURRY_SYMB])
return fn
function curried (...xs)
const args = xs.length
? xs
: [undefined]
if (args.length < fn.length)
return curry(Function.bind.apply(fn, [null].concat(args)))
const val =
args.length === fn.length
? fn.apply(null, args)
: args.reduce(applyCurry, fn)
if (isFunction(val))
return curry(val)
return val
Object.defineProperty(curried, CURRY_SYMB,
enumerable: false,
writable: false,
value: true,
)
return curried
【问题讨论】:
【参考方案1】:如果您愿意模拟Promise
-esque API,您可以编写一个首先“提升”您的初始值的函数,然后您可以使用then
链接后续函数调用。
您只需要手动 curry 您的函数或使用库。
我们可以看到 TypeScript 允许对附加到对象的函数进行可能的无限类型。 (另外,我们基本上只是写了一个非常简单的 monad...)
See this compile in the TypeScript playground
interface Composer<T>
value: T,
then<R>(f: (x: T) => R): Composer<R>,
unwrap(): T,
function compose<T>(value: T): Composer<T>
return
value,
then<R>(f: (x: T) => R)
const result = f(this.value);
return compose(result);
,
unwrap()
return this.value;
,
;
//
// Example usage
const map = <T, R>(f: (x: T) => R) => (xs: T[]) => xs.map(f);
const filter = <T,>(f: (x: T) => boolean) => (xs: T[]) => xs.filter(f);
const backpack = [
name: 'apple', coins: 5 ,
name: 'pear' , coins: 10 ,
name: 'sword', coins: 45 ,
];
const result =
compose(backpack)
.then(map(x => ( ...x, coins: x.coins * 2 )))
.then(filter(x => x.coins < 25))
.unwrap();
console.log(result); // "[ name: "apple", coins: 10 , "name": "pear", coins: 20 ]
【讨论】:
感谢您提供有趣的方法。方法链似乎更适合该语言。【参考方案2】:不管我多么希望它不是这样,TypeScript 不是 Haskell(或在此处插入您最喜欢的类型语言)。它的类型系统有很多漏洞,并且它的类型推断算法不能保证产生良好的结果。它是not meant to be provably correct(启用惯用的 JavaScript 更重要)。公平地讲 TypeScript,Haskell 没有子类型,面对子类型的类型推断是more difficult。无论如何,这里的简短答案是:
不要试图依赖上下文类型(从函数输出推断类型) 尝试依赖前向类型推断(从函数输入推断类型) 当类型推断失败时,注释并指定类型。由于 TypeScript 不是 Haskell,我要做的第一件事就是使用 TypeScript 命名约定。另外,因为compose()
的实现和其余的都不是问题,所以我将只使用declare
那些函数并忽略实现。
我们来看看:
type Predicate<A> = (a: A) => boolean;
//====[compose]===============================
declare function compose<B, C>(
f: (x: B) => C
): (<A>(g: (a: A) => B) => (a: A) => C) & (<A>(g: (a: A) => B, a: A) => C); //altered
declare function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B): (a: A) => C;
declare function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B, a: A): C;
//====[filter]=================================
declare function filter<A>(p: Predicate<A>, xs: A[]): A[];
declare function filter<A>(p: Predicate<A>): (xs: A[]) => A[];
//====[mapArr]=================================
declare function mapArr<A, B>(f1: (a: A) => B): (xs: A[]) => B[];
declare function mapArr<A, B>(f1: (a: A) => B, xs: A[]): B[];
//====[prop]===================================
declare function prop<K extends keyof any>(
propName: K
): <O extends Record<K, any>>(source: O) => O[K]; // altered
declare function prop<K extends keyof O, O>(propName: K, source: O): O[K];
其中大部分只是直接重写,但我想提请您注意我所做的两个实质性更改。 compose()
的第一个重载签名已从
declare function compose<A, B, C>(
f: (x: B) => C
): ((g: (a: A) => B) => (a: A) => C) & ((g: (a: A) => B, a: A) => C);
到
declare function compose<B, C>(
f: (x: B) => C
): (<A>(g: (a: A) => B) => (a: A) => C) & (<A>(g: (a: A) => B, a: A) => C);
也就是说,compose()
在A
、B
和C
中不是泛型的,而是在B
和C
中是泛型的,并返回A
中的泛型函数。我这样做是因为在 TypeScript 中,泛型函数类型参数的推断通常基于函数传入参数的类型,而不是函数的预期返回类型。是的,有contextual typing 可以从所需的输出类型推断输入类型,但它不如正常的“及时”推断可靠。
当你在A
中调用一个泛型函数,而它的所有参数都没有用作A
的推理站点时,可能会发生什么? (例如,当它只有一个(x: B) => C
类型的参数时)编译器将推断unknown
(as of TS3.5) 为A
,你稍后会不高兴。通过将通用参数规范推迟到调用返回的函数,我们更有可能按照您的意图推断A
。
同样,我将prop()
的第一个重载从
declare function prop<K extends keyof O, O>(
propName: K,
): (source: O) => O[K];
到
declare function prop<K extends keyof any>(
propName: K
): <O extends Record<K, any>>(source: O) => O[K];
这也有同样的问题...调用prop("id")
会导致K
被推断为"id"
,而O
可能会被推断为unknown
,然后因为@987654356 @ 不知道是keyof unknown
(即never
)的一部分,你会得到一个错误。这很可能是导致您看到的错误的原因。
无论如何,我已经将O
的规范推迟到调用返回函数的时间。这意味着我需要将通用约束从 K extends keyof O
反转为 O extends Record<K, any>
... 说类似的事情,但方向相反。
好的,如果我们尝试您的compose()
测试会发生什么?
//====[typescript test]===================================
interface Data
relationId: string;
id: string;
type isMine = Predicate<Data>;
const isMine: isMine = x => x.relationId === "1";
const testFnWithFaultyContextualTyping: (a: Data[]) => string[] = compose(
mapArr(prop("id")), // error!
filter(isMine)
);
// Argument of type '<O extends Record<"id", any>>(source: O) => O["id"]'
// is not assignable to parameter of type '(a: unknown) => any'.
糟糕,那里仍然有错误。这是一个不同的错误,但这里的问题是,通过将返回值注释为(a: Data[]) => string[]
,它触发了一些上下文输入,在正确推断prop("id")
之前逐渐消失。在这种情况下,我的直觉是尽量不要依赖上下文类型,而是看看常规类型推断是否有效:
const testFn = compose(
mapArr(prop("id")),
filter(isMine)
); // okay
// const testFn: (a: Data[]) => string[]
所以 有效。实时推理的行为符合预期,testFn
是您期望的类型。
如果我们尝试您的咖喱版本:
const testFnCurriedWithFaultyContextualTyping = compose(mapArr(prop("id")))( // error!
filter(isMine)
);
// Argument of type '<O extends Record<"id", any>>(xs: O[]) => O["id"][]'
// is not assignable to parameter of type '(x: unknown) => any[]'.
是的,我们仍然收到错误消息。这又是一个尝试进行上下文类型的问题......编译器只是不知道如何推断compose()
的参数类型,因为您打算如何调用它的返回函数。在这种情况下,无法通过移动泛型类型来解决它。推理不能在这里发生。相反,我们可以依靠在 compose()
函数调用中显式指定泛型类型参数:
const testFnCurried = compose<Data[], string[]>(mapArr(prop("id")))(
filter(isMine)
); // okay
这行得通。
无论如何,我希望这能给你一些关于如何进行的想法,尽管可能会令人失望。每当我对 TypeScript 的不健全和有限的类型推断能力感到难过时,我都会提醒自己所有类型系统的 neat features 和 make up for it,至少是 in my opinion。
无论如何,祝你好运!
Link to code
【讨论】:
这是一个很好的答案,正是我正在寻找的那种信息,因为我试图确定 TS 的优点是否超过了专门用于 FP 的缺点。在跟进或接受之前,我将花一些时间在今晚和明天晚些时候进行挖掘。谢谢。 反正没有好的选择; Typescript 并没有迅速成为前端的静态类型黄金标准,它已经成为黄金标准。哎呀,甚至 Jest 都转换为 typescript 和 facebook 发明了 Flow。 Typescript 不像 jcalz 所说的那样是 haskell,但您可以足够接近以获得所需的正确推理,并且在某些地方更好的推理,因为条件类型非常强大 @jcalz 所以基本上,将泛型移动到返回的函数中,并且通常将泛型移动到 equals 的声明端,以便在 TS 无法推断的地方手动提供它们。好吧,你搞定了,你教会了我一些东西,所以这就是你的全部。我将在关于惯用 JS 的一件事上稍作推后。 FP 模式在 JS 中是完全 100% 自然的,因此 TS 优先“启用”惯用 JS 的想法是错误的。 JS 是一门美妙而强大的 FP 语言; FP 在 JS 中比 OOP 更惯用。也许有一天 TS 团队会意识到这一点。 @ShanonJackson 有一个很好的选择...... Javascript。答案一直都在。 “有一个很好的替代 javascript 类型的 javascript”这就是我正在阅读的内容。你看起来像一个非常咸的人,基本上从一开始就在打字稿上大便。 Typescript noImplicitAny: false, strict: false 只是带有枚举和命名空间的 Javascript。 Typescript 是 javascript,因为 javascript 是 typescript 的一个子集。如果您对我发现实际上非常好的推理有问题,那么提出一个拉取请求 typescript 是开源的,或者等待它变得更好。【参考方案3】:是的,您甚至可以按照此处的示例在 3.5.1 中编写通用函数;不想偷东西重新发明***,把它们写在这里。
https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/
不幸的是,如果没有看到正在导入的 curry 的定义,很难给出一个原因,但是在涉及泛型函数时 typescript 3.5.1 中的未知是无法推断泛型的标志
【讨论】:
你链接的compose
函数Typescript的3.5公告是向后的,它不是咖喱。它实际上不是compose
,而是二进制pipe
。看到它没有咖喱,它完全没用。很明显,TS 做出了很多承诺。我的问题更多是关于如何将这个特定的承诺变成可用的现实。
咖喱函数在这里不是问题。它是一个 .js 文件,特别是因为没有合理的方法让 TS 携带这些类型。但是,这就是为什么直接将类型提供给 curried 函数的结果的原因。如果您注意到每个函数都将其 curried 类型直接提供给 curry 的结果,而不是通过汇集类型。我将添加 curry 文件以便您查看,但我很确定这不是这里的一个因素。以上是关于Typescript 能做简单的函数组合吗?的主要内容,如果未能解决你的问题,请参考以下文章
是否可以在 TypeScript 注释中组合多种类型的成员?