From 8fdca49e1d63db1075a23958ff23189818a49d6c Mon Sep 17 00:00:00 2001 From: muqiuhan Date: Thu, 13 Mar 2025 09:18:34 +0000 Subject: deploy: 11fd5b5345c11055a7c18e9b2c0baa4d2787fe15 --- search.xml | 201 ++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 105 insertions(+), 96 deletions(-) (limited to 'search.xml') diff --git a/search.xml b/search.xml index c9a257c8..c43c45c8 100644 --- a/search.xml +++ b/search.xml @@ -504,70 +504,6 @@ Archive - - C++ 20 实现 string split - /2023/05/12/C-20-%E5%AE%9E%E7%8E%B0-string-split/ - C++20引入了范围库ranges,其中提供的两个范围适配器std::split、std::lazy_split可以使我们以一种更为优雅的形式实现split:

-
#include <concept>
#include <ranges>
#include <algorithm>
#include <format>
#include <iostream>

#define stdr std::ranges
#define stdrv std::ranges::views

template<template<typename> typename Container = std::vector, typename Arg = std::string_view>
auto Split(std::string_view str, std::string_view delimiter)
{
Container<Arg> myCont;
auto temp = str
| stdrv::split(delimiter)
| stdrv::transform([](auto&& r)
{
return Arg(std::addressof(*r.begin()), stdr::distance(r));
});
auto iter = std::inserter(myCont, myCont.end());
stdr::for_each(temp, [&](auto&& x) { iter = {x.begin(), x.end()}; });
return myCont;
}
int main()
{
std::string str = "Hello233C++20233and233New233Spilt";
std::string delimiter = "233";
auto&& strCont = Split<std::list, std::string>(str, delimiter);
stdr::for_each(strCont, [](auto&& x) { std::cout << std::format("{} ", x); });
}
//output: Hello C++20 and New Spilt
- -

C++20没有提供关键的 ranges::to<container>函数,导致demo中还需要额外封装并手写for_each来写入数据,等到C++23实装了该函数,split的实现会比现在简洁优雅的多,真正做到方便泛用、无需封装:

-
auto&& strCont = str
| stdrv::lazy_split(delimiter)
| stdr ::to<std::vector<std::string>>;
]]>
- - Technique - C++ - -
- - C++ vector 的 push_back 和 emplace_back - /2023/05/11/C-vector-%E7%9A%84-push-back-%E5%92%8C-emplace-back/ -
/// Inserts a new element at the end of the vector, right after its current last element. This new element is constructed in place using args as the arguments for its constructor.
/// This effectively increases the container size by one, which causes an automatic reallocation of the allocated storage space if -and only if- the new vector size surpasses the current vector capacity.
/// The element is constructed in-place by calling allocator_traits::construct with args forwarded.
///A similar member function exists, push_back, which either copies or moves an existing object into the container.
template <class... Args>
void emplace_back (Args&&... args);
- -

push_back 会构造一个临时对象,这个临时对象会被拷贝或者移入到容器中,然而 emplace_back 会直接根据传入的参数在容器的适当位置进行构造而避免拷贝或者移动。

-

传统观点认为 push_back 会构造一个临时对象,这个临时对象会被移入到 v 中,然而 emplace_back 会直接根据传入的参数在适当位置进行构造而避免拷贝或者移动。从标准库代码的实现角度来说这是对的,但是对于提供了优化的编译器来讲,上面示例中最后两行表达式生成的代码其实没有区别。

-

真正的区别在于,emplace_back 更加强大,它可以调用任何类型的(只要存在)构造函数。而 push_back 会更加严谨,它只调用隐式构造函数。隐式构造函数被认为是安全的。如果能够通过对象 T 隐式构造对象 U,就认为 U 能够完整包含 T 的所有内容,这样将 T 传递给 U 通常是安全的。正确使用隐式构造的例子是用 std::uint32_t 对象构造 std::uint64_t 对象,错误使用隐式构造的例子是用 double 构造 std::uint8_t。

-

