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