Eli*_*igo 5 python io functional-programming python-3.x
我们应该如何用 Python 编写文件,同时保持功能纯粹?通常我会做这样的事情
from typing import Iterable
from io import IOBase
def transform_input(input_lines: Iterable[str]) -> Iterable[str]: ...
def print_pack(input_lines: Iterable[str], output: IOBase) -> None:
for line in input_lines:
print(line, file=output)
def main(*args, **kwargs):
# Somehow we get a bunch iterables with strings and a list of output streams
packs_of_input = ... # Iterable[Iterable[str]]
output_streams = ... # Iterable[IOBase]
packs_to_print = map(transform_input, packs_of_input)
for pack, output_stream in zip(packs_to_print, output_streams):
print_pack(pack, output_stream)
Run Code Online (Sandbox Code Playgroud)
我们可以for用这样的东西替换 -loop
list(map(lambda pack_stream: print_pack(*pack_stream), zip(packs_to_print, output_streams))
Run Code Online (Sandbox Code Playgroud)
但这只会让打印看起来像是功能性完成的。问题是这print_pack不是一个纯函数,它的所有努力都会产生副作用并且什么也不返回。我们应该如何编写文件并保持功能纯(或几乎纯)?
本质上,在Python中,你需要在某个地方有一个不纯的函数,所以在这个应用程序中没有办法拥有100%的纯函数。最后还需要做一些IO,而IO是不纯粹的。
然而,您可以做的是尝试将应用程序中的特定抽象层表示为纯函数,并隔离另一个模块中产生实际副作用的部分。您可以通过一种特殊的方式非常轻松地完成此操作,例如,通过在主代码中将要写入的文件的内容累积为纯不可变数据结构。然后,您的副作用代码的大小就可以减小,因为它所需要做的就是将字符串转储到文件中。
我们可以向 Haskell 寻求一种更严格的方法,用纯函数和数据结构来纯粹地表示副作用操作的全部威力——使用 Monad 抽象。本质上,Monad 是您可以绑定回调的东西,以创建一系列基于纯函数的有效计算。对于 IO monad,一旦您从函数返回 IO 值,Haskell 运行时就会负责实际执行副作用main——因此您编写的所有代码在技术上都是纯函数,并且运行时会负责 IO。
Effect库(免责声明:我写的)基本上在 Python 中实现了某种风格的 Monad(或非常接近 monad 的东西)。这使您可以将任意 IO(和其他副作用)表示为纯对象和函数,并将这些效果的实际性能放在一边。因此,只要您拥有一种相对简单的副作用函数库,您的应用程序代码就可以是 100% 纯净的。
因此,例如,要实现一个使用 Effects 将行列表写入文件的函数,您需要执行以下操作:
@do
def write_lines_to_file(lines, filename):
file_handle = yield open_file(filename)
for line in lines:
yield write_data(file_handle, line)
# alternatively:
# from effect.fold import sequence; from functools import partial
# yield sequence(map(partial(write_data, file_handle), lines))
yield close_file(file_handle)
Run Code Online (Sandbox Code Playgroud)
Effect 库提供了这个特殊的do装饰器,让您可以使用命令式语法来描述纯粹有效的操作。上面的函数与这个函数等效:
def write_lines_to_file(lines, filename):
file_handle_eff = open_file(filename).on(
lambda file_handle:
sequence(map(partial(write_data, file_handle), lines)).on(
lambda _: close_file(file_handle)))
Run Code Online (Sandbox Code Playgroud)
它们都假设存在三个函数:open_file、write_data 和 close_file。假定这些函数返回 Effect 对象,这些对象表示执行这些操作的意图。最后,效果本质上是一种意图(对所请求的操作的一些透明描述),以及当该操作的结果完成时要运行的一个或多个回调。有趣的区别是 write_lines_to_file实际上并不将行写入文件;而是将行写入到文件中。它只是返回一些表示将某些行写入文件的意图。
要实际执行此效果,您需要使用该sync_perform函数,例如sync_perform(dispatcher, write_lines_to_file(lines, filename)). 这是一个不纯的函数,它实际上运行执行者以实现有效计算的纯表示所使用的所有效果。
我可以详细介绍如何实现 open_file、write_data 和 close_file,以及“dispatcher”参数的详细信息,但实际上https://effect.readthedocs.org/上的文档可能是此时可以参考的正确内容。
我还在 Strange Loop 上发表了关于 Effect 及其实现的演讲,您可以在 YouTube 上观看:https://www.youtube.com/watch? v=D37dc9EoFus
值得注意的是,Effect 是一种保持代码纯粹功能性的相当严厉的方法。通过采用“功能核心/命令式 shell”方法并尽力将大部分代码编写为纯函数并最小化有效代码,您可以在实现可维护代码方面取得长足进步。但如果您对更严格的方法感兴趣,我认为 Effect 很好。我的团队在生产中使用它,它提供了很多帮助,尤其是它的测试 API。