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 给它加了两个重要能力:自动化阶段管理和数据流驱动。

我的习惯:每个 process 只做一件事。比如“比对”、“排序”、“去重”分开写,便于调试和重用。

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 输入声明的常见模式

我总结了三种最常见的输入模式,你直接照着用就行:

  1. 单输入,单输出样式—— 适合转化类 process,比如压缩、解压、格式转换。
  2. 多输入,单输出样式—— 适合合并类 process,比如将多个样本的统计结果汇总。
  3. 动态输入样式—— 配合 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 个时,简直噩梦。所以,命名吧,朋友。

核心要点:输入输出定义决定了数据流的拓扑。一个好的命名策略 + 清晰的 process 职责划分,能让你的 workflow 像流水线一样一目了然。

3.6 process 与 workflow 的关系总结

现在我们把整个流程定义的基础逻辑画成一张图。这张 SVG 展示了 process、workflow 和输入输出之间的层级关系:

Nextflow 流程定义基础结构 Workflow(工作流) Process 定义 input: val, path, file, env script: shell / native 命令 output: stdout, path, emit 数据流通道(Channel) process 通过 channel 输入输出,workflow 编排顺序

看到没?workflow 在最上层,负责流程“编排”;process 在中间,包含 input/script/output 三个区;最下面是 channel 通道,负责数据流转。这种分层结构是 Nextflow 设计的精髓——你改 workflow 不影响 process 实现,改 process 不影响 workflow 逻辑。

个人经验:我团队里有一个大型 RNA-seq 流程,有 30 多个 process。最开始写在同一个文件里,后来按照功能拆成 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 里数据流是否一目了然”。下个章节我们聊版本控制的实际策略——到时候这些基础定义会派上大用场。