闭关之 C++ 函数式编程笔记(二):偏函数、组合、可变状态与惰性求值

目录

  • 第四章 以旧函数创建新函数
    • 4.1 偏函数应用
      • 4.1.1 把二元函数转成一元函数的通用方法
      • 4.1.2 使用 std::bind 绑定值到特定的函数参数
      • 4.1.3 二元函数参数的反转 (这节是应用举例,略)
      • 4.1.4 对多参数函数使用 std::bind
      • 4.1.5 使用 lambda 代替 std::bind
    • 4.2 柯里化 (Currying): 看待函数不同的方式
      • 4.2.1 创建柯里化函数的简单方法
    • 4.3 函数组合
    • 4.4 函数提升
    • 总结
  • 第五章 纯洁性:避免可变状态
    • 5.1 可变状态带来的问题
    • 5.2 纯函数和引用透明
    • 5.3 无副作用编程
    • 5.4 并发环境中的可变状态与不可变状态
    • 5.5 const 的重要性
      • 5.5.1 逻辑 const 与内部 const
      • 5.5.2 对于临时值优化成员函数
      • 5.5.3 const 的缺陷
  • 第六章 惰性求值
    • 6.1 C++ 惰性
    • 6.2 惰性作为一种优化技术
      • 6.2.1 集合惰性排序
      • 6.2.2 用户接口中的列表视图
      • 6.2.3 通过缓存函数结果修建递归树
      • 6.2.4 动态编程作为惰性形式
    • 6.3 通用记忆化 (Generalized-memoization)
    • 6.4 表达式模板与惰性字符串拼接
      • 6.4.1 纯洁性与表达式模板
    • 总结
      • 思想

第四章 以旧函数创建新函数

4.1 偏函数应用

  • 通过把已知函数的一个或多个参数设定为特定值的方法创建新函数的概念称为偏函数应用
    • partial function application
    • 偏的意思是,在计算函数结果时,只需传递部分参数,而不需要传递所有参数
      class greater_than 
      {
      public:
          greater_than(int val)
              : value_{ val } 
          {
              
          }
          bool operator()(int arg) const
          {
              return arg > value_;
          }
      private:
          int value_;
      };
      
      greater_than greater_than_1(1);
      greater_than_1(1); //true
      

4.1.1 把二元函数转成一元函数的通用方法

  • 这个函数对象应该能保存一个二元函数和它的一个参数
    template <typename Function, typename SecondArgType>
    class partial_application_on_2nd_impl 
    {
    private:
        Function func_;
        SecondArgType second_arg_;
    public:
        partial_application_on_2nd_impl(Function func, SecondArgType second_arg)
            : func_{ func } 
            , second_arg_{ second_arg}
        {
            
        }
        template <typename FirstArgType>
        auto operator()(FirstArgType&& first_arg) const 
        // -> decltype(func_(std::forward(first_arg), second_arg_))
        {
            return func_(std::forward<FirstArgType>(first_arg), second_arg_);
        }
    
    };
    
    • 由于使用的是普通函数调用语法,因此,只能处理函数对象,而不能处理其他可调用对象
      • 如:成员函数或成员变量的指针
  • 模板参数推断在调用函数时发生
  • 创建上面一元函数的包装函数,以让编译器自动推断类型
    template <typename Function, typename SecondArgType>
    partial_application_on_2nd_impl<Function, SecondArgType>
    bind2nd(Function&& func, SecondArgType&& second_arg) 
    {
        return (partial_application_on_2nd_impl<Function, SecondArgType>(
            std::forward<Function>(func),
            std::forward<SecondArgType>(second_arg)
            ));
    }
    
  • 没必要局限于谓词函数,通过把第二个参数绑定为特定值,可以把任何二元函数转换成一元函数
    • 应用场景
      • 在需要一元函数的场合使用二元函数

4.1.2 使用 std::bind 绑定值到特定的函数参数

  • std::bind 不局限于二元函数,可以用于任意数目参数的函数
    • 也不限制用户指定绑定哪些参数,可以以任意顺序绑定任意数目的参数,而留下不需要绑定的参数
  • std::bind 的使用
    • auto bound = std::bind(std::greater(), 6, 41);
    • bool is_4_greater_than_41 = bound();
    • 只有当调用 bound() 时, std::greater 才会被调用
  • std::bind 引入占位符的概念
    • 如果要绑定一个参数,可以传递一个值给 std::bind
    • 如果要留下一个参数不进行绑定,就必须使用一个占位符
    • 占位符
      • std::placeholders::_1, std::placeholders::_2, ...
      • 定义在 中的 std::placeholders 命名空间中
    • auto is_greater_than_41 = std::bind(std::greater(), std::placeholders::_1, 41);
    • is_greater_than_41(6);

