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

news2024/11/29 10:31:59
  • 现代 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.coloradmin.cn/o/1253929.html

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

相关文章

Shell脚本:Linux Shell脚本学习指南(第二部分Shell编程)三

第二部分&#xff1a;Shell编程&#xff08;三&#xff09; 二十一、Shell declare和typeset命令&#xff1a;设置变量属性 declare 和 typeset 都是 Shell 内建命令&#xff0c;它们的用法相同&#xff0c;都用来设置变量的属性。不过 typeset 已经被弃用了&#xff0c;建议…

MySql之索引,视图,事务以及存储过程举例详解

一.数据准备 数据准备可参考下面的链接中的数据准备步骤 MySql之内连接&#xff0c;外连接&#xff0c;左连接&#xff0c;右连接以及子查询举例详解-CSDN博客 &#xff08;如有问题可在评论区留言&#xff09; 二.存储过程 1.定义 存储过程 PROCEDURE &#xff0c;也翻译…

Leetcode—167.两数之和 II - 输入有序数组【中等】

2023每日刷题&#xff08;四十一&#xff09; Leetcode—167.两数之和 II - 输入有序数组 实现代码 /*** Note: The returned array must be malloced, assume caller calls free().*/ int* twoSum(int* numbers, int numbersSize, int target, int* returnSize) {*returnSiz…

过渡曲线的构造之平面PH曲线

平面PH曲线的构造及其相应性质 平面PH曲线的构造及其相应性质PH曲线理论三次PH曲线的构造及性质四次PH曲线的构造及性质五次PH曲线的构造及性质非尖点五次PH曲线尖点五次PH曲线 参考文献 平面PH曲线的构造及其相应性质 过渡曲线常需要满足在连接点处位置连续、曲率连续以及切线…

Docker Swarm总结+CI/CD Devops、gitlab、sonarqube以及harbor的安装集成配置(3/4)

博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码下载地址&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb;…

makefile编写练习

makefile编写练习 OVERVIEW makefile编写练习文件结构直接编译整个项目并运行将项目制作成为静态库将项目制作成为动态库 编写makefile文件来编译带头文件的程序&#xff0c; 文件结构 初始项目文件结构&#xff0c;如下所示&#xff1a; #ifndef ADD_HPP #define ADD_HPPint…

栈详解(C语言)

文章目录 写在前面1 栈的定义2 栈的初始化3 数据入栈4 数据出栈5 获取栈顶元素6 获取栈元素个数7 判断栈是否为空8 栈的销毁 写在前面 本片文章详细介绍了另外两种存储逻辑关系为 “一对一” 的数据结构——栈和队列中的栈&#xff0c;并使用C语言实现了数组栈。 栈C语言实现源…

Visual Studio 使用MFC 单文档工程绘制单一颜色直线和绘制渐变颜色的直线(实例分析)

Visual Studio 使用MFC 单文档工程从创建到实现绘制单一颜色直线和绘制渐变颜色的直线 本文主要从零开始创建一个MFC单文档工程然后逐步实现添加按键&#xff08;事件响应函数&#xff09;&#xff0c;最后实现单一颜色直线的绘制与渐变色直线的绘制o(&#xffe3;▽&#xffe…

2、Burp使用

文章目录 一、为Firefox浏览器安装数字证书二、利用Intruder模块进行暴力破解 一、为Firefox浏览器安装数字证书 &#xff08;1&#xff09;利用Firefox浏览器访问http://burp或127.0.0.1:<监听端口>&#xff0c;点击页面右上侧的“CA Certificate”处下载CA证书&#xf…

靡靡之音 天籁之声 ——Adobe Audition

上一期讲到了和Pr配合使用的字幕插件Arctime Pro的相关介绍。相信还记得的小伙伴应该记得我还提到过一个软件叫做Au。 当人们对字幕需求的逐渐满足&#xff0c;我们便开始追求更高层次的享受&#xff0c;当视觉享受在进步&#xff0c;听觉享受想必也不能被落下&#xff01; Au即…

