现代 C++ 函数式编程指南

  • 现代 C++ 函数式编程指南
    • 什么是 柯里化 (Curry)
    • 什么是 部分应用 (Partial Application)
      • 二元函数 (Partial Application)
      • 参数排序 (Partial Application)
        • 应用场景
          • 计算碳衰减周期求年龄
        • 多参数 (Partial Application)
        • 高阶函数 (Partial Application)
    • 结论

现代 C++ 函数式编程指南

函数式编程是一种编程范式,它强调程序的构建是通过应用(applying)和组合函数(composing functions)来实现的。在函数式编程中,程序被视为由函数定义的表达式树,这些函数将一个值映射到另一个值,而不是一系列更新程序运行状态的命令式语句。

https://en.wikipedia.org/wiki/Functional_programming

什么是 柯里化 (Curry)

一种函数,将具有多个参数的函数作为输入并返回仅具有一个参数的函数。

Curry: A function that takes a function with multiple parameters as input and returns a function with exactly one parameter.

让我们首先看一个简单的例子,展示柯里化的基本概念:

#include <print> // C++23

// 柯里化函数模板
template<typename Func, typename... Args>
auto curry(Func func, Args... args) {
    return [=](auto... remainingArgs) {
        return func(args..., remainingArgs...);
    };
}

// 示例一:加法器的柯里化
int add(int a, int b) {
    return a + b;
}

int main() {
    // 使用柯里化创建新的加法函数
    auto curriedAdd = curry(add, 5);  // 固定第一个参数为 5

    // 调用柯里化后的函数
    std::println("{:d}", curriedAdd(3));  // 输出 8 (5 + 3)

    return 0;
}

这个例子中,curry 函数接受一个函数和部分参数,返回一个接受剩余参数的函数。curriedAdd 就是一个将加法函数柯里化后的结果,固定了第一个参数为 5。

什么是 部分应用 (Partial Application)

将函数应用于其某些参数的过程。 部分应用的函数将被返回以供以后使用。 换句话说,一个函数接受一个具有多个参数的函数并返回一个具有较少参数的函数。 部分应用修复(部分应用函数)返回函数内的一个或多个参数,返回函数将其余参数作为参数以完成函数应用。

Partial Application: The process of applying a function to some of its arguments. The partially applied function gets returned for later use. In other words, a function that takes a function with multiple parameters and returns a function with fewer parameters. Partial application fixes (partially applies the function to) one or more arguments inside the returned function, and the returned function takes the remaining parameters as arguments in order to complete the function application.

参考 https://en.wikipedia.org/wiki/Partial_application

注文中 特化 代指 Partial Application 。

二元函数 (Partial Application)

作为 API 创建者,我们经常希望特化功能或预填充某些参数,这可以通过部分应用来实现。

partial_application_scheme

我们提供具体论据的子集,并产生一个较小数量的函数。

我们来看一个具体的例子。

该函数计算扇形的面积。

partial_application_circle_sector

double secArea(double theta, double radius){
    return 0.5*theta*pow(radius,2);
}

让我们专门计算这个函数来计算整圆的面积,我们需要嵌套 lambda 来表达 部分应用(Partial Application)

// papply 是 Partial Application 缩写的形式,p(Partial) apply(Application)
auto papply = [](auto f, auto x) {
        return [=](auto y){
            return f(x,y);
    };
};

为了实现特化,我们只需要传递函数及其第一个参数。

auto op = papply(secArea,2*M_PI); // 固定第一个参数为完整的圆弧长度即完整的圆
auto val = op(3.0); // 计算半径为 3 的圆的面积

partial_application_circle_sector2

在前一种情况下,特化涉及第一个函数参数。

double secArea(double rAngle, double radius);

完整代码如下:

#include <print>    // C++23
#include <numbers>  // C++20

// papply 是 Partial Application 缩写的形式,p(Partial) apply(Application)
auto papply = [](auto f, auto x) {
    return [=](auto y) {
        return f(x, y);
    };
};

// 计算圆弧的面积
double secArea(double theta, double radius) {
    return 0.5 * theta * pow(radius, 2);
}