4.1.3 二元函数参数的反转 (这节是应用举例,略)

  • 升序排序可以使用
    • std::less
    • std::sort

4.1.4 对多参数函数使用 std::bind

  • 默认情况下,std::bind在它返回的函数对象中保存绑定值的副本
  • 可以使用 std::ref 绑定引用
    std::for_each(people.cbegin(), people.cend(),
            std::bind(print_person,
                        _1,
                        std::ref(std::cout),
                        person_t::name_only
                ));
    
    std::for_each(people.cbegin(), people.cend(),
            std::bind(print_person,
                        _1,
                        std::ref(file),
                        person_t::full_name
                ));
    
  • std::bind 也可以绑定非成员函数或静态成员函数, 类成员函数无法绑定
    • 但可以绑定成员函数指针
      std::for_each(people.cbegin(), people.cend(),
              std::bind(&person_t::print,
                          _1,
                          std::ref(std::cout),
                          person_t::name_only
                  ));
      

4.1.5 使用 lambda 代替 std::bind

  • std::bind 的缺点
    • 会创建新的函数对应,带来额外的开销
    • 编译器很难进行优化
  • 对于偏函数应用,可以使用 lambda 代替 std::bind
    • 虽然语法上有点冗长,但可以便于编译器优化
  • std::bind 改成 lambda 方式
    • 把任何绑定变量或引用的参数转换成捕获变量
      • [value]
    • 把所有占位符转换成 lambda 的参数
    • 把所有绑定到特定值的参数直接写在 lambda 体中
      auto is_greater_than_41 = [](double value) {
              return std::greater<double>()(value, 41);
          };
      is_greater_than_41(6);
      

4.2 柯里化 (Currying): 看待函数不同的方式

  • 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
    bool greater(double first, double second) 
    {
        return first > second;
    }
    //Currying 函数
    auto greater_curried(double first) 
    {
        return [first] (double second) {        
            return first > second;
        };
    }
    greater(2, 3); //返回false
    greater_curried(2); //返回一个一元函数对象
    greater_curried(2)(3); //返回false
    
    
    • 如果函数有多个参数,则可以嵌套任意多的 lambda 以收集所有的参数,并返回需要的结果

4.2.1 创建柯里化函数的简单方法

  • 多参数函数创建柯里化函数
    auto print_person_cd(const person_t& person) 
    {
        return [&](std::ostream& out) {
            return [&](person_t::output_format_t format) {
                print_person(person, out, format);
            };
        };
    }
    
    • 这样写代码非常繁琐,所以可以使用辅助函数 make_curried
      • 可以把任意的函数转换成它的柯里化版本
    • 生成的柯里化函数还提供了语法糖
      • 可以让程序员根据需要一次指定所有参数
    • 柯里化函数只是一个一元函数,只是更加方便使用
  • make_curried 函数 (实现暂略)
    • 用法
      auto print_person_cd = make_curried(print_person);
      
      print_person_cd(martha, cout, person_t::full_name); //在一次调用中可以选择传递几个参数
      print_person_cd(martha)(cout, person_t::full_name);
      print_person_cd(martha, cout)(person_t::full_name);
      print_person_cd(martha, cout, person_t::full_name);
      print_person_cd(martha)(cout)(person_t::full_name);
      
      auto print_martha = print_person_cd(martha);  //返回一个柯里化函数对象
      print_martha(std::cout, person_t::name_only);  //输出
      
      auto print_martha_to_cout = print_person_cd(martha, std::cout);
      print_martha_to_cout(person_t::name_only)
      
      
  • 柯里化函数可以解决 API 膨胀问题
  • 柯里化函数限制
    • 它必须按顺序绑定参数
  • 偏函数和柯里化各自的适用场景
    • 当有一个要绑定其参数的特定函数时, 偏函数比较有用
    • 当函数可以有任意多个参数时, 柯里化很实用

4.3 函数组合

  • 书中的例子略,这里记一下函数组合的思想
  • 思想
    • 首先将一个问题拆分,获得几个函数
      • 每个函数只做一些简单的事情
    • 创建一种机制,让他们容易组合起来
      • 一般做法是,让这些函数的返回值成为下一个函数的参数
    void print_common_words(const std::string& text) 
    {
        return print_pair(
            sort_by_frequency(
                revers_pairs(
                    count_occurrences(
                        words(text)
                    )
                )
            )
        );
    }
    
  • 做法
    • 从一个大问题开始,不是要分析获得结果所需要的步骤,而是要分析对输入进行哪些转换
    • 并对每个转换创建一个简短、简单的函数
    • 最后把他们组合在一起,构成一个大函数解决这个问题