【模板】KMP算法笔记

练习链接&#xff1a;【模板】KMP - 洛谷 题目&#xff1a; 输入 ABABABC ABA 输出 1 3 0 0 1 思路&#xff1a; 根据题意&#xff0c;用到的是KMP算法&#xff0c;KMP算法思想是通过一个一个匹配首字母的原理进行整个匹配效果&#xff0c;当某个首字母不匹配的时候&#x…

【全栈开发】Blitz.js与RedwoodJS

技术的不断发展是必然的。如果你仔细观察这片土地&#xff0c;你会注意到随着技术的成熟而出现的某些模式。特别是&#xff0c;开发人员一直在努力提高性能&#xff0c;简化开发过程&#xff0c;增强开发人员体验。 在本指南中&#xff0c;我们将分析两个帮助全栈应用程序世界…

2023年3月电子学会青少年软件编程 Python编程等级考试一级真题解析(判断题)

2023年3月Python编程等级考试一级真题解析 判断题(共10题,每题2分,共20分) 26、在Python编程中,print的功能是将print()小括号的内容输出到控制台,比如:在Python Shell中输入print(北京,你好)指令,小括号内容可以输出到控制台 答案:错 考点分析:考查python中print…

Proteus仿真--基于PG12864LCD设计的指针式电子钟

本文介绍基于PG12864LCD设计的指针式电子钟&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 本设计中时间芯片选用DS1302芯片&#xff0c;液晶选用PG12864LCD模块&#xff0c;按键K1-K3&#xff0c;K1用于时分选择&#xff0c;K2用于调整功能&#xff0c…

积跬步至千里 || 为循环添加进度条

有时候&#xff0c;我们的Python程序需要运行较长时间&#xff0c;原因是一些环节占用时间太长&#xff08;如for循环&#xff09;。如果我们要观察那些占用时间任务的执行进度&#xff0c;那么有一个进度条能实时的显示任务进度情况的话&#xff0c;将会非常方便。而tqdm库就是…

Docker容器化部署若依微服务ruoyi-cloud项目

系统环境 接下来的内容以 Ubuntu 22.04.1 操作系统为例。 下载安装Docker Ubuntu hihi-IdeaCentre-GeekPro-15ICK:~$ sudo su [sudo] hi 的密码&#xff1a; roothi-IdeaCentre-GeekPro-15ICK:/home/hi# docker ps 找不到命令 “docker”&#xff0c;但可以通过以下软件包安…

JDK、JRE、JVM的特点和关联

Java 的三个重要的概念是 JDK&#xff08;Java Development Kit&#xff09;、JRE&#xff08;Java Runtime Environment&#xff09;和 JVM&#xff08;Java Virtual Machine&#xff09;。它们之间有着密切的关联&#xff0c;同时又有不同的职责和特点。 JDK&#xff08;Java…

使用skforecast进行时间序列预测

时间序列预测是数据科学和商业分析中基于历史数据预测未来价值的一项重要技术。它有着广泛的应用&#xff0c;从需求规划、销售预测到计量经济分析。由于Python的多功能性和专业库的可用性&#xff0c;它已经成为一种流行的预测编程语言。其中一个为时间序列预测任务量身定制的…

【数据库】执行计划中二元操作对一趟扫描算法的应用,理解代价评估的应用和优化,除了磁盘代价还有CPU计算代价不容忽略

二元操作的一趟算法 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏会定…

makefile项目构建

makefile项目构建 OVERVIEW makefile项目构建1.概念2.make选项3.makefile语法&#xff08;1&#xff09;基本语法&#xff08;2&#xff09;系统与自定变量&#xff08;3&#xff09;常用函数&#xff08;4&#xff09;模式匹配与伪目标 4.makefile编译流程&#xff08;1&#…