在 Ramda 中有条件地针对另一个 array/string 过滤一个数组

Filter an array against another array/string conditionally in Ramda

我正在练习Ramda,尝试构造一个函数如下:

该函数有两个参数:

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

目标:

例如: 如果目标是:

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

查询为:

["pen", "paper"]

那么过滤后的结果应该是:

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

我以 normal/vanilla(?) js 的方式实现了目标。但这不一定是 FP,也不是利用 Ramda。

到目前为止我的实验是这样的:

在代码方面,我正在考虑使用 mapapplystartsWith 函数应用于目标数组的每个元素。到目前为止我只做了这个:

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

但是,我被柯里化函数的组合困在这里。

如有任何帮助,我们将不胜感激!

如果 query 不是数组,将其转换为数组(参见 convertToArray)。映射 query,并使用 R.startsWith 创建一组测试。过滤 target,并使用 R.anyPass 作为谓词:

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>

如果您需要处理非字符串值,我会 return false 用于每个不是字符串的值。请注意,使用 R.toString 转换字符串会转换字符串 - R.toString('abc'); //=> '"abc"' (请参阅 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>

我会像这样组合 startsWithanyPass

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>

如果你想一次性传递参数,你可以把它包起来 uncurry:

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

textStartsWith (query, target)

我认为这确实指出了 Ramda 中缺少的功能。 Ramda 具有可变 composepipe 函数,以及柯里化的二进制组合 o。但是没有等效的咖喱二进制 pipe.

如果你阅读Haskell

实现这种实现的一种可能方法是创建一个完全柯里化的函数,然后将 Haskell 等价物粘贴到 http://pointfree.io.

所以如果我们从这个函数开始:

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

我们可以制作这样的 Haskell 版本:

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

然后 return 是这样的:

filter . (. string) . anyPass . map startsWith

我们可以像上面的第一个答案一样将其转换回 JS,注意 foo . barfoobar 的组合,而 (. foo) 相当于flip (o) (foo)o (__, foo)

我们可以得到类似上面第一个片段的结果。

更新

用户 Kuncheria 询问了 flip (o) (String)。浏览一下签名也许会有所帮助。我们将四个函数传递给 pipe.

map (startsWith) 具有签名 [String] -> [(String -> Boolean)]。它需要一个字符串列表和 return 一个从字符串到布尔的函数列表。

anyPass 具有签名 [(a -> Boolean)] -> (a -> Boolean)。它需要一些任意类型的函数列表,aBoolean 和 returns 一个从 aBoolean 的函数(这将是 true 恰好当这些​​函数中的至少一个 return 对于所提供的 a 为真。)

现在我们可以将 map (startsWith) 的输出([(String -> Boolean)]anyPass 的输入结合起来,方法是用 String 代替 a,如此 pipe (map (startsWith), anyPass)) 具有签名 [String] -> (String -> Boolean).

flip (o) (String)是这里最复杂的一个函数,我们下面会进行说明。在那里我们会发现它的类型是 (String -> c) -> (a -> c).

现在用 Boolean 代替 c,我们结合上面的内容可以看到 pipe (map (startsWith), anyPass, flip (o) (String)) 具有签名 [String] -> (a -> Boolean).

filter 只是具有签名 (a -> Boolean) -> [a] -> [a]。它接受一个函数,该函数将 a 类型的值转换为布尔值,returns 是一个接受 a 类型值列表和 returns 过滤列表的函数函数 returns true.

所以结合上面的内容,我们可以注意到我们的主要功能 - pipe (map (startsWith), anyPass, flip (o) (String), filter) - 具有签名 [String] -> [a] -> [a]

我们可以像这样更紧凑地写上面的讨论:

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

但我们还需要讨论flip (o) (String)

o 是柯里化二进制 compose 函数,其签名是

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

我们可以flip它,得到:

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

现在我们运行进入一个符号问题。我们一直使用 String 来表示 String 类型。但是在 JS 中,String 也是一个函数:从任何值构造一个 String。我们可以认为它是从某种类型 a 到 String 的函数,即类型 a -> String。所以,因为

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

我们可以看到:

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

我们可以将 flip (o) (String) 视为一个函数,它接受一个将字符串转换为类型 c 的函数,而 return 是一个将类型转换为 [=40= 的函数] 变成 c 类型的东西。一个例子是 length,该函数获取字符串的长度:

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