通用的高阶函数

时间:2017-11-14 06:47:06

标签: javascript types flowtype

我正在尝试将Flow类型信息添加到我的小型库中。

该库定义了一些在Object,Array,Set,Map和其他类型上通用的函数。

这是一个小小的例子来提出一个想法:

function set( obj, key, value ) {
  if( isMap(obj) )      { obj.set(key, value); }
  else if( isSet(obj) ) { obj.add(value);      }
  else                  { obj[key] = value;    }
}
function instantiateSameType( obj ) {
  if( isArray(obj) )    { return [];        }
  else if( isMap(obj) ) { return new Map(); }
  else if( isSet(obj) ) { return new Set(); }
  else                  { return {};        }
}
function forEach( obj, fn ) {
  if( obj.forEach ) obj.forEach(    ( value, key )=>fn(value, key, obj) );
  else Object.entries(obj).forEach( ([key, value])=>fn(value, key, obj) );
}
function map( obj, fn ) {
  const result = instantiateSameType( obj );
  forEach(obj, (value, key)=>{
    set( result, key, fn(value, key, this) );
  });
  return result;
}

如何定义map的类型?

我想避免为示例中列出的4种类型中的每种类型提供专门版本,因为map对它们是通用的。 我觉得有必要定义更高阶的接口,并为现有类型实现它们,但是找不到任何关于它的信息......

任何提示或想法?

2 个答案:

答案 0 :(得分:1)

2017-11-28更新: fp-ts是flow-static-land的继承者。 fp-ts是同一作者的新库。它支持Flow和Typescript。

有一个库flow-static-land,它可以执行与您尝试的内容非常相似的内容。你可以通过查看代码并阅读@gcanti附带的blog posts来学习一些有趣的东西。我将在流动静态土地上扩展战略;但请记住,如果你可以使用一组封闭的可迭代类型,那么你可以在没有高级类型的情况下实现你的迭代函数。

正如@ftor所提到的,如果你想要可以在一组开放的集合类型上工作的多态函数,那么你需要更高级的类型(HKT)。较高类型的类型是采用类型参数的类型,但其中一个或多个参数未指定。例如,Flow中的数组采用类型参数来指定数组中元素的类型(Array<V>),而地图(Map<K, V>)也是如此。有时您希望能够引用参数化类型而不指定其所有类型参数。例如,map应该能够在所有数组或映射上运行,而不管它们的类型参数如何:

function map<K, A, B, M: Array<_> | Map<K, _>>(M<A>, fn: A => B): M<B>

在这种情况下,M是表示更高通道类型的变量。我们可以将M作为第一类传递,并在不同时间使用不同类型填充其类型参数。 Flow本身不支持HKT,因此上面的语法不起作用。但是有可能伪造一些类型别名间接的HKT,这就是flow-static-land的作用。博文中有详细信息Higher kinded types with Flow

要获得map的完全多态版本,flow-static-land模拟Haskell类型类(依赖于HKT)。 map是名为Functor的类型类的定义特征; flow-static-land具有Functor(来自Functor.js)的定义:

export interface Functor<F> {
  map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
}

HKT类型是flow-static-land的解决方法,用于实现更高级别的类型。实际的高等级类型为F,您可以将其视为代表ArrayMap或任何可以实施map的类型。像HKT<F, A>这样的表达式可以被视为F<A>,其中较高级别的F已应用于类型参数A。 (我在这里挥手致意 - F实际上是一个类型级别的标签。但简化视图在某种程度上起作用。)

您可以为任何类型创建Functor的实现。但有一个问题:您需要根据HKT来定义类型,以便它可以用作更高级的类型。在模块Arr.js中的flow-static-land中,我们看到了数组类型的更高版本:

class IsArr {}  // type-level tag, not used at runtime
export type ArrV<A> = Array<A>;  // used internally
export type Arr<A> = HKT<IsArr, A>;  // the HKT-compatible array type

如果您不想在代码中的任何地方使用Arr<A>代替Array<A>,则需要使用inj: (a: Array<A>) => Arr<A>prj: (fa: Arr<A>) => Array<A>进行转换。 injprj是类型级转换器 - 在运行时,这两个函数都只返回它们的输入,因此它们很可能被JIT内联。 Arr<A>Array<A>之间没有运行时差异。

Functor的{​​{1}}实现如下所示:

Arr

