3. 流程定义基础:process, workflow, 输入输出定义归纳总结
好了,从这一章开始,咱们正式进入 Nextflow 的“骨架”部分。说实话,我最初接触 Nextflow 时,感觉 process 和 workflow 有点绕——后来才发现,把这两个概念理解透,后面写流程就像搭积木一样简单。我带你从头捋一遍。
3.1 什么是 process?
process 是 Nextflow 里的最小执行单元。你可以把它看作一个“黑盒”:给输入,执行一段脚本,产生输出。我习惯把 process 想象成一个函数——只不过这个函数可以跑在集群上,还能自动处理数据分片。
一个最简的 process 长这样:
process sayHello {
input:
val name
output:
stdout
script:
"""
echo "Hello, $name!"
"""
}
这里 val name 定义了一个普通值类型的输入,stdout 捕获标准输出。你可能觉得这跟写 shell 差不多——嗯,本质上确实是把批处理封装成可复用的单元。但 Nextflow 给它加了两个重要能力:自动化阶段管理和数据流驱动。
3.2 什么是 workflow?
workflow 就是把这些 process 串起来的东西。它定义了数据怎么流动,哪个 process 先跑,哪个后跑。我曾经见过有人把所有逻辑塞进一个 process 里,然后说“Nextflow 不好用”……其实 workflow 才是灵魂。
一个简单的 workflow 示例:
workflow {
read_ch = channel.fromPath("data/*.fastq")
fastqc_ch = FASTQC(read_ch)
multiqc_ch = MultiQC(fastqc_ch.collect())
multiqc_ch.view()
}
看到了吗?channel.fromPath 创建通道,FASTQC 和 MultiQC 是之前定义的 process。workflow 里只写数据传递的逻辑,不写具体执行命令。这样当你想换某个工具时,只需改对应的 process 定义 —— 工作流的拓扑结构基本不变。
3.3 输入输出定义详解
输入输出是 process 的门面和屁股。定义得不好,数据流就会乱套。我整理了一张表,帮你快速对照不同输入输出类型的作用:
| 修饰符 | 类型 | 说明 |
|---|---|---|
val |
普通值 | 字符串、数字等,不产生文件传输 |
path |
文件路径 | 自动处理文件暂存、传输和清理 |
file |
文件对象(旧版) | 相当于 path,但推荐用 path |
env |
环境变量 | 传递环境变量,不经过文件系统 |
stdout |
标准输出 | 捕获脚本的标准输出流 |
emit |
命名输出 | 给输出起个名字,便于在 workflow 中引用 |
file 修饰符,结果 Nextflow 升级后报了一堆 deprecated 警告。现在官方推荐统一用 path。如果你还在用旧代码,建议趁早迁移。
3.4 输入声明的常见模式
我总结了三种最常见的输入模式,你直接照着用就行:
- 单输入,单输出样式—— 适合转化类 process,比如压缩、解压、格式转换。
- 多输入,单输出样式—— 适合合并类 process,比如将多个样本的统计结果汇总。
- 动态输入样式—— 配合
when或each指令,根据条件选择不同输入。
举个例子,多输入合并的 process:
process MergeCounts {
input:
path "counts_*"
output:
path "merged_counts.txt"
script:
"""
cat counts_* > merged_counts.txt
"""
}
这里用通配符 counts_* 接收多个文件。Nextflow 会把它们全部收集到 process 的工作目录下,然后执行 cat 合并。注意:如果文件量极大,这种简单通配符可能会造成临时目录爆炸 —— 我会建议配合 .collect() 或 .toList() 显式控制。
3.5 输出声明与数据流
输出也有讲究。最简单的就是在 output: 块里直接写 path "result.txt",然后 Nextflow 会自动把它变成通道。但如果你有多个输出,最好给它们命名:
process SplitFastq {
input:
path fastq
output:
path "read1.fastq", emit: read1
path "read2.fastq", emit: read2
script:
"""
seqtk sample -s100 $fastq 0.5 > read1.fastq
seqtk sample -s100 $fastq 0.5 > read2.fastq
"""
}
然后在 workflow 里就能这样用:
workflow {
input_ch = channel.fromPath("data.fastq")
split_ch = SplitFastq(input_ch)
split_ch.read1.view()
split_ch.read2.view()
}
你看,命名输出让代码可读性一下子提升了。我个人很不喜欢用 [0] [1] 这种方式索引输出 —— 特别是当 process 输出超过 3 个时,简直噩梦。所以,命名吧,朋友。
3.6 process 与 workflow 的关系总结
现在我们把整个流程定义的基础逻辑画成一张图。这张 SVG 展示了 process、workflow 和输入输出之间的层级关系:
看到没?workflow 在最上层,负责流程“编排”;process 在中间,包含 input/script/output 三个区;最下面是 channel 通道,负责数据流转。这种分层结构是 Nextflow 设计的精髓——你改 workflow 不影响 process 实现,改 process 不影响 workflow 逻辑。
qc.nf, align.nf, quant.nf,每个文件包含 3-5 个 process,然后用一个主 workflow.nf 引用它们。从此版本冲突少了一半。
3.7 几个常见错误
最后,说说我踩过的坑,给你省点时间:
- 忘记命名输出通道 —— 结果在 workflow 里只能用
[0]下标访问,代码可读性极差。我后来强制自己每个 process 都有emit:。 - 输入文件路径写死 —— 比如
input: path "/data/sample.fastq"。这样 process 只能处理单个文件,失去了 Nextflow 的并行优势。用channel.fromPath动态传递才是正道。 - 输出声明与实际文件不匹配 —— 比如 output 里写
path "output.txt",但脚本实际生成的是result.txt。Nextflow 不会帮你校验文件名,只按声明收集。我曾经因为这个丢过一个重要中间结果……嗯,后来加了个ls -l在脚本末尾调试。 - workflow 里直接写复杂的逻辑判断 —— 比如把 if-else 嵌套在 workflow 块里。建议把判断逻辑封装到 process 中,或者用
when条件来控制 process 执行与否。
好了,这一章的内容就是这些。我把最核心的基础概念给你拆解清楚了。你写的时候,多想想“这个 process 输入输出是否够清晰”、“workflow 里数据流是否一目了然”。下个章节我们聊版本控制的实际策略——到时候这些基础定义会派上大用场。