Skip to content

Question: Is it possible to do these two operations in "one go"?

Vesa Karvonen edited this page May 21, 2017 · 10 revisions

Is it possible to do these two operations in "one go" (playground)?

const target = {
  elements: [
    {
      time: 1,
      subelements: [1,2,3,4]
    },
    {
      time: 2,
      subelements: [1,2,3,4]
    },
    {
      time: 3,
      subelements: [1,2,3,4]
    }
  ]
}

const filteredByTime =
      L.remove(['elements', L.filter(elem => elem.time < 2)],
               target)
const filteredBySubelement =
      L.remove(['elements', L.elems, 'subelements', L.filter(i => i < 3)],
               filteredByTime)

filteredBySubelement

I mean filtering elements by time and subelements by their value. Without actually calling L.remove twice.

Answer: Yes it is.

Note: This is not directly about the question, but just a note that L.filter is a little bit of a relic. It does have some uses, but often you actually want to use L.elems with L.when.

Here is traversal to do both at once (playground):

L.remove(['elements',
          L.elems,
          L.choose(elem => elem.time < 2
                   ? L.identity
                   : ['subelements', L.elems, L.when(i => i < 3)])],
         target)

Note that traversals have this property that the focuses are non-overlapping. You essentially cannot make a traversal where the focuses would overlap. This is why the above uses L.choose to decide what to do.

We can also write the above traversal using L.choice in a more point-free style (playground):

L.remove(['elements',
          L.elems,
          L.choice(L.when(elem => elem.time < 2),
                   ['subelements', L.elems, L.when(i => i < 3)])],
         target)

Here is a transform to do both (playground):

L.remove(['elements',
          L.seq(L.filter(elem => elem.time < 2),
                [L.elems, 'subelements', L.filter(i => i < 3)])],
         target)

The transform version may appear simpler, because you can just sequence transforms one after another. However, generally speaking traversals are preferable to transforms, because you can only properly use transforms to modify things. IOW, you lose the bidirectionality. You can't fold over transforms in the same way as with traversals. (Well, there are ways to do that, but not as nicely as with traversals.) Traversals also have the nice property that they only go over the data structure once.

Note that in the traversal version you could also replace the traversal L.elems, L.when(i => i < 3) with L.filter(i => i < 3) lens, but I recommend the traversal version. You notice the difference if you try to fold over the elements of the traversal.

Also, note that the traversal version goes over the elements array just once. The transform version goes over it twice.

BTW, I suspect that uses of L.identity may be confusing. A way to think about lenses and traversals is that they declare a path or set of paths (which are possibly conditional) to elements within a data structure. The path specified by L.identity is the empty path or []. So, in the above traversal version, the L.choose expression either chooses to address the element at that point, by returning L.identity (it could also return []), or it specifies a set of paths to things in subelements.

Here is a third approach using transform ops with a traversal (playground):

L.transform(['elements',
             L.elems,
             L.choose(elem => elem.time < 2
                      ? L.removeOp
                      : ['subelements', L.elems, L.when(i => i < 3), L.removeOp])],
            target)

Using transform ops might be a preferred approach when you don't need bidirectionality and need to perform different operations in different parts of a data structure.

You can also use transform ops in transforms that use L.seq (playground):

L.transform(['elements',
             L.elems,
             L.seq([L.when(elem => elem.time < 2), L.removeOp],
                   ['subelements', L.elems, L.when(i => i < 3), L.removeOp])],
            target)

Note that the above use of L.seq is arguably an improvement over the previous use of L.seq in the sense that the elements array is only traversed once.