Reputation: 103
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:
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:
startsWith
from Ramda);any
or anyPass
from Ramda);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!
Upvotes: 3
Views: 884
Reputation: 50807
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
.
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.
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]'
Upvotes: 3
Reputation: 192422
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>
Upvotes: 1