int main() {
    auto op  = papply(secArea, 2 * std::numbers::pi);  // 固定第一个参数为完整的圆弧长度即完整的圆
    auto val = op(3.0);                                // 计算半径为 3 的圆的面积

    // 使用 std::format 格式化浮点数并保留两位小数
    std::println("{:.2f}", val);  // 输出半径为2的圆面积 28.27

    return 0;
}

然而,我们常常不得不处理参数排序。

参数排序 (Partial Application)

partial_application_pow

double pow (double base, double exponent);

例如 将 pow C 库函数将基数(base)位置参数置换为指数(exponent)位置参数。

我们如何特化 pow 来返回基数(base)的 2 次方?

partial_application_pow2

下面这种特化可以解决我们上面的问题吗?

partial_application_pow3

如果我们特化 base 部分,papply 将返回一个 2 的任意幂函数。

auto op = papply(pow,2); // 2 的任意幂
auto val = op(3); // 2^3 = 8

该结果不是我们想要的。

pow 函数需要对第二个参数进行特化。

double pow (double base, double exponent);

这个问题可以通过参数交换来解决。

auto swapArgs = [] (auto f){
        return [=](auto x, auto y){
            return f(y,x);
    };
};
auto op = papply(swapArgs(pow), 2); // 现在2作为了指数,解决了我们上面的问题。
auto val = op(3); // 3^2 = 9

我们也可以使用特化专用于 pow 的 lambda 来解决。

auto powS = [](auto exponent, auto base){
               return pow(base, exponent);
};
auto op = papply(powS, 2); 
auto val = op(3); // 3^2 = 9

或者使用下面这种更加紧凑的形式。

auto op = papply([](auto exponent, auto base){
                     return pow(base, exponent);}, 2);

auto val = op(3); // 3^2 = 9

另一种选择是使用库函数 std::bind

此解决方案绕过了使用 lambda 表达式。

auto op = std::bind(pow, std::placeholders::_1, 2);
auto val = op(3); // 3^2 = 9
应用场景
计算碳衰减周期求年龄

接下来,让我们看一个关于碳衰减周期求年龄的例子:

含有有机物质的物体的年龄可以通过放射性同位素测年法确定。

partial_application_radioactive2

这是放射性衰变的一般方程

partial_application_radioactive3

double age(double remainingProportion, double halflife){
    return log(remainingProportion)*halflife / -log(2);
}

将半衰期替换为碳C14的半衰期,即5730年。

auto op = papply(swapArgs(age),5730);

问题1. 与活体样本相比,含有 40% C14 的化石有多少年了?

auto val = op(0.4); // 7575 years

完整代码如下:

#include <print> // C++23

// papply 是 Partial Application 缩写的形式,p(Partial) apply(Application)
auto papply = [](auto f, auto x) {
    return [=](auto y) {
        return f(x, y);
    };
};

auto swapArgs = [](auto f) {
    return [=](auto x, auto y) {
        return f(y, x);
    };
};

double age(double remainingProportion, double halflife) {
    return log(remainingProportion) * halflife / -log(2);
}

int main() {
    auto op  = papply(swapArgs(age), 5730);                  // 将半衰期替换为碳C14的半衰期,即5730年。
    auto val = op(0.4);                                      // 计算含有 40% C14 的化石有多少年了?
    std::println("{:d}", static_cast<int>(std::ceil(val)));  // 7575 years
    return 0;
}

与正则表达式相关的函数的特化也非常有用。

让我们专门研究 std::regex_match

std::regex_match 确定正则表达式 re 是否匹配整个字符序列 s。

bool std::regex_match( const std::string& s,
                  const std::regex& re,
                  std::regex_constants::match_flag_type flags =
                  std::regex_constants::match_default);

我们如何特化使用 std::regex_match 来验证电子邮件地址?

我们使用特化的 lambda 来实现所需的参数排序

auto op = papply([](auto re, auto str){
        return std::regex_match(str, re);}, 
        std::regex("(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+"));
auto val1 = op("test@cheungxiongwei.com"); // return 1, i.e. true
auto val2 = op("test@cheungxiongweicom"); // return 0, i.e. false

完整代码:

#include <print>  // C++23
#include <regex>  // C++11

