V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
FH0
V2EX  ›  C++

熟悉函数式和 C++ 的老哥有偿帮忙解决个问题

  •  
  •   FH0 · 2023-10-30 12:54:24 +08:00 · 1936 次点击
    这是一个创建于 390 天前的主题,其中的信息可能已经有所发展或是发生改变。

      不知道下面的代码高亮了没有,我加了 ```c++,但是预览下的没高亮。应该是支持高亮的,我看其他的帖子里面有高亮的代码。

      新做的一个项目,本来是用 Rust 写的,包管理很方便。但是组长说 Rust 招不到人,就让我用 C++ 重写。C++ 的话现在用的是 git submodules 和 cmake ,没那么爽。好在组长没说 C++ 的版本,应该能用上 C++20 。

      本来是面向对象的命令式写法,但是网上冲浪太多了,经常看到函数式,早就想试试了,但其实自己面向对象也才懂一点点。最近想把测试写起来,因为自己的代码的其中两个 bug 让项目延期了几天,如果能写测试的话项目就不会在我这里卡住了。

      如果是面向对象的测试的话,我看一般是定义一个接口,然后实现里面的纯虚函数,然后写 mock 类测试。但这样的话就需要为了测试去额外定义一个接口。我就想试试函数式,虽然我还不确定函数式能否让测试变得简单、没有额外负担。

      下面的代码和测试无关,我只是想体验一下函数式。我想的是把那一大堆的 if 用 std::map 来代替,同时这个 std::map 在添加子命令的时候自动生成,这样子 main 函数的可读性就比原来好多了,行数也比较少。还有就是上面定义了变量,下面又传递给 SubCommand ,感觉复用度太低了,也许可以用一个结构体,然后和 SubCommand 组合一下。但是不知道具体怎么写。

      有偿的话不知道 200 元行不行。

    int main(int argc, char **argv) {
        // parse command line
        CLI::App cli;
        string   logLevel = "info";
        cli.add_option("-l,--logLevel", logLevel, "log level")
            ->option_text("debug, info, warn, error")
            ->capture_default_str();
        string logFile = "stdout";
        cli.add_option("-f,--logFile", logFile, "log file")->capture_default_str();
        bool version = false;
        cli.add_flag("-v,--version", version, "show version")->capture_default_str();
    
        auto &serveCli = *cli.add_subcommand("serve", "serve uart-manager service");
    
        auto    &readDataCli = *cli.add_subcommand("readData", "read data from uart-manager service");
        uint32_t readDataPort;
        readDataCli.add_option("-p,--port", readDataPort, "uart port")->required();
        uint32_t messages = 1;
        readDataCli.add_option("-m,--messages", messages, "stop after read ? messages")
            ->capture_default_str();
    
        auto    &setBaudRateCli = *cli.add_subcommand("setBaudRate", "set uart baud rate");
        uint32_t setBaudRatePort;
        setBaudRateCli.add_option("-p,--port", setBaudRatePort, "uart port")->required();
        uint32_t setBaudRateBaudRate;
        setBaudRateCli.add_option("-b,--baudRate", setBaudRateBaudRate, "uart baud rate")
            ->option_text("9600, 14400, 19200, 38400, 57600, 115200")
            ->required();
    
        auto &writeDataCli = *cli.add_subcommand("writeData", "write data to uart-manager service");
        vector<uint32_t> writeDataPorts;
        writeDataCli.add_option("-p,--port", writeDataPorts, "uart port")->required();
        string writeDataHex;
        writeDataCli.add_option("-H,--hex", writeDataHex, "hex data");
        string writeDataStr;
        writeDataCli.add_option("-s,--str", writeDataStr, "string data");
    
        auto  &updateFpgaCli = *cli.add_subcommand("updateFpga", "update fpga");
        string updateFpgaFile;
        updateFpgaCli.add_option("-f,--file", updateFpgaFile, "fpga file")->required();
    
        auto    &getAutoResponseCli = *cli.add_subcommand("getAutoResponse", "get auto response");
        uint32_t getAutoResponsePort;
        getAutoResponseCli.add_option("-p,--port", getAutoResponsePort, "uart port")->required();
    
        auto    &clearAutoResponseCli = *cli.add_subcommand("clearAutoResponse", "clear auto response");
        uint32_t clearAutoResponsePort;
        clearAutoResponseCli.add_option("-p,--port", clearAutoResponsePort, "uart port")->required();
    
        auto    &addAutoResponseCli = *cli.add_subcommand("addAutoResponse", "add auto response");
        uint32_t addAutoResponsePort;
        addAutoResponseCli.add_option("-p,--port", addAutoResponsePort, "uart port")->required();
        vector<string> addAutoResponseHex;
        addAutoResponseCli.add_option("-H,--hex", addAutoResponseHex, "response hex data");
        vector<string> addAutoResponseStr;
        addAutoResponseCli.add_option("-s,--str", addAutoResponseStr, "response string data");
        vector<string> addAutoResponseWhenContains;
        addAutoResponseCli
            .add_option("-w,--whenContains", addAutoResponseWhenContains,
                        "response str or hex data when contains str or hex data")
            ->required();
    
        auto &getFpgaVersionCli = *cli.add_subcommand("getFpgaVersion", "get fpga version");
    
        auto &set12vStatusCli = *cli.add_subcommand("set12vStatus", "set 12v of ports 1-48 status");
        auto  set12vStatusEnableFirst = true;
        set12vStatusCli
            .add_flag("-e,--enableFirst", set12vStatusEnableFirst, "enable first 12v, ports 1-24")
            ->capture_default_str();
        auto set12vStatusEnableSecond = true;
        set12vStatusCli
            .add_flag("-s,--enableSecond", set12vStatusEnableSecond, "enable second 12v, ports 25-48")
            ->capture_default_str();
    
        auto &get12vStatusCli = *cli.add_subcommand("get12vStatus", "get 12v of ports 1-48 status");
    
        CLI11_PARSE(cli, argc, argv);
    
        // version
        if (version) {
            printf("version: %s\n", VERSION);
            return 0;
        }
    
        // log
        initLog(logLevel, logFile);
    
        // subcommands
        if (serveCli) {
            Manager().start();
        } else if (readDataCli) {
            SubCommand().readData(readDataPort, messages);
        } else if (setBaudRateCli) {
            SubCommand().setBaudRate(setBaudRatePort, setBaudRateBaudRate);
        } else if (writeDataCli) {
            SubCommand().writeData(writeDataPorts, writeDataHex, writeDataStr);
        } else if (updateFpgaCli) {
            SubCommand().updateFpga(updateFpgaFile);
        } else if (getAutoResponseCli) {
            SubCommand().getAutoResponse(getAutoResponsePort);
        } else if (clearAutoResponseCli) {
            SubCommand().clearAutoResponse(clearAutoResponsePort);
        } else if (addAutoResponseCli) {
            SubCommand().addAutoResponse(addAutoResponsePort, addAutoResponseHex, addAutoResponseStr,
                                         addAutoResponseWhenContains);
        } else if (getFpgaVersionCli) {
            SubCommand().getFpgaVersion();
        } else if (set12vStatusCli) {
            SubCommand().set12vStatus(set12vStatusEnableFirst, set12vStatusEnableSecond);
        } else if (get12vStatusCli) {
            SubCommand().get12vStatus();
        } else {
            cout << cli.help() << endl;
        }
    
        return 0;
    }
    
    15 条回复    2023-11-01 14:24:02 +08:00
    buf1024
        1
    buf1024  
       2023-10-30 13:15:02 +08:00
    伪代码示意
    ```

    std::list xx;
    class Cmd {
    bool* isSet;
    func handler
    };

    cmd 定义以下宏定义
    #define AddCmd
    do {

    auto &get12vStatusCli = *cli.add_subcommand("get12vStatus", "get 12v of ports 1-48 status");
    xx.push(new Cmd{get12vStatusCli, handler...})

    } while (0)

    最后的 if
    for cmd in xx
    if (cmd.isSet) {
    xx()
    }

    ```
    s7964926
        2
    s7964926  
       2023-10-30 13:43:31 +08:00
    ```cpp
    #include <map>
    #include <functional>
    #include <CLI/CLI.hpp>

    struct CmdOptions {
    bool serveCli = false;
    uint32_t readDataPort = 0;
    uint32_t messages = 1;
    };

    class SubCommand {
    public:
    void serve(const CmdOptions &options) {
    // 实现
    }

    void readData(const CmdOptions &options) {
    // 使用 options.readDataPort 和 options.messages
    }

    // 其他
    };

    int main(int argc, char **argv) {
    CLI::App cli;
    CmdOptions options;
    SubCommand subCmd;

    auto &serveCli = *cli.add_subcommand("serve", "serve uart-manager service");
    serveCli.callback([&options, &subCmd]() { subCmd.serve(options); });

    auto &readDataCli = *cli.add_subcommand("readData", "read data from uart-manager service");
    readDataCli.add_option("-p,--port", options.readDataPort, "uart port")->required();
    readDataCli.add_option("-m,--messages", options.messages, "stop after read ? messages");
    readDataCli.callback([&options, &subCmd]() { subCmd.readData(options); });

    // 其他

    CLI11_PARSE(cli, argc, argv);

    return 0;
    }

    ```
    FH0
        3
    FH0  
    OP
       2023-10-30 13:45:42 +08:00
    @buf1024 兄弟,你这个没用到函数式吧
    FH0
        4
    FH0  
    OP
       2023-10-30 13:56:31 +08:00
    @s7964926 确实,用了 callback 就可以省略后面的 if 了。那前面的代码能不能用函数式处理一下。比如:
    FH0
        5
    FH0  
    OP
       2023-10-30 13:58:58 +08:00
    ```c++
    compose(
    newCli,
    serve,
    readData,
    setBaudRate,
    );
    ```


    ```c++
    cli.map(serve)
    .map(readData)
    .map(setBaudRate);
    ```
    luassuns
        6
    luassuns  
       2023-10-30 15:08:32 +08:00
    @FH0 你想要这样只要在 add_option 返回 return this 不就好了
    luassuns
        7
    luassuns  
       2023-10-30 15:23:27 +08:00
    ```cpp

    struct Command
    {
    std::string msg;
    std::string description;
    bool require;
    std::function<void(std::any)> command;
    void exec(std::any v)
    {
    if (command)
    command(v);
    }
    };

    class Args
    {
    public:
    Args *add_opt(Command *command)
    {
    commandMap[command->msg] = command;
    return this;
    }

    void parse(int argc, char *argv[])
    {
    // do parse....
    std::map<std::string, std::any> valueMap;
    for (auto &[key, value] : commandMap) {
    if(valueMap.find(key) != valueMap.end()) {
    value->exec(valueMap[key]);
    } else {
    if(value->require) {
    // require but not found
    }
    }
    }
    }
    std::map<std::string, Command *> commandMap;
    };

    Args *args = new Args();

    args->add_opt(
    new Command{"-p", "port", true, [](std::any v) { std::cout << std::any_cast<int>(v); }})
    ->add_opt(new Command())
    ->add_opt(new Command())
    ->add_opt(new Command())
    ->add_opt(new Command());
    ```
    FH0
        8
    FH0  
    OP
       2023-10-30 15:26:40 +08:00
    @luassuns return this 确实可以,但是方法都是封装在一个类里面的,这是面向对象的思想。我想尝试一下函数式,就是纯函数、副作用、柯里化、函子那些。
    netabare
        9
    netabare  
       2023-10-30 16:44:20 +08:00 via Android   ❤️ 1
    c++没必要太函数式吧,如果 op 想要用的话可以试试 optional 来替代返回错误码/exception 的写法,但不确定 20 是否支持流式的 flatmap 写法。

    感觉用好 move 和 unique_ptr 就好了,c++真的没必要太追求纯函数或者柯里化。
    FH0
        10
    FH0  
    OP
       2023-10-30 18:20:18 +08:00
    @netabare 好吧,C++ 函数式的讨论确实比较少,我似乎走上了一条荒无人烟的道路。
    kilasuelika
        11
    kilasuelika  
       2023-10-30 23:18:27 +08:00 via Android
    boost 里面有个 hof ,高阶函数,可以去了解一下。c++ 本身不是专门做函数式的,要想实现那些必须要进行一些封装和借助一些奇技淫巧。
    FH0
        12
    FH0  
    OP
       2023-10-31 01:03:40 +08:00
    netabare
        13
    netabare  
       2023-10-31 08:28:35 +08:00 via Android   ❤️ 1
    @FH0 其实如果要做函数式的话,C#、Java 之类的虚拟机托管语言会合适很多,至少写起来没那么痛苦。

    函数式写法主要还是追求更好的抽象和表达能力,比较吃编译器的代码优化和语法糖。在 C++这种偏底层,程序员希望能够对内存和变量生命周期有更多掌控权的语言里,有点吃力不讨好。Rust 里面其实也没多少函数式的写法,更多是从函数式编程里面借用了比如模式匹配、trait 之类的思路。

    不过 C++也没有模式匹配或者 trait ,所以还是有点尴尬。
    FH0
        14
    FH0  
    OP
       2023-10-31 14:24:15 +08:00
    @netabare 我找到了一个项目用了很多的函数式思想,还是想探索一下。github.com/arximboldi/ewig
    FH0
        15
    FH0  
    OP
       2023-11-01 14:24:02 +08:00
    最后代码优化成了这样,先暂时用着。
    ```c++
    int main(int argc, char **argv) {
    auto [mainCli, beforeSubcommand] = makeCli(argc);

    const auto subcommands = {
    serve(),
    readData(),
    writeData(),
    setBaudRate(),
    clearAutoResponse(),
    getAutoResponse(),
    addAutoResponse(),
    clearTimedResponse(),
    getTimedResponse(),
    addTimedResponse(),
    get12vStatus(),
    set12vStatus(),
    getFpgaVersion(),
    updateFpga(),
    };
    unordered_map<App *, function<void()>> subcommandMap;
    for (const auto &[cli, callback] : subcommands) {
    mainCli->add_subcommand(cli);
    subcommandMap.emplace(&*cli, callback);
    }

    CLI11_PARSE(*mainCli, argc, argv);

    beforeSubcommand();

    for (auto *key : mainCli->get_subcommands()) {
    subcommandMap[key]();
    }

    return 0;
    }
    ```
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1105 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 18:45 · PVG 02:45 · LAX 10:45 · JFK 13:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.