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