如果想要调用显示构造函数,那么就调用 emplace_back。如果只希望调用隐式构造函数,那么请使用更加安全的 push_back:

-
std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile
- -

std::unique_ptr<T> 包含了显示构造函数通过 T* 进行构造。因为 emplace_back 能够调用显示构造函数,所以传递一个裸指针并不会产生编译错误。然而,当 v 超出了作用域,std::unique_ptr<T> 的析构函数会尝试 delete 类型 T* 的指针,而类型 T* 的指针并不是通过 new 来分配的,因为它保存的是栈对象的地址,因此 delete 行为是未定义的。

-]]>
- - Technique - C++ - -
- - C++ 的 Trait - /2023/05/04/C-%E7%9A%84-Trait/ - -

C++ 的 traits 技术,是一种约定俗称的技术方案,用来为同一类数据(包括自定义数据类型和内置数据类型)提供统一的类型名(traits),这样可以统一的操作函数,例如 advance(), swap(), encode()/decode() 等。

- -

例如,拥有义类型Foo, Bar,以及编译器自带类型 int, double, string,我们想要为这些不同的类型提供统一的编码函数 decode() 。

-

除了使用 trait 技术之外,函数重载和模板函数 + 内置字段也可以实现,前者每增加一种数据类型就需要重新实现一个函数,而同一类数据(int, unsinged int)可以使用同样的编码方法。我们想要的是针对同一种数据类型,只编写一个函数。后者对于系统自定义变量 int, double 而言,是无法在其内部定义 type 的。

-

traits 技术的关键在于,使用另外的模板类 type_traits 来保存不同数据类型的 type,这样就可以兼容自定义数据类型和内置数据类型:

-
// 定义数据 type 类
enum Type {
TYPE_1,
TYPE_2,
TYPE_3
}
- -

对于自定义类型,在类内部定义 type,然后在 traits 类中定义同样的 type:

-
// 自定义数据类型
class Foo {
public:
Type type = TYPE_1;
};
class Bar {
public:
Type type = TYPE_2;
};
template<typename T>
struct type_traits {
Type type = T::type;
}
- -

对于内置数据类型,使用模板类的特化为自定义类型生成独有的 type_traits:

-
// 内置数据类型
template<typename int>
struct type_traits {
Type type = Type::TYPE_1;
}
template<typename double>
struct type_traits {
Type type = Type::TYPE_3;
}
- -

这样就可以为不同数据类型生成统一的模板函数

-
// 统一的编码函数
template<typename T>
void decode<const T& data, char* buf) {
if(type_traits<T>::type == Type::TYPE_1) {
...
}
else if(type_traits<T>::type == Type::TYPE_2) {
...
}
}
- -

总结

-
    -
  • traits 技术的关键在于使用第三方模板类 traits,利用模板特化的功能, 实现对自定义数据和编译器内置数据的统一
  • -
  • 这个例子使用了枚举变量来表示数据类型,而实际操作中通常使用不同的类来表示不同的类型,这样可以在编写模板函数时更好的优化。
  • -
  • tratis 技术常见于标准库的实现中,但对日常开发中降低代码冗余也有很好的借鉴意义
  • -
  • C++20 提供了Concept 的特性,使用Concept 可以使得实现类似的功能更加方便
  • -
