第一章 函数式编程基础

函数式编程的益处

  • 装逼

Yagur Alex - Functional Programming with C - 2024.pdf - p22 - But why should you consider adopting functional programming in your projects? The benefits aremany, including the following

C#对函数式编程的支持

  • Lambda表达式

  • LINQ

  • 数据类型不可变

  • 模式匹配

    object obj = "Hello, World!";
    if (obj is string str) // 模式匹配
    {
    Console.WriteLine(str.ToUpper()); // 输出 "HELLO, WORLD!"
    }
  • 委托与事件

怎么写函数式代码

  • 表达式写法

    // 非函数式写法:命令式
    int sum = 0;
    foreach (var number in numbers)
    {
    if (number > 0)
    {
    sum += number;
    }
    }

    // 函数式写法:声明式(表达式)
    var sum = numbers.Where(x => x > 0).Sum();
  • 纯函数(Pure Function)

    // 即永远: 相同输入->相同输出
    // 同时 : 没有副作用(不干别的事情,如log,更改外部状态)

    // 非纯函数:修改外部状态
    int result = 0;
    void Add(int a, int b)
    {
    result = a + b; // 修改外部状态
    }

    // 纯函数
    int Add(int a, int b) => a + b;
  • 诚实函数(Honest Functions)

    // 诚实函数 = 明确输入 + 明确输出 + 明确失败路径 + 无隐藏副作用
    // 诚实函数 是一种 对调用者“诚实”的函数
    // 它 不会隐藏信息、不会偷偷修改外部状态、不会意外地抛出异常、也不会返回模糊不清的值

    // 诚实函数
    int Multiply(int a, int b) => a * b;
    // 非诚实函数(隐含使用了外部状态)
    int result;
    void Multiply(int a, int b) => result = a * b;

    // 诚实函数
    int Square(int x) => x * x;
    // 非诚实函数(有副作用,打印log)
    int Square(int x)
    {
    Console.WriteLine("计算平方");
    return x * x;
    }

    // 诚实函数
    string Divide(int numerator, int denominator)
    {
    if (denominator == 0)
    throw new ArgumentException("Denominator cannot be zero.");
    return (numerator / denominator).ToString();
    }
    // 非诚实函数 (未处理错误)
    string Divide(int numerator, int denominator)
    {
    return (numerator / denominator).ToString();
    }

    // 非诚实函数
    // 1. 悄悄访问数据库(副作用)
    // 2. 如果 userId 无效可能抛异常
    // 3. 签名没有任何表示错误或失败的可能
    double CalculateDiscountedPrice(double price, int userId)
    {
    var user = Database.GetUser(userId); // 隐式依赖外部资源
    if (user.IsPremium)
    return price * 0.8;
    return price;
    }
  • 高阶函数

    // 即接受其他函数作为参数,或者返回函数作为结果

    // 非高阶函数:显式的操作
    int result;
    int a = 5, b = 10;
    string operation = "add"; // 假设通过某种方式传入操作类型

    if (operation == "add")
    {
    result = a + b;
    }
    else if (operation == "multiply")
    {
    result = a * b;
    }

    // 高阶函数:接受函数作为参数
    Func<int, int, int> Add = (a, b) => a + b;
    Func<int, int, int> Multiply = (a, b) => a * b;

    Func<int, int, Func<int, int>> ApplyOperation(Func<int, int, int> operation) =>
    (a, b) => operation(a, b);

    var result = ApplyOperation(Add)(5, 10); // 15
  • 函子(Functors)

    // 函子 : 一个类型(容器),一个定义了Map操作的类型(容器)
    // Map方法: 对内部的值应用函数(映射),同时保持容器结构不变
    // 函子满足规则:
    // 1. 恒等律: box.Map(x => x) 等价于 box
    // 2. 组合律: box.Map(f).Map(g) 等价于 box.Map(x => g(f(x)))

    // 示例
    public class Box<T>
    {
    public T Value { get; }

    public Box(T value)
    {
    Value = value;
    }

    public Box<U> Map<U>(Func<T, U> func)
    {
    return new Box<U>(func(Value));
    }
    }

    var box = new Box<int>(5);
    var newBox = box.Map(x => x * 2); // Box<int>,Value 为 10
  • 单子(Monads)

    // 单子 : 一个更牛逼的“函子”, 有Return 和 Bind 操作的函子
    // Return : 把普通值变成单子
    // Bind : 接收一个函数,这个函数会产生另一个单子(Monad),然后把它“展开”并返回
    // 从一个容器 M<T> 出发,
    // 接收一个函数 T → M<U>,
    // 结果是一个新的容器 M<U>。

    // Bind 示例
    Maybe<int> GetNumber() => new Maybe<int>(3);

    Maybe<int> MultiplyByTwo(int x) => new Maybe<int>(x * 2);

    var result = GetNumber().Bind(MultiplyByTwo);

    // Return示例
    public static Maybe<T> Return<T>(T value) => new Maybe<T>(value);

    Maybe<int> five = Return(5); // Maybe<int> with value 5

C# 单子实现范例

public class Maybe<T>
{
private readonly T _value;
public bool HasValue { get; } = false;

private Maybe() {}
public Maybe(T value)
{
_value = value;
HasValue = value != null;
}

public static Maybe<T> Return(T value) => new Maybe<T>(value);

public Maybe<U> Bind<U>(Func<T, Maybe<U>> func)
{
return HasValue ? func(_value) : Maybe<U>.Return(default);
}

public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue;
}

// 使用

Maybe<int> GetUserAge(string userId) =>
userId == "admin" ? Maybe<int>.Return(42) : Maybe<int>.Return(default);

Maybe<int> DoubleAge(string userId) =>
GetUserAge(userId).Bind(age => Maybe<int>.Return(age * 2));

// 示例

Maybe<string> GetName(int id) =>
id == 1 ? Maybe<string>.Return("Alice") : Maybe<string>.Return(null);

Maybe<int> GetLength(string s) =>
s != null ? Maybe<int>.Return(s.Length) : Maybe<int>.Return(0);

// 用 Map:得到 Maybe<Maybe<int>>(嵌套)
var nested = GetName(1).Map(GetLength);

// 用 Bind:得到 Maybe<int>(展开)
var flat = GetName(1).Bind(GetLength);

在Linq中, Return 对应了创建的起始容器,Bind对应了SelectMany

var result =
from age in GetAge("Alice") // <-- 启动 Monad:Return(x)
from bonus in GetBonus(age) // <-- SelectMany:Bind
select age + bonus; // <-- Select:map 输出

书中代码示例

Yagur Alex - Functional Programming with C - 2024.pdf - p25 - A practical example – a book publishing system

这个例子展现了函数式编程的几个关键概念:

  • 不可变性: 使用 record 类型来定义 Book
  • 纯函数(Pure functions)IsValidFormatBook 都是纯函数。它们在相同输入下总是返回相同的输出,并且没有副作用
  • 高阶函数(Higher-order function)ProcessBooks 是一个高阶函数,它接收两个函数作为参数(一个验证器和一个格式化器
  • 可组合性(Composability) :在 ProcessBooks 函数中,我们将验证和格式化操作进行了组合
  • 声明式风格(Declarative style) :我们描述了我们想要的结果(合法且已格式化的书籍),而不是一步一步地说明如何实现
  • LINQ:我们使用了 LINQ 方法(如 WhereSelect),它们非常符合函数式编程的原则

如何结合C#的OOP和函数式编程

  • 使用不可变对象
  • 使用扩展方法
  • 将((20250423205629-fwkf2n6 “高阶函数”))作为类的实例方法
  • 依赖注入与组合