F#:啥叫做 map 和 fold 的组合,或者 map 和 reduce 的组合?

Posted

技术标签:

【中文标题】F#:啥叫做 map 和 fold 的组合,或者 map 和 reduce 的组合?【英文标题】:F#: What to call a combination of map and fold, or of map and reduce?F#:什么叫做 map 和 fold 的组合,或者 map 和 reduce 的组合? 【发布时间】:2022-01-05 18:51:11 【问题描述】:

一个简单的例子,灵感来自this question:

module SimpleExample =
    let fooFold projection folder state source =
        source |> List.map projection |> List.fold folder state
    // val fooFold :
    //   projection:('a -> 'b) ->
    //     folder:('c -> 'b -> 'c) -> state:'c -> source:'a list -> 'c

    let fooReduce projection reducer source =
        source |> List.map projection |> List.reduce reducer
    // val fooReduce :
    //   projection:('a -> 'b) -> reducer:('b -> 'b -> 'b) -> source:'a list -> 'b

    let game = [0, 5; 10, 15]
    let minX, maxX = fooReduce fst min game, fooReduce fst max game
    let minY, maxY = fooReduce snd min game, fooReduce snd max game

在本例中,函数 fooFoldfooReduce 的自然名称是什么?唉,mapFoldmapReduce 已经被占用了。

mapFold 是 F# 库的一部分,对输入执行 fold 操作以返回 'result list * 'state 的元组,类似于 scan,但没有初始状态并且需要将元组提供为自己的状态的一部分。它的signature 是:

val mapFold : ('State -> 'T -> 'Result * 'State) -> 'State -> 'T 列表 -> '结果列表 * '状态

由于投影可以很容易地集成到文件夹中,fooFold 函数仅用于说明目的。

还有MapReduce

MapReduce 是一种算法,用于处理特定的大型数据集 使用大量节点的各种可分布问题


现在来看一个更复杂的示例,其中fold/reduce 不直接应用于输入,而是应用于选择键后的分组。 该示例是从 a Python library 借用的,它被称为 - 可能具有误导性 - reduceby

module ComplexExample =
    let fooFold keySelection folder state source =
        source |> Seq.groupBy keySelection 
        |> Seq.map (fun (k, xs) ->
            k, Seq.fold folder state xs) 
    // val fooFold :
    //   keySelection:('a -> 'b) ->
    //     folder:('c -> 'a -> 'c) -> state:'c -> source:seq<'a> -> seq<'b * 'c>
    //     when 'b : equality

    let fooReduce keySelection projection reducer source =
        source |> Seq.groupBy keySelection 
        |> Seq.map (fun (k, xs) ->
            k, xs |> Seq.map projection |> Seq.reduce reducer) 
    // val fooReduce :
    //   keySelection:('a -> 'b) ->
    //     projection:('a -> 'c) ->
    //     reducer:('c -> 'c -> 'c) -> source:seq<'a> -> seq<'b * 'c>
    //     when 'b : equality

    type Project =  name : string; state : string; cost : decimal 
    let projects =
        [  name = "build roads";  state = "CA"; cost = 1000000M 
           name = "fight crime";  state = "IL"; cost = 100000M  
           name = "help farmers"; state = "IL"; cost = 2000000M 
           name = "help farmers"; state = "CA"; cost = 200000M   ]
    fooFold (fun x -> x.state) (fun acc x -> acc + x.cost) 0M projects
    // val it : seq<string * decimal> = seq [("CA", 1200000M); ("IL", 2100000M)]

    fooReduce (fun x -> x.state) (fun x -> x.cost) (+) projects
    // val it : seq<string * decimal> = seq [("CA", 1200000M); ("IL", 2100000M)]

这里的函数fooFoldfooReduce 的自然名称是什么?

【问题讨论】:

【参考方案1】:

我可能会调用前两个mapAndFoldmapAndReduce(尽管我同意mapFoldmapReduce 如果它们还没有被使用的话会是个好名字)。或者,我会选择mapThenFold(等),这可能更明确,但读起来有点麻烦。

对于更复杂的,reduceByfoldBy 听起来不错。问题是,如果您还想要不执行映射操作的那些函数的版本,这将不起作用。如果您需要,您可能需要mapAndFoldBymapAndReduceBy(以及foldByreduceBy)。这有点难看,但恐怕这是你能做的最好的了。

更一般地说,将名称与 Python 进行比较时的问题是 Python 允许重载,而 F# 函数则不允许。这意味着您需要为具有多个重载的函数指定一个唯一的名称。这意味着您只需要提出一个一致的命名方案,不会使名称变得难以忍受。

(我在为 Deedle 库中的函数命名时遇到了这种情况,这在一定程度上受到了 Pandas 的启发。例如,您可以看到 the aggregation functions in Deedle 示例 - 命名中有一个模式来处理事实上,每个函数都需要一个唯一的名称。)

【讨论】:

这很客观地回答了这个问题,尽管我现在倾向于评估重用来自其他功能的名称并没有错,只要它们很好地隐藏在一个模块中,即@ 987654333@/SimpleExample.mapReduce也应该没问题。【参考方案2】:

我和托马斯有不同的看法。

首先;我认为没有重载是一件好事,并且给每个操作唯一的名称也是 好东西。我还要说,给很少使用的函数起长名更为重要 并且不应该避免。

编写更长的名称通常从来都不是问题,因为我们作为程序员通常使用具有自动完成功能的 IDE。 但是阅读和理解是不同的。知道一个函数的作用是因为一个长的描述性名称 比简称好。

