1

I am starting with functional programming / fp-ts. I am trying to write a function that, taking a list, retains an element if a condition on the next element is fulfilled.

Example:

const condition = (i: number) => i % 10 === 0;
filterNext(condition, [19, 20, 3, 18, 8, 48, 20, 4, 10]) // => [3, 4]

I may also need to extend this so that both the match and the next element are included, as:

const condition = (i: number) => i % 10 === 0;
filterNext(condition, [19, 20, 3, 18, 8, 48, 90, 4, 10]) // => [10, 3, 90, 4, 10]

I have no idea how to build a proper pure function to do so for either of the two.

Any hints would be welcome.

Pierre
  • 6,084
  • 5
  • 32
  • 52
  • If you need look-ahead semantics you usually must resort to recursion. Maybe there is a fancy recursion scheme for that (fold), but I am not aware of one. –  May 04 '20 at 08:35

2 Answers2

1

First, let me show you the code:

import * as O from "fp-ts/lib/Option";
import * as A from "fp-ts/lib/Array";
import { pipe } from "fp-ts/lib/pipeable";

const filterOnPrevious = <T>(condition: (T) => boolean, xs: T[]): T[] =>
  pipe(
    A.tail(xs),
    O.getOrElse<T[]>(() => []),
    tail => A.zip(tail, xs),
    A.filter(([_, x]) => condition(x)),
    A.map(([x]) => x)
  );

Second, you write ‘if a condition on the next element is fulfilled’ but your example shows that condition should pass on the previous one. The code is for your example.

That task has three parts:

  1. Prepare data for filter
  2. Filter
  3. Extract what you need

The magical function is Array.zip which creates pairs from two arrays.

I run it with the original array and a shifted (with the first element removed) array. This creates pairs where the first value in the pair is the original value and the second one is the value on which the condition should be run.

Because the Array.tail returns an Option (returns none if the array is empty), we need to run it trough Option.getOrElse first.

In the last step, after filtering the pairs, I map through them and take the original value from each pair.

While I use a pair, you could use an object or other structure. I like pairs because they are easy to type and work with thanks to array destructuring.

How to change behaviour:

  • filterOnNext: Switch A.zip(tail, xs) to A.zip(xs, tail). Again, the first value is the original, second is the one on which the condition should be run. Alternatively, update the A.filter and A.map so it uses the other value in the pair.
  • Keep both: this is tricky because if e.g. three number in a row fulfil the condition it is not clear what is the desired result. In any way, I'd still recommend to first create a pair with one value being the result if the condition passes on the second value.
Robin Pokorny
  • 10,657
  • 1
  • 24
  • 32
0

Let me know if this works

import {Predicate, Refinement} from 'fp-ts/lib/function'
import {filterWithIndex} from 'fp-ts/lib/Array'
function filterWithNext<A>(predicate: Predicate<A>, xs: Array<A>): Array<A>
function filterWithNext<A, B extends A>(
  predicate: Refinement<A, B>,
  xs: Array<A>
): Array<B> {
  return filterWithIndex<A, B>(
    (i, x): x is B => predicate(x) && i !== xs.length && predicate(xs[i + 1])
  )(xs)
}
Brett M
  • 178
  • 6