初探函数式编程---以Map/Reduce/Filter为例

news2025/1/15 21:04:52

函数式编程--酷壳[1] 总结,

函数式编程的三大特性;

  • 数据不可变性

  • 函数作为一等公民(函数可以像变量一样来创建/修改/传递 等)

  • 尾递归优化(重用stack,减轻栈的压力)


函数式编程用到的几个技术:

alt

函数式编程的理念:把函数当成变量来用,关注于描述问题而不是怎么实现(这样可以让代码更易读)




下面详细探讨 Map、Reduce、Filter,这三种操作可以非常方便灵活地对一些数据进行处理,而不是大量使用for循环

(有的也把Reduce称为fold;比较早期且经典的函数式语言有OCaml, Lisp,Haskell等)

其实恰好对应PHP中的array_map()、array_reduce()、array_filter()


Map


如 有这样一个人名的集合["ZhangSan","lisi","WANGWU"],有大写有小写,将其全部转为大写,

Go语言版本

对于传统方式,对切片进行循环,在循环中进行处理即可:


func UpperSli(arr []string) (newArr []string) {
 for _, item := range arr {
  newArr = append(newArr, strings.ToUpper(item))
 }
 return
}

func main() {

 arr := []string{"ZhangSan","lisi","WANGWU"}

 newArr := UpperSli(arr)

 fmt.Printf("%v\n", newArr)
    //[ZHANGSAN LISI WANGWU]

}

“在函数式编程中,不应该用循环迭代的方式,而该用更为高级的方法”

使用函数式编程的写法:


func MapStrUpper(arr []string, fn func(s string) string) []string {
 var newArray []string
 for _, it := range arr {
  newArray = append(newArray, fn(it))
 }
 return newArray
}

func main() {
 var list = []string{"ZhangSan","lisi","WANGWU"}

 x := MapStrUpper(list, func(s string) string {
  return strings.ToUpper(s)
 })

 fmt.Printf("%v\n", x)
  //[ZHANGSAN LISI WANGWU]
}

“这样的代码很易读,因为,代码是在描述要干什么,而不是怎么干”


PHP版本


<?php

$arr = ["ZhangSan""lisi""WANGWU"];
var_export($arr);

$newArr = [];
foreach ($arr as $val) {
    $newVal = strtoupper($val);
    array_push($newArr, $newVal);
}

var_export($newArr);

使用函数式编程的写法:

array_map()  函数可将用户自定义的函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。
可以传递多个数组,回调函数接受的参数数目应该和传递给 array_map() 函数的数组数目一致。

<?php

$arr = ["ZhangSan""lisi""WANGWU"];
var_export($arr);

$newArr = array_map(function ($val1) {
    return strtoupper($val1);
}, $arr);

var_export($newArr);

Rust版本


传统方式,对数组进行循环,在循环中进行处理:

fn main() {
    let arr: [String;3] = ["ZhangSan".to_string(),"lisi".to_string(),"WANGWU".to_string()];
    println!("{:?}",arr);
    
     let  mut new_arr: [String;3] = ["".to_string(),"".to_string(),"".to_string()];
    
    // for i in arr.iter() {
    //     println!("值为 : {}", i);
    // }
    
    for index in 0..3 {
         println!("index is: {} & value is : {}",index,arr[index]);
         new_arr[index] = arr[index].to_ascii_uppercase();
    }
    
    
     println!("{:?}",new_arr);  
    
}

输出:

["ZhangSan""lisi""WANGWU"]
// 值为 : ZhangSan
// 值为 : lisi
// 值为 : WANGWU
index is: 0 & value is : ZhangSan
index is: 1 & value is : lisi
index is: 2 & value is : WANGWU
["ZHANGSAN""LISI""WANGWU"]

麻雀虽小,却涉及到

关于rust:如何打印结构和数组?[2] {:?}

Rust 数组[3]

Rust中的String和&str


函数式编程的方式:

fn main() {
    let arr: [String3] = ["ZhangSan".to_string(), "lisi".to_string(), "WANGWU".to_string()];
    println!("{:?}", arr);
    
    let new_arr: [String3] = arr.iter()
        .map(|s| s.to_ascii_uppercase())
        .collect::<Vec<String>>()
        .try_into()
        .unwrap_or_else(|_| panic!("转换失败"));

    println!("{:?}", new_arr);
}