4.4 函数提升

  • 提升是一种编程模式
    • 它提供了一种方式,把给定的函数转换成一个类似可广泛应用的函数
  • 函数提升的示例 (把一个字符串转换成大写字母的函数提升)
    • 基本函数为:void to_upper(std::string& str);
    • 增加功能
      • 对字符串指针进行操作
      void pointer_to_upper(std::string* str)
      {
        if(str) to_upper(*str);
      }
      
      • 对集合进行操作
      void vector_to_upper(std::vector<std::string>& strs)
      {
        for(auto& str: strs)
        {
          to_upper(str);
        }
      }
      
      • 对map进行操作
      void map_to_upper(std::map<int, std::string>& strs)
      {
        for(auto& pair: strs)
        {
          to_upper(pair.second);
        }
      }
      
    • 上面这些函数就称为提升函数
      • 它们把操作某一类型的函数提升为操作更多类型的数据结构的函数
    • 上面可以看出,这些实现都与容器类型有关
      • 因此可以创建高阶函数,简化代码量
      template <typename Function>
      auto pointer_lift(Function f) 
      {
          return [f](auto* item) {
              if (item) {
                  f(*item);
              }
          };
      }
      
      template <typename Function>
      auto collection_lift(Function f)
      {
          return [f](auto& items) {
              for (auto& item : items) {
                  f(item);
              }
          };
      }
      
      

总结

  • 偏函数
  • std::bind
  • 柯里化 (Currying)
  • 函数组合
  • 函数提升

第五章 纯洁性:避免可变状态

5.1 可变状态带来的问题

  • 自己的理解
    • 某函数在使用某个变量进行计算时,如果函数外部对该变量进行了修改,就可能使计算结果不正确
      • 而该变量就是可变状态
      • 从并行编程角度讲,就是该变量不是线程安全变量

5.2 纯函数和引用透明

  • 纯函数定义已在第一章解释
  • 引用透明
    • 如果表达式的结果替换整个表达式,而程序不会出现不同行为,那么这个表达式就是引用透明的
    • 如果表达式是引用透明的,那他就没有任何可见的副作用
    • 因此表达式中所有函数都是纯函数
    • 中介
    double max(const std::vector<double> numbers) 
    {
        auto result = std::max_element(numbers.cbegin(), numbers.cend());
        std::cerr << "max is: " << *result << std::endl;
        return *result;
    }
    
    auto sum_max_1 = max({ 1 }) + max({ 1, 2 }) + max({ 1, 2 ,3 });
    auto sum_max_2 = 1 + 2 + 3;
    
    • 当 sum_max_1 和 sum_max_2 结果相同时,同时删除 max 函数的 cerr 函数,max 函数就为引用透明,是纯函数
  • 在判定纯函数和引用透明时,需要忽略日志等调试代码段。

5.3 无副作用编程

  • 思想
    • 不去改变一个值,而是创建一个新值(原有值得副本)
    • 对新值进行操作就不会有可变状态,因此,无副作用
  • 个人理解
    • 这样做复制的开销怎么办?

5.4 并发环境中的可变状态与不可变状态

  • 个人理解
    • 使用锁或原子操作约束可变状态

5.5 const 的重要性

  • C++ 限制改变两种方式
    • const
    • constexpr
  • const T 是一种类型。因此不能赋值给 T&,但能赋值给 const T&
  • 常引用的特殊性 const T&
    • 常引用除了可以绑定到 const T 类型的变量,还可以绑定到一个临时的值
    • 这种情况下,常引用会延长临时变量的生存时间

5.5.1 逻辑 const 与内部 const

  • 当类的成员变量有 const 修饰时,某些编译器会丢弃移动构造和移动复制操作符
  • 内部 const
    • 把类的所有公共成员函数声明为 const
    • 这时 this 都指向该类的 const 对象
      • 用户不能修改成员变量
    • 因此 const 修饰的成员函数无法修改成员变量
      • 可以使用 mutable 修饰成员变量,这时可修改
        • 但是最好加锁进行修改

5.5.2 对于临时值优化成员函数

  • 成员函数声明为 const&
    • 可以利用 this 指向对象创建副本
  • 成员函数声明为 &&
    • 可以移动 this 指向的对象

5.5.3 const 的缺陷

  • const 禁止对象移动
    • 编译器无法使用命名返回值优化(named return value optimization, NRVO)
  • Shallow Const
    • 虽然常成员函数无法修改成员变量,但是如果成员变量是一个指向对象的指针,这时是可以修改指针指向的对象的

第六章 惰性求值

  • 惰性的意义
    • 对于工作不是提前而是尽可能推后。
    • 因为有惰性,也不可能多次重复做一件事,所以当得到结果后,就应记住这个结果

6.1 C++ 惰性

  • C++ 不支持开箱即用的惰性求值,但提供了一些工具,可以用来模拟
  • 设计惰性模板类
    • 应包含的内容
      • 计算(过程)
      • 指示结果是否已经计算的标志
      • 计算结果
      • 未完成
    • Code_6_1_1
      • 如果不希望成员函数在构造时调用默认构造函数,可以使用 optional
      • std::call_onestd::once_flag
        • 检测标识位是否设置,未设置调用函数
        • 线程安全

6.2 惰性作为一种优化技术

6.2.1 集合惰性排序

  • 思想
    • 实际应用中,经常使用排序功能。但是都是翻页模式,每次显示排序前面若干值
    • 如果每次全排序,会浪费时间。
    • 因此,可以使用惰性排序方法进行优化
      • 惰性快速排序
      • Code_6_2_1

6.2.2 用户接口中的列表视图

  • 如果有一大批数据,却只有有限的屏幕空间显示数据,就可以进行惰性优化
    • 通常做法,数据惰性加载
      • 只在显示给用户时才加载数据
  • 数据库是惰性排序

6.2.3 通过缓存函数结果修建递归树

  • Fibonacci 数的例子
    • 思想是
      • 如果在递归计算时有重复的计算,可以使用惰性求值方式,跳过某个递归过程的计算

6.2.4 动态编程作为惰性形式

  • 动态编程 (Dynamic programming)
    • 是一种将复杂问题分解成更小问题的一种技术
    • 在解决这些小问题时,可以保存解决方案以备后续使用

6.3 通用记忆化 (Generalized-memoization)

  • 个人理解
    • 就是如何将函数保存到容器中,以便后续使用(是惰性的一种用法,缓存函数)
  • 函数指针的记忆化包装
    • Code_6_3_1
      • 对普通函数起作用,但不能优化递归函数
  • 递归函数的记忆化包装
    • Code_6_3_2
    • 优点
      • 可以接收任意类型的函数对象
  • 这两个例子重点为运行时优化
    • 程序运行时可以执行不同的代码路径
    • 但要对所有的代码路径进行优化,即使能提前知道程序的每一步,惰性求值也是十分有好处的

6.4 表达式模板与惰性字符串拼接

  • 字符串拼接
    • 使用 + 操作符拼接字符串不高效,因为它是二元操作符,在拼接多个字符串时会产生临时变量
    • 优化方式
      • 先收集所有要拼接的字符串,在进行拼接计算
      • 收集完成后,创建一个足够大的缓存,一次性拼接字符串
  • 表达式模板作用
    • 允许产生表达式的定义,而不是求解表达式的值
  • 保存任意数目字符串的结构
    • Code_6_4_1

6.4.1 纯洁性与表达式模板

  • Code_6_4_1 保存了原始字符串的副本
    • 如果最求高效,可以保存 const std::string&
    • 但是存在缺陷
      • 无法在原始字符串定义范围外使用这个表达式,如果原始字符串释放,所持引用会无效
      • 用户希望拼接结果是一个字符串类型,而不是其他中间类型。如果将拼接换成自动推导,会出现意想不到的问题
  • 惰性求值必须是“纯”的
    • 纯函数对相同的参数总会给出相同的结果
    • 因此,即使延迟执行也不会有任何后果
  • 表达式模板可产生一种结构,它表示一种计算,而不会立即计算这个表达式

总结

  • 代码惰性执行和结果缓存可显著提高程序的运行速度
  • 构造程序中算法的惰性变体有时并不那么容易,如果试着去做,可提高程序的响应能力
  • 有很大部分指数级复杂度算法,通过缓存中间结果,可以优化到线性级或平方级
  • levenshtein 距离有许多应用(声音处理,DNA分析,拼写检查等)。
    • 当需要将数据模型中的更改通知UI时,能够减少UI需要执行的操作数目
  • 对不同的问题单独编写缓存机制
  • 表达式模板是一种延迟计算的强大工具
    • 经常用在操作矩阵库中,或提交编译器之前对表达式进行优化的场合

思想

  • 惰性模板类 Code_6_1_1
  • 函数指针的记忆化包装 Code_6_3_1
  • 递归函数的记忆化包装 Code_6_3_2
  • 表达式模板 Code_6_4_1

你可能感兴趣的:(笔记,c++)