简体   繁体   English

使用 FP-TS 或 Ramda 展平可能有或没有子级的嵌套惰性列表?

[英]Flattening a nested Lazy List that may or not have children with FP-TS or Ramda?

I just learned about lift and applicatives and on my quest to trying to understand these structures I am trying to implement a real use case.我刚刚了解了提升和应用程序,并且在尝试理解这些结构的过程中,我正在尝试实现一个真实的用例。

I have a List (array) that is lazy, meaning that I can't get the count of items or their children until I load it.我有一个惰性列表(数组),这意味着在加载它之前我无法获取项目或其子项的数量。 getting the nodes and loading it is async, same for its nested children (if any).获取节点并加载它是异步的,与其嵌套的子节点(如果有)相同。

so if I would have the structure below:所以如果我有下面的结构:

[{title:"test1",children:[]},{title:"test2",children:[{title:"test2_1",children:[]}]}]

For each one of the children I don't know if they have children until I load the node and check the children count.对于每个孩子,我不知道他们是否有孩子,直到我加载节点并检查孩子的数量。

How could I check the entire list with FP (regardless of how nested it can go) either:我怎么能用 FP 检查整个列表(不管它如何嵌套):

-By loading and checking each node at the time. -通过当时加载和检查每个节点。 and stop when we find a match or run out of nodes.当我们找到匹配或用完节点时停止。

or或者

-By loading all nodes then (probably nesting it into Rights() and Lefts()), flattening into a single list, then foldmapping for a item by title with a predicate just like in the example below. - 然后加载所有节点(可能将其嵌套到 Rights() 和 Lefts() 中),展平为单个列表,然后使用谓词按标题折叠项目,如下例所示。

This is what I know works for the first match of a element in a non nested array:这是我所知道的适用于非嵌套数组中元素的第一次匹配:


[{title:"test1"},{title:"test2"},{title:"test3"}] //structure we are loading

const find= (l,f)=>l.foldMap(x=>First(f(x)?Right(x):Left()),First.empty())

const nodes = await getNodes() //not gonna even put in a type just to illustrate that getting and loading the nodes is async. 
const list = List(await load(nodes)) //not gonna even put in a type just to illustrate that getting and loading the nodes is async. 

console.log(find(list,x=>x.title==='test3').fold(x=>x).fold(console.error,x=>x)) 

Edit: current imperative working code:编辑:当前命令式工作代码:

This is a sharepoint code that will get all nested navigation nodes from the globalNav.这是一个共享点代码,它将从 globalNav 获取所有嵌套导航节点。 The important thing is I want to understand how I can turn this into a FP implementation preferable using applicatives.重要的是我想了解如何使用应用程序将其转变为更可取的 FP 实现。

GetNavigationNodeChildren = node => node.get_children();
GetNavigationNodeRoot = spCtx => spCtx.get_web()
  .get_navigation().get_topNavigationBar();
ExecQuery = spCtx => resource => {
  return new Promise((res, rej) => spCtx.executeQueryAsync(
    () => res(resource),
    (s, a) => rej({ s, a }),
  ));
};
LoadResource = spCtx => resource => (spCtx.load(resource) ? resource : resource);
LoadAndExec = spCtx => async resource => {
    LoadResource(spCtx)(resource )
    await ExecQuery(spCtx)(resource )
    return resource
}
getAll = spCtx=> async resource=> {
    return {node:await LoadAndExec(spCtx)(resource),children:await hasChildren(c)(resource.get_children())}
}
hasChildren = spCtx => async resource => {
    LoadResource(spCtx)(resource )
    await ExecQuery(spCtx)(resource )
    return Promise.all(resource.get_count()>0?resource.get_objectData().$1G_0.map(await getAll(spCtx)):[])
}

c=new SP.ClientContext()
root=GetNavigationNodeRoot(c)
await LoadAndExec(c)(root)
all=await hasChildren(c)(root)

PS: Look... The idea is to learn and understand FP, please if all you have to say is that I don't need to change the code/I am making it complicated... That is really not the point of the question. PS:看...这个想法是学习和理解FP,如果您只想说我不需要更改代码/我让它变得复杂...那真的不是重点问题。

It seems like you want to retrieve all the children of a nested root navigation node:您似乎想要检索嵌套根导航节点的所有子节点:

import * as T from 'fp-ts/Task'

// Something like this

interface NavigationNode {
  readonly title: string
}

declare const getNavChildren: (node: NavigationNode) => T.Task<NavigationNode[]>

const flattenNavNode = (node: NavigationNode): T.Task<readonly NavigationNode[]> => {
  // ... do something with getNavChildren to flatten the node
}

A Task<A> is simply () => Promise<A> , that is, a function that returns a Promise. Task<A>就是() => Promise<A> ,即返回 Promise 的函数。 This represents an asynchronous side effect.这代表了异步的副作用。


One way you could implement flattenNavNode is like this:你可以实现flattenNavNode的一种方法是这样的:

import * as RA from 'fp-ts/ReadonlyArray'
import {flow, pipe} from 'fp-ts/function'

const flattenNavNode = (node: NavigationNode): T.Task<readonly NavigationNode[]> =>
  pipe(
    getNavChildren(node),                     // 1
    T.chain(T.traverseArray(flattenNavNode)), // 2
    T.map(flow(RA.flatten, RA.prepend(node))) // 3
  )