这段代码主要使用了iter()map()collect()方法

  1. 首先,创建了一个与之前相同的包含三个字符串的数组 arr

  2. 使用 iter() 方法创建一个数组的迭代器。

  3. 使用 map() 方法对迭代器中的每个元素进行转换操作。这里使用了一个闭包 |s| s.to_ascii_uppercase(),它将每个字符串转换为大写形式。

  4. 使用 collect() 方法将转换后的结果收集到一个 Vec<String> 中。

  5. 使用 try_into() 方法将 Vec<String> 转换为 [String; 3] 类型的新数组 new_arr。这里使用了 try_into(),它尝试将 Vec<String> 转换为 [String; 3],如果转换失败则会返回一个错误。

  6. 最后,使用 println!("{:?}", new_arr) 打印新数组 new_arr 的内容。

这种重构后的代码更加函数式和简洁,通过方法链式调用和闭包的组合,实现了对原始数组的转换。




Reduce


map()是将传入的函数依次作用到序列的每个元素,每个元素都是独自被函数“作用”一次; reduce()是将传入的函数作用在序列的第一个元素得到结果后,把这个结果继续与下一个元素作用(累积计算)

reduce()方法是对数组的遍历,返回一个单个返回值

如 有一个数字集合[1,4,7,2,8],计算其和


会把上一次迭代返回的结果存起来,带到下一次迭代中,使用reduce方法可以很容易的计算数组累加,累乘


Go语言版本


package main

import "fmt"

