3
votes

I am practising Ramda, and trying to construct a function as follows:

the function takes two args:

userInput: {
  query: Array[String] || String,
  target: Array[String]
}

goal:

  • filter the target array and return a new array meeting certain criteria
  • criteria being the new array should only consist of strings that are starting with the given queries.

For example: if the target being:

["pen", "pencil", "paper", "", undefined, True, "books", "paperback"]

and the query being:

["pen", "paper"]

then the filtered result should be:

["pen", "pencil", "paper", "paperback"]

I have achieved the goal in a normal/vanilla(?) js way. But that was not necessarily FP, nor was it utilising Ramda.

My experiment so far has been like this:

  • iterate the query Array (if it is an array, or just plain string), and check the target against each query (using startsWith from Ramda);
  • return true/false whether the target string meets any of the criteria (using any or anyPass from Ramda);
  • filter the whole array based on the T/F value from the step above;

When it comes to code, I am thinking of using map or apply to apply that startsWith function to each element of the target array. So far I have only done this:

const textStartsWith = curry((query, target) =>
  pipe(toString, startsWith(query))(target)
);

However, I am stuck here with currying the composition of functions.

Any help would be much appreciated!

2

2 Answers

3
votes

I would combine startsWith and anyPass like this:

const textStartsWith = pipe (
  map (startsWith), 
  anyPass,
  flip (o) (String),
  filter
)

console .log (
  textStartsWith 
    (['pen', 'paper']) 
    (['pen', 'pencil', 'paper', '', undefined, true, 'books', 'paperback'])
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script>const {pipe, map, startsWith, anyPass, flip, o, filter} = R </script>

If you want to be able to pass the arguments in one go, you can just wrap this up in uncurry:

const textStartsWith = uncurryN (2) (pipe (
  map (startsWith), 
  anyPass,
  flip (o) (String),
  filter
))

textStartsWith (query, target)

This does point out a missing function in Ramda, I think. Ramda has variadic compose and pipe functions, and a curried binary compose, o. But there's no equivalent curried binary pipe.

If you read Haskell

One possible way to arrive at such an implementation is to make a fully curried function, and then paste a Haskell equivalent into http://pointfree.io.

So if we started with this function:

const f1 = (query) => (target) => filter (pipe (
  String, 
  anyPass (map( startsWith) (query))
)) (target)

We can make a Haskell version like this:

\query -> \target -> filter ((anyPass ((map startsWith) query)) . string) target

which then returns this:

filter . (. string) . anyPass . map startsWith

which we can convert back into JS like the first answer above by noting that foo . bar is the composition of foo and bar and that (. foo) is equivalent to flip (o) (foo) or o (__, foo)

And we can end up with something like the first snippet above.

Update

User Kuncheria asked about flip (o) (String). Perhaps a walk through the signatures might help. We pass four functions to pipe.

map (startsWith) has the signature [String] -> [(String -> Boolean)]. It takes a list of Strings and returns a list of functions from String to Boolean.

anyPass has the signature [(a -> Boolean)] -> (a -> Boolean). It takes a list of functions from some arbitrary type, a to Boolean and returns a single function from an a to Boolean (which will be true exactly when at least one of those functions return true for the a supplied.)

Now we can combine the output of map (startsWith) ([(String -> Boolean)] with the input to anyPass, by substituting String for a, and so pipe (map (startsWith), anyPass)) has the signature [String] -> (String -> Boolean).

flip (o) (String) is the most complex function here, and we'll explain it below. There we'll find out that its type is (String -> c) -> (a -> c).

And now substituting Boolean for c, we combine with the above to to see that pipe (map (startsWith), anyPass, flip (o) (String)) has the signature [String] -> (a -> Boolean).

filter simply has the signature (a -> Boolean) -> [a] -> [a]. It accepts a function that transforms a value of type a into a boolean, and returns a function that takes a list of values of type a and returns the filtered list of those for which the function returns true.

So combining this with the above, we can note that our main function -- pipe (map (startsWith), anyPass, flip (o) (String), filter) -- has the signature [String] -> [a] -> [a]

We might write the above discussion more compactly like this:

const textStartsWith = pipe (
  map (startsWith),    // [String] -> [(String -> Boolean)]
  anyPass,             // [(a -> Boolean)] -> (a -> Boolean)
     // a = String  =>    [String] -> (String -> Boolean)
  flip (o) (String),   // (String -> c) -> (a -> c)
     // c = Boolean =>    [String] -> (a -> Boolean) 
  filter               // (a -> Boolean) -> [a] -> [a]
     //             =>    [String] -> [a] -> [a]
)

But we still need to discuss flip (o) (String).

o is a curried binary compose function, whose signature is

o :: (b -> c) -> (a -> b) -> (a -> c)

We can flip it, to get:

flip (o) :: (a -> b) -> (b -> c) -> (a -> c)

Now we run into a notational problem. We've been using String to denote the String type. But in JS, String is also a function: constructing a String out of any value. We can think of it as the function from some type a to a String, that is with type a -> String. So, since

flip (o) :: (a -> b) -> (b -> c) -> (a -> c)

We can see this:

flip (o) (String)
;            ^----------------- Constructor function
flip (o) (a -> String)
;                 ^------------ Data type
flip (o) (String) :: (String -> c) -> (a -> c)
;            ^           ^----- Data type
;            +----------------- Constructor function

We can think of flip (o) (String) as a function that accepts a function which transforms a String into type c, and returns a function which transforms something of type a into something of type c. An example would be length, the function which takes the length of a string:

const strLength = flip (o) (String) (length)
strLength ('abc')  //=> 3  because String ('abc') = 'abc'
strLength (42)     //=> 2  because String (42) = '42'
strLength (void 0) //=> 9  because String (void 0) = 'undefined'
strLength ({})     //=> 15 because String ({}) = 'object [Object]'
1
votes

If query is not an array, convert it to an array (see convertToArray). Map the query, and create an array of tests using R.startsWith. Filter the target, and use R.anyPass as the predicate:

const { curry, unless, is, of, filter, anyPass, map, startsWith } = R;

const convertToArray = unless(is(Array), of);

const textStartsWith = curry((query, target) =>
  filter(anyPass(map(startsWith, convertToArray(query))))(target)
);

const query = ["pen", "paper"];
const target = ["pen", "pencil", "paper", "", "books", "paperback"];

const result = textStartsWith(query, target);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

If you need to handle non string values, I would return false for every value, which is not a string. Note that convert a string using R.toString would transform the string - R.toString('abc'); //=> '"abc"' (see docs)

const { curry, unless, is, of, filter, ifElse, anyPass, map, startsWith, always } = R;

const convertToArray = unless(is(Array), of);

const textStartsWith = curry((query, target) =>
  filter(ifElse(
    is(String),
    anyPass(map(startsWith, convertToArray(query))),
    always(false)
  ))(target)
);

const query = ["pen", "paper"];
const target = ["pen", "pencil", "paper", "", undefined, true, "books", "paperback"];

const result = textStartsWith(query, target);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>