// papply 是 Partial Application 缩写的形式,p(Partial) apply(Application)
auto papply = [](auto f, auto x) {
    return [=](auto y) {
        return f(x, y);
    };
};

int main() {
    // 该例子中使用的特化 lambda 形式进行参数交换
    auto op   = papply([](auto re, auto str) { return std::regex_match(str, re); }, std::regex("(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+"));
    auto val1 = op("test@cheungxiongwei.com");  // return 1, i.e. true
    auto val2 = op("test@cheungxiongweicom");   // return 0, i.e. false

    std::println("{} {}", val1, val2);  // true false
    return 0;
}

让我们继续讨论多参数问题

多参数 (Partial Application)

这是运动物体最终速度的公式。

partial_application_velocity

Note:这个函数有三个参数,而不是前面2个参数的形式

// 计算速度
double velocity(double v0/*初始速度*/, double a/*加速度*/, double t/*加速时间*/){
    return v0 + a*t;
}

我们如何将这个公式特化用于解决自由落体问题?

我们想要专门研究 两个参数 :初始速度和加速度。

我们需要嵌套 lambda 和参数包。

auto papply = [](auto f, auto... args) {
    return [=](auto... rargs) {
        return f(args..., rargs...);
    };
};

多个参数的使用通过参数包来表示 ...

我们将其类似地应用于二元情况

partial_application_velocity2

auto op = papply(velocity, 0.0, 9.81);
auto val = op(4.5/*4.5秒加速时间*/); // returns 44.15 m/s

在这种特定情况下,不需要交换。

完整代码:

#include <print>  // C++23

// papply 是 Partial Application 缩写的形式,p(Partial) apply(Application)
auto papply = [](auto f, auto... args) {
    return [=](auto... rargs) {
        return f(args..., rargs...);
    };
};

double velocity(double v0 /*初始速度*/, double a /*加速度*/, double t /*加速时间*/) {
    return v0 + a * t;
}

int main() {
    auto op  = papply(velocity, 0.0, 9.81);
    auto val = op(4.5 /*4.5秒加速时间*/);  // returns 44.15 m/s

    std::println("{:.2f} m/s", val);  // 44.15 m/s
    return 0;
}
高阶函数 (Partial Application)

如何特化高阶函数?

此函数对集合执行左折叠

auto leftFold = [](auto col, auto op, auto init) {
    return std::accumulate(std::begin(col), std::end(col), 
           init, op);
};

函数 leftFold 使用二元运算 op 从值 init 开始组合集合 col 的元素。

  1. 使用 leftFold 特化执行求和
auto op = papply([](auto op, auto init, auto col){
                    return leftFold(col, op, init);},
                    std::plus<>(), 0.0);

该函数计算从值 0.0 开始的集合元素的总和。

  1. 使用 leftFold 特化计算集合的乘积
auto op = papply([](auto op, auto init, auto col){
                    return leftFold(col, op, init);},
                    std::multiplies<>(),1.0);

完整代码:

#include <print>    // C++23
#include <numeric>  // C++20
#include <vector>

auto papply = [](auto f, auto... args) {
    return [=](auto... rargs) {
        return f(args..., rargs...);
    };
};

auto leftFold = [](auto col, auto op, auto init) {
    return std::accumulate(std::begin(col), std::end(col), init, op);
};

int main() {
    auto op_plus       = papply([](auto op, auto init, auto col) { return leftFold(col, op, init); }, std::plus<>(), 0.0);
    auto op_multiplies = papply([](auto op, auto init, auto col) { return leftFold(col, op, init); }, std::multiplies<>(), 1.0);

    std::vector<int> set = {1, 2, 3, 4, 5};

    auto val1 = op_plus(set);        // 1 + 2 + 3 + 4 + 5 = 15
    auto val2 = op_multiplies(set);  // 1 * 2 * 3 * 4 * 5 = 120

    std::println("{:d} {:d}", static_cast<int>(val1), static_cast<int>(val2)); // 15 120
    return 0;
}

结论

柯里化(Curry) 和 (部分应用)Partial Application 作为函数式编程的重要概念,通过现代 C++ 中的函数对象和 lambda 表达式实现,为代码的模块化和灵活性提供了更多可能性。通过固定部分参数,生成新的函数,柯里化让函数处理变得更加高效、灵活和可复用。

