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()ABC 中不是泛型的,而是在BC 中是泛型的,并返回A 中的泛型函数。我这样做是因为在 TypeScript 中,泛型函数类型参数的推断通常基于函数传入参数的类型,而不是函数的预期返回类型。是的,有contextual typing 可以从所需的输出类型推断输入类型,但它不如正常的“及时”推断可靠。

当你在A 中调用一个泛型函数,而它的所有参数都没有用作A 的推理站点时,可能会发生什么? (例如,当它只有一个(x: B) =&gt; 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&lt;K, any&gt;... 说类似的事情,但方向相反。


好的,如果我们尝试您的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[]) =&gt; 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中函数组合方法(链)的类型推断

python能做网站吗

TypeScript 素描 - 装饰器

是否可以在 TypeScript 注释中组合多种类型的成员?

TypeScript:你能根据函数的参数定义一个返回类型结构吗?

来做操吧!深入 TypeScript 高级类型和类型体操