pipe pipes a value through multiple functions ( pipe(a, ab, bc) is equivalent to bc(ab(a)) ). pipe通过多个函数传递一个值( pipe(a, ab, bc)等价于bc(ab(a)) )。 Let's go through this function pipeline (I've omitted some readonly s for brevity):让我们看一下这个函数管道(为简洁起见,我省略了一些readonly ):

  1. Get the children of the navigation node.获取导航节点的子节点。 We now have a Task<NavigationNode[]> .我们现在有一个Task<NavigationNode[]>

  2. This is the recursive part.这是递归部分。 We want to get all the children at all depths, so we must flatten out each one of the children from the original node.我们想要得到所有深度的所有孩子,所以我们必须从原始节点中展平每个孩子。

    Getting the children of the children is going be asynchronous and return some Task , but the array of children is wrapped up in a Task already from step 1, so we use chain :获取孩子的孩子将是异步的并返回一些Task ,但是孩子的数组已经从第 1 步开始包裹在一个Task中,所以我们使用chain

     declare const chain: <A, B>(f: (a: A) => Task<B>) => (ma: Task<A>) => Task<B>

    This is analogous to using flatMap on an array.这类似于在数组上使用flatMap

    Inside the T.chain we have a NavigationNode[] .T.chain我们有一个NavigationNode[] You might think of using RA.map(flattenNavNode) to get the children of each node, but that would result in a Task<NavigationNode[]>[] ( Array<Task<...>> ) and we need to return a Task directly from chain .您可能会考虑使用RA.map(flattenNavNode)来获取每个节点的子节点,但这会导致Task<NavigationNode[]>[] ( Array<Task<...>> ),我们需要返回一个Task直接来自chain

    T.traverseArray allows us to return a Task<NavigationNode[][]> ( Task<Array<...>> ) instead: T.traverseArray允许我们返回一个Task<NavigationNode[][]>Task<Array<...>> ):

     declare const traverseArray: <A, B>(f: (a: A) => Task<B>) => (as: readonly A[]) => Task<readonly B[]>

    This executes the Task s in parallel ( T.traverseArray(f)(xs) is analogous to Promise.all(xs.map(f)) ), which is the default in fp-ts.这会并行执行TaskT.traverseArray(f)(xs)类似于Promise.all(xs.map(f)) ),这是 fp-ts 中的默认设置。 T.traverseSeqArray will traverse the array sequentially, which is probably not what you want. T.traverseSeqArray将按顺序遍历数组,这可能不是您想要的。

    This is a specialised and more efficient variant of traverse , which comes from Traversable .这是traverse的一种专门且更高效的变体,来自Traversable

  3. It looks like we're almost done — we need a Task<NavigationNode[]> , and we have a Task<NavigationNode[][]> .看起来我们快完成了——我们需要一个Task<NavigationNode[]> ,并且我们有一个Task<NavigationNode[][]> However, we haven't included the original node in this array, and we also need to flatten the result.但是,我们还没有将原始node包含在这个数组中,我们还需要将结果展平。

    First, we use T.map to work on the NavigationNode[][] inside the Task .首先,我们使用T.mapTask内的NavigationNode[][]上工作。 Using flow , which is left-to-right function composition, we first RA.flatten the array of arrays, and then RA.prepend the original node to the flattened array.使用flow ,这是从左到右的函数组合,我们首先RA.flatten数组数组,然后RA.prepend原始节点到扁平数组。


A different way of going about it is to use a (rose) Tree .另一种解决方法是使用 (rose) Tree

type Forest<A> = Array<Tree<A>>

interface Tree<A> {
  readonly value: A
  readonly forest: Forest<A>
}

fp-ts even comes withunfoldTreeM that allows you to build a tree in the context of a monad: fp-ts 甚至附带了unfoldTreeM ,它允许您在 monad 的上下文中构建树:

import {pipe} from 'fp-ts/function'
import type {Tree} from 'fp-ts/Tree'

const navNodeToTree = (node: NavigationNode): T.Task<Tree<NavigationNode>> =>
  unfoldTreeM(
    T.Monad // use the Task monad
  )(
    node, // root node
    // Type annotations not necessary but I added them for clarity
    (node: NavigationNode): T.Task<[value: A, forest: A[]]> =>
      pipe(
        getChildren(node),
        T.map(children => [node, children])
      )
  )

Then you could flatten the tree:然后你可以把树弄平:

import * as A from 'fp-ts/Array'
import * as RA from 'fp-ts/ReadonlyArray'
import {flow} from 'fp-ts/function'
import type {Forest} from 'fp-ts/Tree'

const flattenForest: <A>(forest: Forest<A>) => readonly A[] = RA.chain(
  ({value, forest}) => [value, ...flattenForest(forest)]
)

// (node: NavigationNode) => T.Task<readonly NavigationNode[]>
const flattenNavNode = flow(
  // make the tree
  navNodeToTree,
  T.map(
    flow(
      // make a forest out of the tree
      // equivalent to x => [x]
      // I’m using the Array instead of the ReadonlyArray module because
      // Forest is defined as a mutable, not readonly, array for some reason
      A.of,
      // flatten the forest
      flattenForest
    )
  )
)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM