使用 FP-TS 或 Ramda 展平可能具有或不具有 children 的嵌套惰性列表?

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

我刚刚了解了 lift 和 applicatives,在我试图理解这些结构的过程中,我正在尝试实现一个真实的用例。

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

所以如果我有以下结构:

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

对于 children 中的每一个,我不知道它们是否有 children,直到我加载节点并检查 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().G_0.map(await getAll(spCtx)):[])
}

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

PS: 看...这个想法是学习和理解 FP,如果你要说的只是我不需要改变 code/I 让它变得复杂,请... 这真的不是问题的重点

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

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>,也就是说,return是一个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)))。让我们来看看这个函数管道(为简洁起见,我省略了一些 readonlys):

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

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

    获取子项的子项将是异步的并且 return 一些 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<...>>) 我们需要 return 一个 Task 直接来自 chain.

    T.traverseArray 允许我们 return 一个 Task<NavigationNode[][]> (Task<Array<...>>) 而不是:

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

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

    这是来自 Traversable.

    traverse 的一种特殊且更有效的变体
  3. 看起来我们快完成了——我们需要一个 Task<NavigationNode[]>,我们有一个 Task<NavigationNode[][]>。但是,我们并没有把原来的node包含在这个数组中,我们还需要把结果压平

    首先,我们使用T.map来处理Task中的NavigationNode[][]。使用 flow,这是从左到右的函数组合,我们首先 RA.flatten 数组的数组,然后 RA.prepend 原始节点到展平的数组。


另一种方法是使用(玫瑰)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
    )
  )
)