代码之家  ›  专栏  ›  技术社区  ›  peoro

一般高阶函数

  •  2
  • peoro  · 技术社区  · 8 年前

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

    该库定义了一些通用于对象、数组、集合、映射和其他类型的函数。

    下面是一个小示例,给出一个想法:

    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种类型中的每一种提供专门的版本,如下所示: 地图 是通用的。 我觉得有必要定义高阶接口,并为现有类型实现它们,但在这方面找不到太多。。。

    有什么提示或想法吗?

    2 回复  |  直到 8 年前
        1
  •  1
  •   Jesse Hallett    8 年前

    2017年11月28日更新: fp-ts 是流动静态土地的继承者。fp ts是同一作者开发的一个较新的库。它支持Flow和Typescript。

    有一个图书馆, flow-static-land ,这与您正在尝试的操作非常相似。通过查看代码并阅读附带的 blog posts

    正如@ftor所提到的,如果您想要能够在一组开放的集合类型上工作的多态函数,那么您需要更高级的类型(HKT)。高级类型是采用类型参数的类型,但其中一个或多个参数未指定。例如,流中的数组采用类型参数来指定数组中元素的类型( 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 .

    获取的完全多态版本 地图 ,flow static land模拟Haskell类型的类(依赖于HKT)。 地图 是类型类的定义特性,称为 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 ,你可以认为是代表 Array 或 Map 或任何可以实现 地图 . 表达式如下 HKT<F, A> 可以被认为是 F<A> 更高级的类型 F 已应用于类型参数 A . (我在这里挥手。)- F 实际上是一个类型级标记。但简化的视图在某种程度上起作用。)

    您可以创建 函子 适用于任何类型。但有一个陷阱:你需要根据 香港电讯 因此,它可以作为一种更高级的类型。模块中的流动静态区域 Arr.js 我们看到了数组类型的更高版本:

    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> inj 和 prj 是类型级转换器-在运行时,这两个函数都只返回其输入,因此它们很可能由JIT内联。两者在运行时没有区别 Arr<A> 和 阵列(<);A> .

    A. 函子 实施 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.js 模块是一个 Arr公司 实施 函子 , 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)
    

    computeTotal 需要申请 地图 和 reduce 它的输入。不是将输入约束到给定的集合类型, 计算机总数 使用其第一个参数将其输入约束为同时实现这两个参数的类型 可折叠的 函子 : f: Foldable<F> & Functor<F> . 在类型级别,参数 f 充当“见证人”,以证明给定的集合类型实现了 地图 和 减少 . 在运行时 f 提供对特定实现的参考 地图 和 减少 待使用。在多态代码的入口点(其中 计算机总数 可折叠的 & 函子 实现作为参数给出 Arr公司 . 因为Javascript不是为类型类设计的 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)
    

    chain 是流静态土地的版本 flatMap . 对于集合中的每个元素, 链条 运行回调,该回调必须生成相同类型的集合(但可以包含不同的值类型)。这有效地产生了一个集合。 链条 然后为您将其展平到单个级别。所以 链条 基本上是 和 flatten .

    我包括在内 fromArray 因为给的回调 链条 必须返回与 allItems公司 接受并返回-返回 大堆 从 链条 Pointed 中的约束 fromArray 获得 of 函数,它将单个值放入适当类型的集合中。 指出 不会出现在的约束中 allItems公司 因为 有一个 Monad 约束,以及 单子 实施也是 指出 , Chain , 函子 ,以及其他一些。

    我个人是流静之地的粉丝。HKT的功能风格和使用使得代码的类型安全性优于使用面向对象风格的鸭子类型。但也有缺点。当使用类型联合时,来自流的错误消息可能变得非常冗长,例如 Foldable<F> & Functor<F> . 而且代码风格需要额外的训练——对于不熟悉Haskell的程序员来说,这似乎太奇怪了。

        2
  •  1
  •   Jesse Hallett    8 年前

    我想接着给出另一个与你实际提出的问题相匹配的答案。Flow可以做你想做的事。但是,在实现对这四种集合类型都进行操作的函数时,确实会遇到一些麻烦,因为在 Map 键的类型是完全泛型的,但对于 Array 密钥类型必须为 number ,并且由于对象在Javascript中的实现方式 Object 始终有效 string . ( Set 没有键,但这并不重要,因为不需要使用键在 设置 .) 最安全的工作方式 和 对象 特殊情况是为每个函数提供重载类型签名。但事实证明,很难说出这一点 key 可能是完全泛型类型 K 或 或 数字 取决于 obj . 最实用的选择是使每个函数在键类型中完全通用。但是您必须记住,如果您试图使用错误键类型的数组或普通对象,这些函数将失败,在这些情况下,您不会得到类型错误。

    type MyIterable<K, V> = Map<K, V> | Set<V> | Array<V> | Pojo<V>
    type Pojo<V> = { [key: string]: V } // plain object
    

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

    Flow有一个简便的技巧:你可以放置关键字 %checks 在返回 boolean ,流将能够在类型检查时使用该函数的调用 type refinements . 但是函数体必须使用Flow知道如何用于类型细化的构造,因为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 :Flow知道当分配给数组索引时,索引变量应该是一个数字,并且它还知道 K 可能不是 数字 . 同样的情况也适用于指定普通对象属性,因为 Pojo 类型别名指定 一串 钥匙。因此,在这些情况下,您需要在代码分支中键入cast 钥匙 到 any 钥匙 .

    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 函数只需要一个类型签名。需要记住的一点是 实例化Metype 构建结果 map ,并且当使用时,集合中的值类型可以在输入和输出之间更改 地图 . 因此,在输入和输出中使用两个不同类型的变量作为值类型很重要 实例化Metype 也您也可以允许 实例化Metype 更改键类型;但这不是必须的 地图

    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 {};        }
    }
    

    这意味着 实例化Metype 可以保存任何值。它可能与输入集合中的值的类型相同,也可能不同。

    在您实施 forEach 您检查是否存在 obj.forEach 作为类型细化。这让Flow感到困惑,因为构成Flow的 MyIterable 是一个普通的Javascript对象,可以包含任何字符串键。Flow不能假设 obj。forEach公司 将是falsy。所以你需要使用不同的支票。重新使用 isArray 等。谓词工作良好:

    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 看起来像这样(从 core.js ):

    declare class Object {
        /* ... */
        static entries(object: any): Array<[string, mixed]>;
        /* ... */
    }
    

    流假设 将 mixed ,但该类型应为 V . 解决方法是通过循环中的对象属性访问获取值。

    的类型 钥匙 给定回调的参数应为 K ,但Flow知道,在数组情况下,该类型实际上是 数字 ,在普通对象的情况下 一串 . 修复这些情况需要再进行几个类型转换。

    最后 地图 :

    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 . 这是因为 地图 可能会更改值的类型。我设置了一个类型变量 M obj公司 与输入集合的类型相同。这允许您在回调中使用特定于调用时提供的特定集合类型的函数 地图 .