func Reduce(arr []int, fn func(s int) intint {
 sum := 0
 for _, it := range arr {
  sum += fn(it)
 }
 return sum
}

func main() {
 var list = []int{1,4,7,2,8}

 x := Reduce(list, func(s int) int {
  return s
 })
 fmt.Printf("%v\n", x)
 // 22
}

PHP版本


<?php

function sum($carry, $item)
{
    var_dump($carry, $item);
    $carry += $item;
    echo "\n";
    return $carry;
}

$a = array(14728);

$sum = array_reduce($a, 'sum'0);

echo $sum;

输出为:

int(0)
int(1)

int(1)
int(4)

int(5)
int(7)

int(12)
int(2)

int(14)
int(8)

22

更多参考

array_reduce 的理解[4]

[JS中的Array.reduce()方法](https://www.cnblogs.com/steamed-twisted-roll/p/10917405.html "JS中的Array.reduce( "JS中的Array.reduce()方法")方法")


Rust版本


fn reduce<T, F>(arr: &[T], f: F) -> T
where
    T: std::ops::Add<Output = T> + Copy + Default,
    F: Fn(T) -> T,
{
    arr.iter().fold(T::default(), |acc, &item| acc + f(item))
}

fn main() {
    let list = vec![14728];

    let x = reduce(&list, |s| s);

    println!("{:?}", x);
    // 输出:22
}

这段代码实现了一个通用的归约函数 reduce,它接受一个泛型切片 arr 和一个泛型函数 f,并返回一个泛型类型 T

  • reduce 函数使用了泛型类型参数 TFT 代表归约结果的类型,F 代表传入的函数的类型。

  • 在函数签名的 where 子句中,我们对类型参数 T 进行了约束条件:

    • T: std::ops::Add<Output = T>:要求类型 T 实现了 std::ops::Add trait,这允许我们对类型 T 的值进行加法操作,并得到类型 T 的结果。

    • T: Copy:要求类型 T 实现了 Copy trait,这允许我们对类型 T 进行复制操作,以避免所有权转移的问题。

    • T: Default:要求类型 T 实现了 Default trait,这允许我们使用 T::default() 获取类型 T 的默认值。

  • 函数体内部使用 arr.iter().fold() 方法进行归约操作。iter() 方法用于创建切片 arr 的迭代器,fold() 方法接受一个初始值 T::default() 和一个闭包作为参数。闭包中的 acc 是归约过程中的累加器,item 是切片中的每个元素。在闭包中,我们对累加器 acc 和传入闭包函数 f 处理后的元素 f(item) 执行加法操作,并将结果作为新的累加器返回。


main 函数中,我们定义了一个整数切片 list,其中包含了一些整数。

然后,我们调用了 reduce 函数,将整数切片 &list 和一个匿名闭包作为参数传入。这个匿名闭包的功能很简单,它只是返回传入的整数本身。

reduce 函数会对整数切片中的每个元素应用传入的匿名闭包,并将所有元素的结果进行累加。最后,将归约结果打印出来。

在这个例子中,整数切片中的元素分别为 1、4、7、2 和 8,对应的应用函数的结果也分别为 1、4、7、2 和 8。因此,最终的归约结果为 1 + 4 + 7 + 2 + 8 = 22。代码通过调用 println! 打印出结果 22




Filter


Filter 重点在于过滤(而不是新增)某个元素

如 有一个数字集合[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],筛选出哪些是奇数,哪些大于 5


Go 版本

package main

import "fmt"

func Filter(arr []int, fn func(n int) bool) []int {
 var newArray []int
 for _, it := range arr {
  if fn(it) {
   newArray = append(newArray, it)
  }
 }
 return newArray
}

func main() {
 var intset = []int{12345678910}
 out := Filter(intset, func(n int) bool {
  return n%2 == 1
 })
 fmt.Printf("%v\n", out) //[1 3 5 7 9]

 out = Filter(intset, func(n int) bool {
  return n > 5
 })
 fmt.Printf("%v\n", out) //[6 7 8 9 10]

}


这段代码是一个示例程序,展示了在 Go 语言中使用函数式编程风格的过滤功能。

首先,定义了一个名为 Filter 的函数,它接受一个整数切片 arr 和一个函数 fn 作为参数,返回一个新的整数切片。Filter 函数的作用是根据传入的函数 fn 对整数切片 arr 中的元素进行过滤,并返回符合条件的元素组成的新切片。

main 函数中,创建了一个整数切片 intset,其中包含了 1 到 10 的整数。

接下来,通过调用 Filter 函数进行过滤操作。第一次调用 Filter,传入的函数是一个匿名函数 func(n int) bool { return n%2 == 1 },它的作用是判断一个整数是否为奇数。经过过滤,返回的结果是一个新的整数切片,其中包含原始切片中所有奇数值的元素。该结果通过 fmt.Printf 函数打印输出。

第二次调用 Filter,传入的函数是另一个匿名函数 func(n int) bool { return n > 5 },它的作用是判断一个整数是否大于 5。经过过滤,返回的结果是一个新的整数切片,其中包含原始切片中所有大于 5 的元素。同样地,该结果也通过 fmt.Printf 函数打印输出。

以上这段代码展示了如何使用函数作为参数,实现对整数切片的过滤操作,并打印输出过滤后的结果。第一次过滤输出奇数,第二次过滤输出大于 5 的数。


PHP 版本


<?php

$arr = [12345678910];

$newArr = array_filter($arr, function ($val) {
    return $val % 2 == 1;
});
//返回结果
var_export($newArr);


$newArr = array_filter($arr, function ($val) {
    return $val > 5;
});
//返回结果
var_export($newArr);

输出:

array (
  0 => 1,
  2 => 3,
  4 => 5,
  6 => 7,
  8 => 9,
)

array (
  5 => 6,
  6 => 7,
  7 => 8,
  8 => 9,
  9 => 10,
)

Rust 版本


fn main() {
    let intset = vec![12345678910];

    let out: Vec<i32> = filter(&intset, |&n| n % 2 == 1);
    println!("{:?}", out); // [1, 3, 5, 7, 9]

    let out: Vec<i32> = filter(&intset, |&n| n > 5);
    println!("{:?}", out); // [6, 7, 8, 9, 10]
}

fn filter<F>(arr: &[i32], predicate: F) -> Vec<i32>
where
    F: Fn(&i32) -> bool,
{
    arr.iter().cloned().filter(predicate).collect()
}


可以借助 Rust 的函数式编程特性,如闭包和迭代器

以上定义了一个 filter 函数,它接受一个整数切片 arr 和一个闭包 predicate 作为参数,并返回一个符合条件的整数切片。

在 main 函数中,创建了一个整数向量 intset,其中包含了 1 到 10 的整数。

通过调用 filter 函数,传入了一个匿名闭包作为 predicate 参数。这个闭包接受一个整数引用 &n,并返回一个布尔值,表示是否满足过滤条件。

filter 函数通过使用迭代器方法链式调用的方式,对整数切片 arr 进行过滤。首先,使用 iter() 方法创建切片的迭代器,然后使用 cloned() 方法将整数引用转换为整数值的克隆。最后,使用 filter() 方法,传入闭包 predicate 进行过滤操作。

过滤后的结果是一个迭代器,使用 collect() 方法将迭代器的元素收集到一个新的整数向量 Vec 中。

最后,使用 println! 打印出过滤后的结果。

整个重构后的代码保留了函数式编程的风格,使用闭包和迭代器实现了类似的过滤功能。第一次过滤输出奇数,第二次过滤输出大于 5 的数。



array_filter() 重点在于过滤(而不是新增)某个元素,当你处理到一个元素时,返回过滤后的数组

array_map() 重点在于遍历一个数组或多个数组的元素,返回一个新的数组

array_walk() 重点在于遍历数组进行某种操作

array_filter() 和 array_walk()对一个数组进行操作,数组参数在前,函数参数在后

array_map() 可以处理多个数组,因此函数参数在前,数组参数在后,可以根据实际情况放入多个数组参数



参考:

GO编程模式:MAP-REDUCE[5]

robpike/filter[6]

[array_filter()、array_map()、array_walk()三者的用法和区别](https://blog.csdn.net/kelinfeng16/article/details/95243982 "array_filter( "array_filter()、array_map()、array_walk()三者的用法和区别")、array_map()、array_walk()三者的用法和区别")

更多可参见

阮一峰-函数式编程初探[7]

阮一峰-函数式编程入门教程[8]

阮一峰-图解 Monad[9]

参考资料

[1]

函数式编程--酷壳: https://coolshell.cn/articles/10822.html

[2]

关于rust:如何打印结构和数组?: https://www.codenong.com/30253422/

[3]

Rust 数组: https://www.twle.cn/c/yufei/rust/rust-basic-array.html

[4]

array_reduce 的理解: https://www.jianshu.com/p/3c856976813e

[5]

GO编程模式:MAP-REDUCE: https://coolshell.cn/articles/21164.html

[6]

robpike/filter: https://github.com/robpike/filter

[7]

阮一峰-函数式编程初探: https://www.ruanyifeng.com/blog/2012/04/functional_programming.html

[8]

阮一峰-函数式编程入门教程: https://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html

[9]

阮一峰-图解 Monad: https://www.ruanyifeng.com/blog/2015/07/monad.html

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/926890.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

亚马逊电咖啡壶UL1082测试标准

UL1082标准是适用于额定电压为120V,按照国家电气编码进行使用的便携式电咖啡壶&#xff0c;咖啡渗漏壶及其它酿造类器具&#xff0c;除了咖啡壶外&#xff0c;本标准也适用于荼壶、水煲、玻璃水煲、汤保温壶及其它类似器具。这些器具都具有以下特点&#xff1a; &#xff08;1…

Day06-Vue全家桶项目

Day01-Vue全家桶项目 一 全家桶项目介绍 Vue在使用脚手架创建项目的时候,提供前端工程化项目 目前主要学习了Vue基础:Vue指令、Vue组件开发、Vue样式、组件通信、生命周期 全家桶项目是很多技术结合的一种开发模式: 全家桶项目搭建路由搭建(前端路由)网络请求封装Elem…

【Java从0到1学习】11 Java集合框架

1. Collection 1.1 Java类中集合的关系图 1.2 集合类概述 在程序中可以通过数组来保存多个对象&#xff0c;但在某些情况下开发人员无法预先确定需要保存对象的个数&#xff0c;此时数组将不再适用&#xff0c;因为数组的长度不可变。例如&#xff0c;要保存一个学校的学生信…

华为数通方向HCIP-DataCom H12-821题库(单选题:81-100)

第81题 某公司新购入一台网络设备,作为网络管理员,初次配置该设备通常通过什么方式? A、FTP B、Telnet C、SNMP D、Console 口登录 答案: D 解析&#xff1a; 通常情况下&#xff0c;初次配置网络设备会通过Console口登录的方式进行。Console口是一种串口接口&#xff0c…

网络安全工程师岗位一览-徐庆臣(黑客洗白者)

安全服务工程师 安全运维工程师 渗透测试工程师 Web安全工程师 安全攻防工程师 等保测评工程师 …… 代码审计工程师 威胁分析工程师 无线安全工程师 安全研发工程师 移动安全工程师 云计算安全工程师 ……

Socket通信与WebSocket协议

文章目录 目录 文章目录 前言 一、Socket通信 1.1 BIO 1.2 NIO 1.3 AIO 二、WebSocket协议 总结 前言 一、Socket通信 Socket是一种用于网络通信的编程接口&#xff08;API&#xff09;&#xff0c;它提供了一种机制&#xff0c;使不同主机之间可以通过网络进行数据传输和通信…

算法通关村十三关 | 进制转换问题处理模板

1. 七进制数 题目&#xff1a;LeetCode504&#xff1a;504. 七进制数 - 力扣&#xff08;LeetCode&#xff09; 思路 进制转换&#xff0c;对几转换就是对几求余&#xff0c;最后将所有的余数反过来即可、如果num< 0&#xff0c;先取绝对值&#xff0c;再进行操作。 100转7…

Ceres Solver 入门

1. Ceres Solver 是什么 Ceres 可以解决以下形式的边界约束鲁棒化非线性最小二乘问题&#xff1a; 给定初始值&#xff0c;通过优化算法&#xff0c;得到最优解。 其中&#xff0c; f i f_i fi​是CostFunction&#xff0c;也叫误差函数&#xff0c;或者代价函数。 ρ i \rho…

第十七课:利用 Setup Factory 制作 Qt 软件安装包

功能描述&#xff1a;详细介绍如何利用 Setup Factory 制作 Qt 软件安装包&#xff0c;从 Setup Factory 软件下载、安装&#xff0c;到如何利用 Setup Factory 制作软件安装包&#xff0c;手把手教你将 Qt 应用程序制作成具有安装向导的安装包。 一、Setup Factory 简介 Setu…

C语言程序结构、基本语法与数据类型

文章目录 1. 程序结构1.1 Hello World示例1.2 编译并执行C程序 2. 基本语法2.1 C 标记2.2 分号2.3 注释2.4 标识符2.5 关键字2.6 C中的空格 3. 数据类型3.1 整数类型3.2 浮点类型3.3 void类型 1. 程序结构 1.1 Hello World示例 #include <stdio.h>int main() {/* my fi…

DirectExchange直连交换机

目录 一、简介 二、使用步骤 三、demo 父pom文件 pom文件 配置文件 config 消费者 生产者 测试 一、简介 直连型交换机&#xff0c;根据消息携带的路由键将消息投递给对应队列。 大致流程&#xff0c;有一个队列绑定到一个直连交换机上&#xff0c;同时赋予一个路由…

AMEYA360代理品牌:纳芯微芯片解决方案为光伏市场赋能

近年来&#xff0c;光伏市场进入了一个新的增长维度。SolarPower Europe数据显示&#xff0c;2022年全球光伏新增装机量达239GW&#xff0c;占所有可再生能源新增容量的三分之二。国家能源局也宣称&#xff0c;2022年我国工商业光伏新增装机达25.87GW&#xff0c;同比增长236.7…

淘宝商品数据采集(如何快速获取淘宝商品信息),淘宝API接口申请指南

淘宝作为国内的电商平台&#xff0c;拥有海量的商品信息。对于想要进行淘宝商品数据采集的人来说&#xff0c;如何快速获取淘宝商品信息是一个重要的问题。本文将介绍一些快速获取淘宝商品信息的方法。 1. 使用淘宝开放平台PI 淘宝开放平台提供了多种PI接口&#xff0c;可以通…

如何选择合适的开源许可证?

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

java八股文面试[java基础]——异常

自定义异常&#xff1a; 异常Exception 是指程序运行时&#xff0c; 由于输入错误、网络、程序逻辑等原因导致运行时出现的问题。出现异常时&#xff0c;程序会暂时中断执行&#xff0c;并根据产生异常的原因&#xff0c;创建对应异常类型的异常对象&#xff0c;并抛出给JVM捕…

高速收费站的智慧之选,工控机助力顺畅通行!

2020年初取消高速公路省界收费站后&#xff0c;全国高速公路进入“一张网运行、一体化服务”的新阶段。随着ETC用户量快速增长、驾乘人员对收费站高效通行需求不断提升&#xff0c;收费数据在线化运营及精准化、智能化、人性化的收费服务将成为主流。如何提高收费系统集成度、降…

Day4:前端路由(进阶篇)

目标: 持续输出&#xff01;每日分享关于web前端常见知识、面试题、性能优化、新技术等方面的内容。 主要面向群体&#xff1a;前端开发工程师&#xff08;初、中、高级&#xff09;、应届、转行、培训等同学 Day4-今日话题 今天分享的是前端路由的进阶篇&#xff0c;将从路由的…

传感网应用开发1+X实训室建方案

一、概述 1.1建设背景 从院校实际教学情况与人才培养计划为出发点&#xff0c;贯彻传感网应用开发1X实训室职业技能等级标准&#xff0c;充分考虑传感网应用开发1X实训室从业人员的职业发展路径与成长路径&#xff0c;以职业素养、职业技能、知识水平为主要框架结构&#xff…

无涯教程-进程 - 镜像

现在&#xff0c;我们已经了解了如何获取进程及其父进程的基本信息&#xff0c;是时候来研究进程信息的细节了。 以下是进程镜像的图形表示。 进程镜像(Process Image)到底是什么? 进程镜像是执行程序时所需的可执行文件&#xff0c;该镜像通常包含以下部分- 代码段或文本片段…

三维模型数据加载速度不理想?这三种加载方式供你选择!

在四维轻云平台的使用过程中&#xff0c;有用户反映三维模型数据加载速度较慢。因此&#xff0c;平台推出了默认方式、质量优先、速度优先三种数据加载方式供用户选择。下面就来简单介绍一下这三种加载方式的特点&#xff0c;用户可根据需求选择合适的数据加载方式。 默认方式…