事实上,整个const arrFunctor: Functor<IsArr> = { function map<A, B>(f: (a: A) => B, fa: Arr<A>): Arr<B> { const plainArray = prj(f) const mapped = plainArray.map(f) return inj(mapped) } } 模块是Arr.jsArrFunctor以及其他有用类型类的Foldable实现。使用具有多态代码的实现看起来像这样:

Traversable

import * as Arr from 'flow-static-land/lib/Arr' import { type Foldable } from 'flow-static-land/lib/Foldable' import { type Functor } from 'flow-static-land/lib/Functor' import { type HKT } from 'flow-static-land/lib/HKT' type Order = { items: string[], total: number } // this code is polymorphic in that it is agnostic of the collection kind // that is given function computeTotal<F> ( f: Foldable<F> & Functor<F>, orders: HKT<F, Order> ): number { const totals = f.map(order => order.total, orders) return f.reduce((sum, total) => sum + total, 0, totals) } // calling the code with an `Arr<Order>` collection const orders = Arr.inj([{ items: ['foo', 'bar'], total: 23.6 }]) const t = computeTotal(Arr, orders) 需要将computeTotalmap应用于其输入。 reduce使用其第一个参数将其输入约束为同时实现computeTotalFoldableFunctor的类型,而不是将输入约束到给定的集合类型。在类型级别,参数f: Foldable<F> & Functor<F>充当&#34;证人&#34;证明给定的集合类型同时实现了fmap。在运行时reduce提供了对要使用的fmap的特定实现的引用。在多态代码的入口点(使用静态已知的集合类型调用reducecomputeTotal&amp; Foldable实现作为参数Functor给出。因为Javascript不是为类型类设计的,所以必须明确给出Arr的选择;但如果您尝试使用与所使用的集合类型不兼容的实现,Flow至少会抛出错误。

为了解决这个问题,这里是一个多态函数Arr的示例,它接受一个集合,并返回一个相同类型的集合。 allItems不知道它运行的特定集合类型:

allItems

import { type Monad } from 'flow-static-land/lib/Monad' import { type Monoid, concatAll } from 'flow-static-land/lib/Monoid' import { type Pointed } from 'flow-static-land/lib/Pointed' // accepts any collection type that implements `Monad` & `Monoid`, returns // a collection of the same kind but containing `string` values instead of // `Order` values function allItems<F> (f: Monad<F> & Monoid<*>, orders: HKT<F, Order>): HKT<F, string> { return f.chain(order => fromArray(f, order.items), orders) } function fromArray<F, A> (f: Pointed<F> & Monoid<*>, xs: A[]): HKT<F, A> { return concatAll(f, xs.map(f.of)) } // called with an `Arr<Order>` collection const is = allItems(Arr, orders) 是{-1}}的flow-static-land版本。对于集合中的每个元素,chain运行一个必须生成相同类型集合的回调(但它可以包含不同的值类型)。这有效地产生了一系列集合。 flatMap然后将其展平为一个级别。因此chain基本上是chainchain的组合。

我添加了map,因为提供给flatten的回调必须返回fromArray接受并返回的同一类集合 - 从chain返回allItems回调将无法正常工作。我在Array中使用chain约束来获取Pointed函数,该函数将单个值放入适当类型的集合中。 fromArray的约束中没有出现of,因为PointedallItems约束,每个allItems实现也是Monad的实现,MonadPointed和其他一些。

我个人是流动静态土地的粉丝。 HKT的功能风格和使用导致代码具有比面向对象式鸭子打字更好的类型安全性。但也有缺点。使用类似Chain的类型联合时,来自Flow的错误消息可能会变得非常冗长。代码风格需要额外的培训 - 对于不熟悉Haskell的程序员来说,这似乎是非常奇怪的。

答案 1 :(得分:1)

我想跟进另一个与你实际提出的问题相符的答案。 Flow可以做你想要的。但是实现对所有这四种集合类型进行操作的函数确实有点痛苦,因为在Map的情况下,键的类型是完全通用的,但对于Array,键类型必须是{{} 1}},由于在Javascript中实现对象的方式,number的密钥类型始终有效Object。 (string没有密钥,但这并不重要,因为您不需要使用密钥来设置Set中的值。)解决Set的最安全方法和Array特殊情况是为每个函数提供重载类型签名。但事实证明,很难告诉Flow Object可能是完全通用类型keyKstring,具体取决于{{1}的类型}。最实用的选择是使每个函数在密钥类型中完全通用。但是你必须记住,如果你试图使用错误键类型的数组或普通对象,这些函数将会失败,并且在这些情况下你不会得到类型错误。