在 C++ 中,柯里化(Curry) 和 (部分应用)Partial Application为处理函数式编程提供了一种强大的工具,可以应对各种复杂的场景,提高代码的可读性和可维护性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Web前端 -----【Vue】(vue组件基础)一文带你了解组件的创建、注册、使用(包括组件的嵌套)

目录 前言 什么是组件 为什么使用组件化开发 组件的使用 组件的使用分为三个步骤 创建组件 为什么配置项中的data不能使用直接对象的形式&#xff0c;必须使用function&#xff08;重点&#xff01;&#xff01;&#xff01;面试喜欢问&#xff09; 注册组件 使用组件 …

【Element】el-switch开关 点击弹窗确认框时状态先改变----点击弹窗取消框失效

一、背景 需求&#xff1a;在列表中添加定期出账的开关按钮&#xff0c;点击开关时&#xff0c;原来的状态不改变&#xff0c;弹出弹窗&#xff1b;点击弹窗取消按钮&#xff1a;状态不改变&#xff0c;点击弹窗确定按钮&#xff1a;状态改变&#xff0c;并调取列表数据刷新页…

JavaWeb学习(未完结)

文章目录 一、基本概念1.1 动态Web网站简介1.2 web应用程序1.3 静态web1.4 动态web 二、web服务器2.1 技术2.2 应用服务器2.3 安装 jdk8 三、Tomcat3.1 安装 Tomcat93.2 文件说明3.3 启动并使用Tomcat3.4 关闭Tomcat3.5 可能遇到的问题3.6 配置3.6.1 修改测试访问的网页地址3.6…

水淹七军(递归,又是递归)

北大2023级最强新生问我的&#xff0c;最后他的问题说是重写了一遍就解决了 乐死了&#xff0c;有的时候根本看不出源代码漏了哪里 我的思路是&#xff1a; 一个数组记录本次放水所经过的格子&#xff0c;经过的不再递归 一个数组记录地图上各地点的高度 一个数组记录地图…

力扣日记11.25-【二叉树篇】对称二叉树

力扣日记&#xff1a;【二叉树篇】对称二叉树 日期&#xff1a;2023.11.25 参考&#xff1a;代码随想录、力扣 101. 对称二叉树 题目描述 难度&#xff1a;简单 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,…

操作无法完成错误0x0000709的解决办法,解决0x0000709错误

操作无法完成错误0x0000709是一种常见的Windows错误。这篇文章将带大家了解错误的原因&#xff0c;并提供一些解决该问题的方法&#xff0c;希望能够帮助大家解决0x0000709错误问题。 操作系统错误是我们在使用电脑时经常遇到的问题之一。其中之一就是操作无法完成错误0x000070…

Redis-主从与哨兵架构

Jedis使用 Jedis连接代码示例&#xff1a; 1、引入依赖 <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version> </dependency> 2、访问代码 public class JedisSingleTe…

超详细的Python+requests+unittest+excel接口自动化测试框架教程

一、框架结构 工程目录 在这我也准备了一份软件测试视频教程&#xff08;含接口、自动化、性能等&#xff09;&#xff0c;需要的可以直接在下方观看&#xff0c;或者直接关注VX公众号&#xff1a;互联网杂货铺&#xff0c;免费领取 软件测试视频教程观看处&#xff1a; 软件测…

ArcGis如何用点连线?

这里指的是根据已有坐标点手动连线&#xff0c;类似于mapgis中的“用点连线”&#xff0c;线的每个拐点是可以自动捕捉到坐标点的&#xff0c;比直接画精确。 我也相信这么强大的软件一定可以实现类似于比我的软件上坐标时自动生成的线&#xff0c;但是目前我还没接触到那里&a…

lv11 嵌入式开发 GPIO实验 9

目录 1 简介 1.1 GPIO 2 LED实验步骤 2.1 通过电路原理图分析LED的控制逻辑 2.2 通过电路原理图查找LED与Exynos4412的连接关系 2.3 通过数据手册分析GPIO中哪些寄存器可以控制LED 2.4 通过程序去操控对应的寄存器完成对LED的控制 2.4.1 使用寄存器写入…