-]]>
- - Technique - C++ - -
Dealing with complex dependency injection in FSharp /2024/10/18/Dealing-with-complex-dependency-injection-in-FSharp/ @@ -687,6 +623,32 @@ Medicine + + G-Machine + /2024/01/23/G-Machine/ + +
  • The G-machine: A fast, graph-reduction evaluator
  • + +

    G-Machine 是一种通过图规约来对函数式语言程序求值的抽象架构。
    与组合子规约不同,组合子规约的control是从表达式图本身动态导出的,而G-Machine是由通过编译Application表达式导出的指令序列指定的。

    +
    +

    FP的程序基本上都可以用一个表达式的图表示,图计算机就是对这个图求值的机器,总的说来对图的求值是一个不停合并图上的节点生产新节点的过程。

    +

    例如:

    +
    let x = 2 + 3 in 
    x * x
    + +

    先计算出5,然后创建一个新的节点 5 * 5,然后再对这个节点求值,于是求值过程中就产生了很多临时的节点,这些中间节点也被叫做是 spine,求值过程是沿着 spine 进行的。

    +

    但是这样就产生了很多额外的开销,lambda lifting 里提到:可以把程序里,很多捕捉了外围绑定的闭包函数中的这些绑定,转换成函数的参数,从而消除闭包,得到的这个函数就可以自由脱离作用域,被静态的编译到机器码里。这些被 float out 的函数也叫 supercombinator.

    +

    在上面的代码中,如果不创建新的节点,顺序计算完了第一个 x,第二个 x 还会再被算一遍。

    +

    Spineless reduction 的概念:只有当面临需要重复计算的情况时,才去创建节点,不然就一路顺序算下去

    + +]]>
    + + Technique + +
    Functional Reactive Programming in F# /2024/09/12/Functional-Reactive-Programming-in-F/ @@ -814,32 +776,6 @@ Archive - - G-Machine - /2024/01/23/G-Machine/ - -
  • The G-machine: A fast, graph-reduction evaluator
  • - -

    G-Machine 是一种通过图规约来对函数式语言程序求值的抽象架构。
    与组合子规约不同,组合子规约的control是从表达式图本身动态导出的,而G-Machine是由通过编译Application表达式导出的指令序列指定的。

    -
    -

    FP的程序基本上都可以用一个表达式的图表示,图计算机就是对这个图求值的机器,总的说来对图的求值是一个不停合并图上的节点生产新节点的过程。

    -

    例如:

    -
    let x = 2 + 3 in 
    x * x
    - -

    先计算出5,然后创建一个新的节点 5 * 5,然后再对这个节点求值,于是求值过程中就产生了很多临时的节点,这些中间节点也被叫做是 spine,求值过程是沿着 spine 进行的。

    -

    但是这样就产生了很多额外的开销,lambda lifting 里提到:可以把程序里,很多捕捉了外围绑定的闭包函数中的这些绑定,转换成函数的参数,从而消除闭包,得到的这个函数就可以自由脱离作用域,被静态的编译到机器码里。这些被 float out 的函数也叫 supercombinator.

    -

    在上面的代码中,如果不创建新的节点,顺序计算完了第一个 x,第二个 x 还会再被算一遍。

    -

    Spineless reduction 的概念:只有当面临需要重复计算的情况时,才去创建节点,不然就一路顺序算下去

    - -]]>
    - - Technique - -
    .NET AOT 下的 F# 命令行参数解析库选择 /2024/06/06/NET-AOT-%E4%B8%8B%E7%9A%84-F-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90%E5%BA%93%E9%80%89%E6%8B%A9/ @@ -2595,7 +2531,7 @@ 二〇二四年十一月十七日 /2024/11/17/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%8D%81%E4%B8%80%E6%9C%88%E5%8D%81%E4%B8%83%E6%97%A5/ - 快乐是正面情绪的原型,许多思想家都说过,人类的所作所为,最终都是为了追求快乐。我们之所以想追求财富、健康或名声,都是为了借此得到快乐。然而,追求快乐也不是因为它可以带给我们其他好处,因为快乐本身就是目的。

    + 快乐是正面情绪的原型,人类的所作所为,最终都是为了追求快乐。我们之所以想追求财富、健康或名声,都是为了借此得到快乐。然而,追求快乐也不是因为它可以带给我们其他好处,因为快乐本身就是目的。

    “世人皆以痛楚、孤独、悲伤为大境界,以为这就是深邃的人生,殊不知快乐才是人生的真谛。一个人获取快乐的能力才是真正有用的能力。伤春悲秋、离愁别绪太容易了,读几首诗词即可,但获取欢乐太难了非大丈夫不可为之。”

    ]]>
    @@ -2792,11 +2728,11 @@ 二零二三年六月十日 /2023/06/10/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AD%E6%9C%88%E5%8D%81%E6%97%A5/ - 我希望与我一样独自生活多年的宝宝们,能绝对理性的面对所有事情,这是一个人工作学习生活的基础,但同时要感同身受的去阅读文学类作品,这是让我们保留一丝感性的方式,感性是不可或缺的,理性可以让人控制感性,但也可以消灭它。

    -

    一个人的世界很容易出现信息茧房,要不断的和人交流,把很多想法说出来,接收一切评论,不然会在长期的不分正确错误的信息堆叠中出现一团巨大的闭塞性的知识,这很不利于我们快乐的活下去。

    -

    接收到的评论不必急于改变,先存起来,让它们陪着你跟着时间走,路上会慢慢的和其他的事情连结起来,这样就能择其善者而从之,其不善者而改之了。

    -

    和我一样处在青少年阶段的宝宝们应当早日从这喧扰的世界冷静下来,让脑子里满是憧憬和情爱的灵魂得到一丝陈酿,理性点抬头看看世界上方的二氧化碳,自己晃晃头打破能回到最初的样子再重来的梦。

    -

    爱你们噢。

    + 请绝对理性的面对所有事情,这是一个人工作学习生活的基础,但同时要感同身受的去阅读文学类作品,这是保留一丝感性的方式,感性是不可或缺的,理性可以让人控制感性,但也可以消灭它。

    +

    独处的世界很容易出现信息茧房,要不断的和人交流,把很多想法说出来,接收一切评论,不然会在长期的不分正确错误的信息堆叠中出现一团巨大的闭塞性的知识,这很不利于快乐的活下去。

    +

    接收到的评论不必急于改变,先存起来,让它们陪着自己,跟着时间走,路上会慢慢的和其他的事情连结起来,这样就能择其善者而从之,其不善者而改之了。

    +

    应当早日从这喧扰的世界冷静下来,让脑子里满是憧憬和情爱的灵魂得到一丝陈酿,理性点抬头看看世界上方的二氧化碳,自己晃晃头打破能回到最初的样子再重来的梦。

    +

    爱你们。

    ]]>
    gallery @@ -2940,6 +2876,27 @@ gallery
    + + 从事件风暴看领域驱动设计 + /2025/02/11/%E4%BB%8E%E4%BA%8B%E4%BB%B6%E9%A3%8E%E6%9A%B4%E7%9C%8B%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1/ + 事件风暴(Event Storming)是一种领域驱动设计(DDD)的实践方法,由 Alberto Brandolini 提出,旨在通过团队协作的方式快速理解和建模业务领域。

    +

    以下是事件风暴及相关领域驱动设计中的一些核心概念和知识点:

    +

    1. 领域(Domain):指的是业务相关知识的集合,可以进一步划分为子域。
    2. 子域(Subdomain):是领域的一部分,可以是核心域、支撑域或通用域。
    3. 核心域(Core Domain):指领域中最核心的部分,通常对应企业的核心业务。
    4. 通用语言(Ubiquitous Language):团队所有成员使用的一种语言,用于确保业务和软件之间的沟通一致性。
    5. 限界上下文(Bounded Context):定义了一组规则和协议,用于明确领域模型的适用范围。
    6. 实体(Entity):具有唯一标识和生命周期的领域对象。
    7. 值对象(Value Object):描述了某种特性或属性的对象,没有概念标识。
    8. 聚合(Aggregate):一组相关对象的集合,由一个聚合根(Aggregate Root)统一管理。
    9. 领域事件(Domain Event):领域中发生的重要事件,可以用于通知其他领域对象或跨限界上下文进行解耦和协作。
    10. 命令(Command):表示要执行的操作,通常与事件一一对应。
    11. 读模型(Read Model):为了优化读取操作而设计的模型,可能与写模型不同。
    12. 决策命令(Decision Command):在事件风暴中,直接导致事件发生的命令。
    13. 战略设计(Strategic Design):高层次的抽象和归类,包括理清上下文和子域的划分。
    14. 战术设计(Tactical Design):对特定上下文下的模型进行详细设计,包括聚合、实体和值对象。
    15. 贫血模型(Anemic Domain Model):领域对象只有属性及其getter/setter方法的纯数据类,业务逻辑通过服务实现。
    16. 充血模型(Rich Domain Model):领域对象包含业务逻辑,每个对象都是活跃的。
    17. 资源库(Repository):用于检索和持久化领域对象的机制。
    18. 服务(Service):在模型中独立的操作,可以是领域服务或应用服务。
    19. 固定规则(Invariant):为设计元素做出的断言,必须一直保持为真。

    +

    事件风暴通常包括以下步骤:

    +
      +
    • 识别领域事件
    • +
    • 确定事件顺序
    • +
    • 识别命令和触发器
    • +
    • 识别聚合和实体
    • +
    • 划分限界上下文
    • +
    • 识别领域服务和资源库
    • +
    • 通过这些步骤,团队可以共同创建出反映业务领域的领域模型,为软件设计和开发提供指导。
    • +
    +]]>
    + + Technique + +
    儿童新型冠状病毒Omicron株和肺炎支原体混合感染重症肺炎1例 /2023/10/25/%E5%84%BF%E7%AB%A5%E6%96%B0%E5%9E%8B%E5%86%A0%E7%8A%B6%E7%97%85%E6%AF%92Omicron%E6%A0%AA%E5%92%8C%E8%82%BA%E7%82%8E%E6%94%AF%E5%8E%9F%E4%BD%93%E6%B7%B7%E5%90%88%E6%84%9F%E6%9F%93%E9%87%8D%E7%97%87%E8%82%BA%E7%82%8E1%E4%BE%8B/ @@ -3286,6 +3243,32 @@ Design + + 实时搜索中的防抖函数 + /2025/02/10/%E5%AE%9E%E6%97%B6%E6%90%9C%E7%B4%A2%E4%B8%AD%E7%9A%84%E9%98%B2%E6%8A%96%E5%87%BD%E6%95%B0/ + 在实现实时搜索功能时,通常会使用输入框的事件监听器来捕获用户的输入变化,并在输入变化时发送搜索请求。为了避免过多的请求导致服务器负担过重,通常会使用“防抖”(debounce)技术来控制请求的频率。

    +

    实现步骤

      +
    1. 监听输入框的变化:使用input事件监听器来捕获用户的输入变化。
    2. +
    3. 防抖处理:使用防抖函数来限制请求的频率。防抖函数会在用户停止输入一段时间后才发送请求。
    4. +
    5. 发送请求:在防抖函数中调用搜索请求。
    6. +
    +

    防抖函数示例

    以下是一个简单的防抖函数示例:

    +
    function debounce(func, wait) {
    let timeout;
    return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
    };
    }
    + +

    实现实时搜索

    假设你有一个输入框用于搜索患者:

    +
    <script>
    import { onMount } from 'svelte';
    import { patientsStore } from '$lib/stores/patients.svelte';

    let searchTerm = '';

    // 防抖函数
    function debounce(func, wait) {
    let timeout;
    return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
    };
    }

    // 搜索函数
    const searchPatients = debounce(async (term) => {
    if (term) {
    // 发送搜索请求
    const response = await fetch(`/api/search-patients?query=${term}`);
    const data = await response.json();
    patientsStore.mbglPatients = data;
    } else {
    // 清空搜索结果或恢复默认数据
    patientsStore.mbglPatients = [];
    }
    }, 300); // 300ms 的防抖时间

    // 监听输入框变化
    function handleInput(event) {
    searchTerm = event.target.value;
    searchPatients(searchTerm);
    }
    </script>

    <input type="text" placeholder="搜索患者..." on:input={handleInput} bind:value={searchTerm} />
    + +

    请求发送间隔

      +
    • 防抖时间:通常设置为 300ms 到 500ms 之间。这个时间足够让用户完成输入并减少不必要的请求。
    • +
    • 考虑用户体验:防抖时间过短可能导致过多请求,过长则可能让用户感到延迟。300ms 是一个常用的折中值。
    • +
    +

    通过这种方式,你可以实现一个高效的实时搜索功能,既能保证用户体验,又能减少服务器的负担。

    +]]>
    + + Technique + Web + +
    急诊与灾难医学--烧伤 /2024/01/25/%E6%80%A5%E8%AF%8A%E4%B8%8E%E7%81%BE%E9%9A%BE%E5%8C%BB%E5%AD%A6-%E7%83%A7%E4%BC%A4/ @@ -5519,6 +5502,32 @@ OCaml + + 领域驱动设计中的“聚合根” + /2025/03/13/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E7%9A%84%E2%80%9C%E8%81%9A%E5%90%88%E6%A0%B9%E2%80%9D/ + 在领域驱动设计(Domain-Driven Design,简称DDD)中,聚合根(Aggregate Root)是聚合(Aggregate)中的核心实体,是一个聚合的入口点和控制者,负责维护聚合内部的一致性和不变性条件。聚合是一组紧密相关的领域对象的集合,这些对象通过一定的业务规则绑定在一起,并被视为一个单元。

    +

    主要的作用如下:

    +
      +
    • 维护不变性:聚合根确保聚合内所有对象的一致性和不变性条件不被破坏。它负责封装与聚合相关的业务逻辑,保证聚合内的对象符合业务规则。
    • +
    • 管理生命周期:聚合根负责管理其内部对象的创建、修改和删除。它控制着聚合内部成员的生命周期,包括它们的创建、更新和删除。
    • +
    • 处理业务逻辑:聚合根负责处理与聚合相关的业务逻辑和操作,外部系统通过调用聚合根的方法来执行这些操作。它不仅是数据的容器,还负责封装与聚合相关的业务逻辑。
    • +
    +

    其具有以下特性:

    +
      +
    • 唯一入口:聚合根是聚合内部对象的唯一入口,外部系统只能与聚合根交互,而无法直接访问聚合内部的其他对象。这样可以避免外部系统直接修改聚合内的实体,确保聚合的一致性和业务逻辑的完整性。
    • +
    • 标识唯一性:每个聚合根都有一个全局唯一的标识符(ID),用以区分不同的聚合实例。
    • +
    • 事务边界:聚合根常常作为事务的边界,确保事务内的所有操作要么全部成功,要么全部失败,以此来维护数据的完整性。
    • +
    +

    用 F# 来描述,以订单管理为例,大概写一下:

    +
    type OrderStatus = 
    | New
    | Shipped
    | Delivered
    | Cancelled

    type OrderItem (productName: string, price: float, quantity: int) =
    do
    if quantity <= 0 then
    failwith "Quantity must be positive"

    member public this.ProductName = productName
    member public this.Price = price
    member public this.Quantity = quantity
    member public this.TotalPrice () = price * quantity

    type Order (id: int, customerName: string) =
    let mutable status = OrderStatus.New
    let mutable orderItems = []

    member public this.Id = id
    member public this.CustomerName = customerName
    member public this.Status = status
    member public this.OrderItems = orderItems

    member public this.AddItem (item: OrderItem, price: float, quantity: int) =
    if quantity <= 0 then
    failwith "Quantity must be positive"

    orderItems <- orderItems @ [OrderItem(item.ProductName, price, quantity)]

    member public this.ChangeStatus (status: OrderStatus) =
    this.Status <- status

    member public this.TotalPrice () =
    orderItems |> List.sumBy (fun item -> item.TotalPrice())

    member public this.GetTotalPrice () =
    orderItems |> List.sumBy (fun item -> item.TotalPrice())
    + +

    在这个例子中,Order 是聚合根,它通过 AddItem 方法来添加订单项,保证每个订单项符合业务规则。同时,聚合根 Order 还负责订单状态的管理,例如通过 ChangeStatus 方法来更新订单状态。OrderItem 是聚合内的一个实体,表示订单项,它通过 GetTotalPrice 方法来计算每个订单项的总价。外部系统只能通过 Order 聚合根来访问和操作订单项,而不能直接访问或修改 OrderItem

    +]]>
    + + Technique + Software Engineering + +
    关于 /about.html -- cgit v1.2.3