让我们从您正在使用的集合类型集的类型开始:

number

此时必须列出所有集合类型。如果您想使用一组开放的集合类型,那么请查看我的其他答案。请注意,我的另一个答案是避免解决方案中的类型安全漏洞。

Flow有一个方便的技巧:您可以将关键字obj放在返回type MyIterable<K, V> = Map<K, V> | Set<V> | Array<V> | Pojo<V> type Pojo<V> = { [key: string]: V } // plain object 的函数的类型签名中,并且Flow将能够在类型上使用该函数的调用检查type refinements的时间。但是函数体必须使用Flow知道如何用于类型细化的构造,因为Flow实际上并没有在类型检查时运行函数。例如:

%checks

我提到你需要几个类型转换。一个实例在boolean中:Flow知道在分配数组索引时,索引变量应该是一个数字,并且它还知道function isMap ( obj: any ): boolean %checks { return obj instanceof Map } function isSet ( obj: any ): boolean %checks { return obj instanceof Set } function isArray ( obj: any ): boolean %checks { return obj instanceof Array } 可能不是set。分配给普通对象属性也是如此,因为K类型别名指定了number个键。因此,在这些案例的代码分支中,您需要键入Pojostring,这有效地禁用了对key的使用的类型检查。

any

您的key函数只需要一个类型签名。需要记住的一点是,使用function set<K, V>( obj: MyIterable<K, V>, key: K, value: V ) { if( isMap(obj) ) { obj.set(key, value); } else if( isSet(obj) ) { obj.add(value); } else { obj[(key:any)] = value; } } 构造instantiateSameType的结果,使用instantiateSameType时,集合中的值类型可以在输入和输出之间变化。因此,对map的输入和输出中的值类型使用两个不同的类型变量也很重要。您可能还允许map更改密钥类型;但是,instantiateSameType无法正常工作。

instantiateSameType

这意味着map的输出可以包含任何值。它可能与输入集合中的值相同,也可能不是。

function instantiateSameType<K, A, B>( obj: MyIterable<K, A> ): MyIterable<K, B> { if( isArray(obj) ) { return []; } else if( isMap(obj) ) { return new Map(); } else if( isSet(obj) ) { return new Set(); } else { return {}; } } 的实施中,您检查是否存在instantiateSameType作为类型细化。这会让Flow感到困惑,因为构成forEach的其中一种类型是一个普通的Javascript对象,它可能包含任何字符串键。 Flow不能假设obj.forEach是假的。所以你需要使用不同的支票。重新使用MyIterable等谓词很有效:

obj.forEach

还有两个问题需要指出:isArray的Flow的库定义如下(来自core.js):

function forEach<K, V, M: MyIterable<K, V>>( obj: M, fn: (value: V, key: K, obj: M) => any ) {
  if( isArray(obj) || isMap(obj) || isSet(obj) ) {
    obj.forEach((value, key) => fn(value, (key:any), obj));
  } else {
    for (const key of Object.keys(obj)) {
      fn(obj[key], (key:any), obj)
    }
  }
}

Flow假定Object.entries返回的值类型为declare class Object { /* ... */ static entries(object: any): Array<[string, mixed]>; /* ... */ } ,但该类型应为Object.entries。解决此问题的方法是通过循环中的对象属性访问来获取值。

给定回调的mixed参数的类型应为V,但Flow知道在数组中该类型实际上是key,并且在普通对象的情况下它将是K。修复这些案件需要更多的类型演员。

最后,number

string

我想在此指出的一些事情:输入集合具有类型变量map,而输出集合具有变量function map<K, A, B, M: MyIterable<K, A>>( obj: M, fn: (value: A, key: K, obj: M) => B ): MyIterable<K, B> { const result = instantiateSameType( obj ); forEach(obj, (value, key)=>{ set( result, key, fn(value, key, this) ); }); return result; } 。这是因为A可能会更改值的类型。我为输入集合的类型设置了一个类型变量B;这是为了通知Flow,回调参数map的类型与输入集合的类型相同。这允许您在回调中使用特定于调用M时提供的特定集合类型的函数。