一个长的描述性函数名称越少使用函数就越重要。它有助于阅读和 理解代码。很少使用的简短且描述性较差的函数名称会引起混淆。这 如果它只是另一个函数名的重载,那么混乱只会增加。

是的;命名可能很难,这就是为什么它很重要且不应避免的原因。


根据你的描述。我会把它命名为mapFoldmapReduce。正如那些准确描述他们所做的那样。

在 F# 中已经有一个 mapFold,在我看来,F# 开发人员要么在命名、参数或 函数的输出。但无论如何,他们只是搞砸了。

我通常会期望mapFold 执行map,然后执行fold。实际上确实如此,但它也返回中间值 运行时创建的列表。我不希望它回来的东西。我也希望它通过两个 函数而不是一个。

当我们得到 Thomas 建议将其命名为 mapAndFoldmapThenFold 时。然后我会期待不同的行为 对于这两个功能。 mapThenFold 准确地说明了它的作用。 map 然后fold 就可以了。我认为 then 是 不重要。这也是我将其命名为mapFoldmapReduce 的原因。以这种方式编写已经暗示了 then

但是mapAndFoldmapAndReduce 并没有说明执行顺序。它只是说它做了两件事 或者以某种方式返回这个AND那个。

考虑到这一点,我想说 F# 库应该将其命名为 mapFoldmapAndFold,更改返回 只返回折叠的值(并且有两个参数而不是一个)。但是,嘿,它现在搞砸了,我们不能再改变它了。

至于mapReduce,我觉得你有点误会了。 mapReduce 算法就是这样命名的,因为它只是 map,然后是 reduce。就是这样。

但函数式编程及其无状态和更具描述性的操作有时会带来额外的好处。技术上 与 for/fold 相比,map 的功能较弱,因为它仅描述了值的更改方式,而顺序并不重要 或列表中的位置。但是由于这个限制,你可以并行运行它,甚至在一个大型计算机集群上。就这样 您引用的 ma​​pReduce 算法是做什么的。

但这并不意味着mapReduce 必须始终在大型集群上或并行运行其操作。在我看来你可以 只需将其命名为mapReduce 就可以了。每个人都会知道它的作用,我认为没有人期望它会突然运行 集群。

总的来说,我认为 F# 提供的 mapFold 很愚蠢,这里有 4 个我认为应该提供的示例。

let double x = x * 2
let add x y  = x + y

mapFold      double add 0 [1..10] // 110
mapAndFold   double add 0 [1..10] // [2;4;6;8;10;12;14;16;18;20] * 110
mapReduce    double add   [1..10] // Some (110)
mapAndReduce double add   [1..10] // Some ([2;4;6;8;10;12;14;16;18;20] * 110)

mapFold 不是这样工作的,所以你有以下选择。

    按照您的方式实施mapReduce。并忽略与mapFold 的不一致。 提供mapAndReducemapReduce。 让您的mapReduce 返回与mapFold 的默认实现相同的废话,并提供mapThenReduce。 点赞 (3) 并添加mapThenFold

选项 4 对 F# 中已有的内容具有最大的兼容性和期望。但这并不意味着你必须这样做。

在我看来,我会:

    实现mapReduce,返回ma​​p然后reduce的结果。 我不会关心返回列表和结果的 ma​​pAndReduce 版本。 提供一个 mapThenFold 期望两个函数参数返回 fold 的结果。

作为一般通知:仅通过调用map 然后调用reduce 来实现mapReduce 有点毫无意义。我会 期望它有一个更底层的实现,只需遍历一次数据结构即可完成这两件事。 如果没有,我可以拨打map,然后拨打reduce

所以一个实现应该是这样的:

let mapReduce mapper reducer xs =
    let rec loop state xs =
        match xs with
        | []    -> state
        | x::xs -> loop (reducer state (mapper x)) xs
    match xs with
    | []    -> ValueNone
    | [x]   -> ValueSome (mapper x)
    | x::xs -> ValueSome (loop (mapper x) xs)

let double x = x * 2
let add x y  = x + y

let some110 = mapReduce double add [1..10]

【讨论】:

这是一个很好的详细答案 :-) 澄清一下,我并没有完全说没有重载是一件坏事。我只是说在想出在 Deedle 中使用的合理命名方案时,我费了很大的劲,其中许多功能都有很多变体。我现在真的不知道我对重载的看法......我认为如果你正在做“软件工程”,那么有很长的清晰名称会很好。至于数据科学脚本,我认为 Python 方法非常好 - 也许在脚本上下文中,我想要重载...... @TomasPetricek 谢谢,我也在 Perl 工作了很长时间,它在某种程度上支持重载。好吧,大多数时候我们使用带有命名参数和默认值的哈希,这在某种程度上是重载的常用用途。但由于缺乏典型的 IDE 功能,我们经常使用长描述名称。它也更容易记住。当我学习 C# 时,我喜欢像 Perl 一样重载,你必须自己实现调度。但随着 F# 的出现,它又变了。还要考虑重载与类型推断相混淆。恕我直言,重载的弊大于利 @TomasPetricek 作为一个很好的比较,我想说比较 C# LINQ 和 F# Lists 模块。 F# List 具有相同的功能,甚至可能更多,并且根本不使用任何重载。

以上是关于F#:啥叫做 map 和 fold 的组合,或者 map 和 reduce 的组合?的主要内容,如果未能解决你的问题,请参考以下文章

基因芯片或者高通量测序结果中的log2 fold change和q-value,分别表示啥意义?

[Compose] 8. A curated collection of Monoids and their uses

[Compose] 9. Delay Evaluation with LazyBox

[Compose] 21. Apply Natural Transformations in everyday work

Kotlin用高阶函数处理集合数据

panic\nMemory ID是啥意思