# 实现一个类型安全的 (f1, f2) -> f2(f1()) 函数 (December 29, 2024)

```python
def compose(first, second):
    return lambda: second(first())
```

有了这个函数，当你需要 **串联** 两个函数的时候，就可以很方便了，下面我举例一个使用场景

### 例子：序列化并用 bzip2 压缩
大家都知道 python 对象的序列化用`pickle.dumps`，压缩用`***.compress`（***替换成各种库，比如标准库的 gzip，bz2，lzma 都是这样）

前者是`(Any)->bytes`，后者是`bytes->bytes`，如果你要封装一个“序列化并压缩”的函数，你得这么写：

```python
def dumps(object: Any):
    serialized = pickle.dumps(object)
    compressed = bz2.compress(serialized)
    return compressed
```

当然这种一次性的代码一般会缩写成一行：

```python
def dumps(object: Any):
    return bz2.compress(pickle.dumps(object))
```

再加上反序列化的代码：

```python
def loads(data: bytes):
    return pickle.loads(bz2.decompress(data))
```

这样，再加上空行，相当于你代码加了8行。不能忍吧

用我们刚刚的`compose`函数，只要这样就行了：

```python
dumps = compose(pickle.dumps, bz2.compress)
loads = compose(bz2.decompress, pickle.loads)
```

只要两行，而且清清楚楚。

### 但是类型呢？
我们刚刚的`compress`函数，没有类型注解，你可能觉得这很简单，这样就行了：

```python
def compose(first: Callable, second: Callable):
    return lambda: second(first())
```

<img src="/nlark/yuque/0/2024/png/26070246/1735472852005-53c15b9f-7177-4f9d-9212-fd91054c0007.png" width="564" title="" crop="0,0,1,1" id="u63f94d44" class="ne-image">

这样只能确保输入`compose`的两个参数都是可调用对象，但是并没有保证：

1. 第二个函数可以接受第一个函数的返回值为参数
2. `compose`后的函数的返回值与`second`的返回值相同

<img src="/nlark/yuque/0/2024/png/26070246/1735473023962-6e659472-871f-4033-94a7-c2b80940befa.png" width="454.85714285714283" title="" crop="0,0,1,1" id="u102fb97e" class="ne-image">

我们希望`h()`的类型推断是`Literal[42]`或者`int`对吧

### 泛型
Python 有勉强够用的泛型支持，而且早在 Python 3.12 就开始支持 typescript 式的更方便的声明泛型的方式：

[PEP 695: Type Parameter Syntax —— What's New In Python 3.12](https://docs.python.org/3/whatsnew/3.12.html#pep-695-type-parameter-syntax)

这么写就可以了：

```python
def compose[T1, T2](first: Callable[[], T1], second: Callable[[T1], T2]):
    return lambda: second(first())
```

你看我甚至返回值都不用标，`pyright`能推算出来返回的是`Callable[[], T2]`

<img src="/nlark/yuque/0/2024/png/26070246/1735473360737-9efec25a-13b0-4c23-baf4-d11174675677.png" width="896" title="" crop="0,0,1,1" id="u513e614b" class="ne-image">

你看，这样错误就能检查出来啦☀️

修复了`g`之后，类型也正确推断啦：

<img src="/nlark/yuque/0/2024/png/26070246/1735473423814-35d15f0e-6dbd-43a3-9b0f-48d14a5d6e22.png" width="490.2857142857143" title="" crop="0,0,1,1" id="ucf5f561d" class="ne-image">

### 如果 `first()`有参数呢
上面的这个例子用途不够广泛，因为它用于无参调用。而有时候我很需要这个 compose 出来的函数，仍然有着原来的 first 函数的参数定义

其实我们需要的就是一个“输入 first 的参数，返回 second 的返回值”这么一个函数对吧，那就简单了：

```python
def compose[T1, T2, **P](first: Callable[P, T1], second: Callable[[T1], T2]) -> Callable[P, T2]:
    return lambda *args, **kwargs: second(first(*args, **kwargs))
```

我只是把`Callable[[], T1]`改成了`Callable[P, T1]`，加上明确最后返回`Callable[P, T2]`就完事儿啦。

当然，如果你不喜欢 lambda，你可以这样写一个函数，这样甚至能少写`-> Callable[P, T2]`这个返回值声明

<img src="/nlark/yuque/0/2024/png/26070246/1735474084051-9b534f1c-94c8-47f8-b8ae-1a609e0ff8b4.png" width="760" title="" crop="0,0,1,1" id="u77d3f8f7" class="ne-image">

> 右边的返回值声明是它自己生成的，这个显示是 IDE 的 inlay hint，不是我打上去的
>

这样你就可以让这个 compose 出的函数具有和`first`一样的入参签名啦：

<img src="/nlark/yuque/0/2024/png/26070246/1735474549522-11976e83-e95f-40d6-976f-de24f0a3528e.png" width="1012" title="" crop="0,0,1,1" id="u3c16493c" class="ne-image">

这个例子其实挺没用的，但是我一时间想不到好例子。总之使用场景就是，可能前面那个函数有很多参数，而且你是会用到这些参数的。

### 后话：为什么做这个
因为我想要一个类型安全的`json.dumps`和`json.loads`。原本的`json.dumps`不是`(Any)->str`嘛，我希望是`(Input)->str`，而原本的`json.loads`不是`(str|bytes)->Any`嘛，我要的是`(str|bytes)->Output`，其中`Input`和`Output`是我两个预先定义好的`TypedDict`

其中 loads 很简单，直接用它的`validate_json`就好了：

<img src="/nlark/yuque/0/2024/png/26070246/1735476062616-0dd44570-56b5-41a2-b018-09d74087e4c2.png" width="792" title="" crop="0,0,1,1" id="u253133af" class="ne-image">

这个入参签名和补全，太完美了

<img src="/nlark/yuque/0/2024/png/26070246/1735477144398-e9119ca7-5ab3-4757-873f-8a4417e8ffb9.png" width="638.8571428571429" title="" crop="0,0,1,1" id="ucb84fdea" class="ne-image">

输出类型也正常

本来以为dumps也可以这么简单

<img src="/nlark/yuque/0/2024/png/26070246/1735475811213-9e1fc51a-fb84-47af-8eae-a2c925064ea7.png" width="673.7142857142857" title="" crop="0,0,1,1" id="u78d9bee6" class="ne-image">

但是`pydantic`的 TypeAdapter 实例的 dump_json 居然是返回 bytes 的！居然不是和 BaseModel 一样地返回 str！简直是大坑

不过我刚刚翻了下[文档](https://docs.pydantic.dev/latest/concepts/type_adapter/)，其实有提到的：

<img src="/nlark/yuque/0/2024/png/26070246/1735476611361-33108670-01b5-4943-b5f6-c8745e02cc70.png" width="1814.2857142857142" title="" crop="0,0,1,1" id="ufcb7e104" class="ne-image">

那就只好自己转成 str 咯，虽然就是`.decode()`的事情，但我希望它仍然是一个`dumps()`函数的形式，而且我在不同场景下需要不同的`dumps`的参数，最后写成了这样：

```python
dumps = compose(Output.dump_json, bytes.decode)
```

<img src="/nlark/yuque/0/2024/png/26070246/1735476936196-9039f4b9-6822-42f1-ae5e-e688ced9ce86.png" width="760.5714285714286" title="" crop="0,0,1,1" id="uef965e14" class="ne-image">

完美~