SpringBoot:邮件发送

官网文档&#xff1a;39. Sending Email (spring.io)。 Sending Email Spring框架提供了JavaMailSender实例&#xff0c;用于发送邮件。 如果SpringBoot项目中包含了相关的启动器&#xff0c;那么就会自动装配一个Bean实例到项目中。 在SpringBoot项目中引入如下Email启动器&a…

十大排序之计数排序、桶排序、基数排序(详解)

文章目录 &#x1f412;个人主页&#x1f3c5;算法思维框架&#x1f4d6;前言&#xff1a; &#x1f380;计数排序 时间复杂度O(nk)&#x1f387;1. 算法步骤思想&#x1f387;2.动画实现&#x1f387; 3.代码实现 &#x1f380;桶排序&#x1f387;1. 算法步骤思想&#x1f38…

activiti流程变量操作api

文章目录 runtimeServicetaskServicedelegateTask测试绘制流程图启动流程runtimeService&taskService查询变量runtimeService&taskService设置变量 runtimeService // ## runtimeService操作的都是executionId runtimeService.startProcessInstanceByKey(processDefin…

ACL权限

ACL权限 目录&#xff1a; 1. 什么是ACL 2. 操作步骤 1. 什么是ACL ACL是Access Control List的缩写&#xff0c;即访问控制列表 每个项目成员在有一个自己的项目目录&#xff0c;对自己的目录有完全权限 项目组中的成员对项目目录也有完全权限 其他人对项目目录没有…

互联网时代的身份标识有哪些?

在互联网时代&#xff0c;我们的在线活动几乎都与IP地址相关。无论是浏览网页、观看视频&#xff0c;还是进行在线交易和沟通交流&#xff0c;我们的设备都会分配到一个独特的IP地址。然而&#xff0c;你可能并未意识到的是&#xff0c;IP地址不仅标识了我们在网络中的身份&…

MySQL 索引相关问题,建议搭建好环境,真实操作一下索引应用到的各种场景

文章目录 什么是 B-tree 和 Btree &#xff1f;B-Tree 和 BTree的区别&#xff1f;MySQL 联合唯一索引是BTree&#xff0c;会带来什么原则&#xff1f;主键索引和单字段唯一索引有什么区别吗什么是 聚簇索引和非聚簇索引 &#xff1f;创建一个三百万数据量的表格&#xff0c;方…

HCIP-七、IS-IS 综合实验

七、IS-IS 综合实验 实验拓扑实验需求及解法1.如图所示&#xff0c;配置所有路由器的接口IP地址。2.运行IS-IS&#xff0c;进程号13.IS-IS优化4.路径优化 实验拓扑 实验需求及解法 本实验模拟IS-IS综合网络&#xff0c;完成以下需求&#xff1a; 1.如图所示&#xff0c;配置所…

Acrel-2000电力监控系统在上海大世界保护修缮工程项目中的应用

摘要&#xff1a;安科瑞生产厂家1876150/-6237黄安南 介绍上海大世界电力监控系统&#xff0c;采用智能电力仪表采集配电现场的各种电参量和开关信号。系统采用现场就地组网的方式&#xff0c;组网后通过现场总线通讯并远传至后台&#xff0c;通过Acrel-2000型电力监控系统实现…

Matplotlib图形配置与样式表_Python数据分析与可视化

Matplotlib图形配置与样式表 配置图形修改默认配置rcParams样式表 Matplotlib的默认图形设置经常被用户诟病。虽然2.0版本已经有了很大改善&#xff0c;但是掌握自定义配置的方法可以让我们打造自己的艺术风格。 配置图形 我们可以通过修个单个图形配置&#xff0c;使得最终图…

搜索引擎语法

演示自定的Google hacking语法&#xff0c;解释含意以及在渗透过程中的作用 Google hacking site&#xff1a;限制搜索范围为某一网站&#xff0c;例如&#xff1a;site:baidu.com &#xff0c;可以搜索baidu.com 的一些子域名。 inurl&#xff1a;限制关键字出现在网址的某…