繁体   English   中英

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

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

我刚刚了解了提升和应用程序,并且在尝试理解这些结构的过程中,我正在尝试实现一个真实的用例。

我有一个惰性列表(数组),这意味着在加载它之前我无法获取项目或其子项的数量。 获取节点并加载它是异步的,与其嵌套的子节点(如果有)相同。

所以如果我有下面的结构:

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

对于每个孩子,我不知道他们是否有孩子,直到我加载节点并检查孩子的数量。

我怎么能用 FP 检查整个列表(不管它如何嵌套):

-通过当时加载和检查每个节点。 当我们找到匹配或用完节点时停止。

或者

- 然后加载所有节点(可能将其嵌套到 Rights() 和 Lefts() 中),展平为单个列表,然后使用谓词按标题折叠项目,如下例所示。

这是我所知道的适用于非嵌套数组中元素的第一次匹配:


[{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)) 

编辑:当前命令式工作代码:

这是一个共享点代码,它将从 globalNav 获取所有嵌套导航节点。 重要的是我想了解如何使用应用程序将其转变为更可取的 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:看...这个想法是学习和理解FP,如果您只想说我不需要更改代码/我让它变得复杂...那真的不是重点问题。

您似乎想要检索嵌套根导航节点的所有子节点:

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
}

Task<A>就是() => Promise<A> ,即返回 Promise 的函数。 这代表了异步的副作用。


你可以实现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通过多个函数传递一个值( pipe(a, ab, bc)等价于bc(ab(a)) )。 让我们看一下这个函数管道(为简洁起见,我省略了一些readonly ):

  1. 获取导航节点的子节点。 我们现在有一个Task<NavigationNode[]>

  2. 这是递归部分。 我们想要得到所有深度的所有孩子,所以我们必须从原始节点中展平每个孩子。

    获取孩子的孩子将是异步的并返回一些Task ,但是孩子的数组已经从第 1 步开始包裹在一个Task中,所以我们使用chain

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

    这类似于在数组上使用flatMap

    T.chain我们有一个NavigationNode[] 您可能会考虑使用RA.map(flattenNavNode)来获取每个节点的子节点,但这会导致Task<NavigationNode[]>[] ( Array<Task<...>> ),我们需要返回一个Task直接来自chain

    T.traverseArray允许我们返回一个Task<NavigationNode[][]>Task<Array<...>> ):

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

    这会并行执行TaskT.traverseArray(f)(xs)类似于Promise.all(xs.map(f)) ),这是 fp-ts 中的默认设置。 T.traverseSeqArray将按顺序遍历数组,这可能不是您想要的。

    这是traverse的一种专门且更高效的变体,来自Traversable

  3. 看起来我们快完成了——我们需要一个Task<NavigationNode[]> ,并且我们有一个Task<NavigationNode[][]> 但是,我们还没有将原始node包含在这个数组中,我们还需要将结果展平。

    首先,我们使用T.mapTask内的NavigationNode[][]上工作。 使用flow ,这是从左到右的函数组合,我们首先RA.flatten数组数组,然后RA.prepend原始节点到扁平数组。


另一种解决方法是使用 (rose) Tree

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

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

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])
      )
  )

然后你可以把树弄平:

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