第3章 变量与数据类型:基础中的基础
说实话,变量和数据类型这玩意儿,学任何语言都绕不开。WDL 虽然是个领域特定的脚本语言,但它的类型系统设计得很实用,不会给你整什么抽象继承那种花活。你想想看,一个工作流脚本里跑的无非就是:这个文件是啥格式、那个参数是整数还是浮点数、样本列表怎么组织——搞清楚了这些,后面写流程就顺了。
我记得第一次用 WDL 写一个批量 RNA-seq 流程时,因为没注意 Array 的默认索引是从 0 开始,硬生生把样本顺序弄反了,结果重跑了好几个任务。嗯,从那以后我对数据类型就格外敏感。今天咱们就把 WDL 的变量声明和作用域这些底层规则掰开揉碎。
3.1 基本数据类型一览
WDL 支持的类型不算多,但足够覆盖绝大多数生信场景。我习惯把它们分成两组:基础标量和集合容器。
| 类型 | 含义 | 示例 |
|---|---|---|
String |
字符串 | "sample_01" |
Int |
整数(64位有符号) | 42 |
Float |
浮点数 | 3.14159 |
Boolean |
布尔值 | true / false |
File |
文件路径 | "gs://bucket/reads.fastq" |
Array[X] |
数组(元素类型为X) | ["a","b"] |
Map[K,V] |
映射表 | {"key": "val"} |
关键点:WDL 里 File 本质是字符串,但对 CROMWELL 引擎有特殊含义——它会自动处理文件上传、路径重写和校验。千万别把 File 当普通字符串去拼接,否则部署到云端会踩坑。
3.2 变量声明与赋值
WDL 的变量声明用 类型 变量名 的形式,感觉跟 TypeScript 有点像。不过有个细节:你可以在声明时给默认值,也可以在 workflow 或 task 里再动态赋值。
version 1.0
workflow my_workflow {
String greeting = "Hello"
Int count = 10
Float pi = 3.14
Boolean debug = false
# 文件类型声明,路径由外部 JSON 输入提供
File input_bam
# 数组声明
Array[String] sample_ids = ["S1", "S2", "S3"]
# 映射声明
Map[String, Int] sample_lanes = {"S1": 1, "S2": 2}
call my_task {
input:
msg = greeting,
times = count
}
}
我在项目里经常遇到新手把 Float 写成 float 小写——WDL 是大小写敏感的,首字母必须大写。你想想看,平台跑完任务发现参数类型不匹配,返回一个很模糊的报错,排查半天结果是大小写写错了,挺冤的。
3.3 类型推导与强制转换
WDL 大多数情况下类型是静态检查的,但有些场景会自动推导。比如 Array[String] 的默认值如果全是数字字符串,引擎不会自动转成 Array[Int]——必须显式声明。
我曾经踩过的坑:在给 Map[String, String] 赋值时,把 Int 值直接写了进去,WDL 编译器不会报错,但运行时 Cromwell 会报类型转换异常。所以写 Map 时建议统一 String 作为 value,需要数值再用 parseInt() 函数提取。
3.4 变量作用域规则
作用域这东西,搞不清楚就会写出诡异的 bug。WDL 的作用域比较简单:workflow 级别和task 级别,没有 block 作用域(比如 if 内部不会创建独立作用域)。但有一点容易混淆——call 语句中 input 块的变量名会覆盖外层同名变量。
version 1.0
workflow scope_demo {
String name = "global"
call task_a {
input:
name = "local" # 只在这里有效,不影响全局
}
# 这里 name 仍然是 "global"
call task_b {
input:
name = name # 显式传递全局变量
}
}
我个人习惯是:全局变量用全小写 + 下划线,task 的 input 变量用驼峰式,这样一眼就能看出变量的层级。别小看命名风格,它能省掉好多次排查作用域问题的功夫。
3.5 空值处理与可选类型
WDL 1.0 开始支持可选类型,用 ? 后缀表示变量可以为 null。这在基因组学里太常用了——比如一个样本可能没有肿瘤 BAM,只有正常 BAM,那么肿瘤 file 就应该是 File?。
workflow optional_example {
File? tumor_bam # 可空
File normal_bam # 必填
if (defined(tumor_bam)) {
call somatic_caller {
input:
tumor = select_first([tumor_bam]),
normal = normal_bam
}
} else {
call normal_only_pipeline {
input:
normal = normal_bam
}
}
}
小技巧:当你需要从一个可选变量中取值时,用 select_first([...]) 比 if 分支更简洁。因为 select_first 会返回数组里第一个非 null 的值,配合默认值使用效果很好。
3.6 实战中的数据类型陷阱
- Int vs Float 的除法运算:WDL 里两个
Int相除得到Float,但精度损失可能让你意外。我个人建议如果需要浮点结果,至少把一个操作数声明成Float。 - Array 索引越界:WDL 的
Array不会自动检查越界,运行时直接 crash。我习惯在访问索引前用length()判断一下。 - Map 键的类型限制:Map 的 key 只能是
String或Int,不能是File或Float。我曾经试过用File做 key,编译时就报错了。 - 文件路径中的空格:
File类型的值如果有空格,有些任务内部解析会出错。我建议在 workflow 外层就写一个 utility task 把路径用双引号保住。
3.7 知识体系一览
下面这张图把本章涉及的主要概念串了起来,你可以当复习提纲用。
3.8 小结与自检清单
- 类型首字母大写:
string不对,要写String。 - File 不等于 String:引擎会特殊处理,不要手动拼接路径。
- 可选类型用 ?:配合
defined()和select_first()处理 null。 - 作用域嵌套:call 的 input 可以屏蔽外层同名变量,但不会修改全局值。
- 类型推导受限:静态类型检查严格,该声明的类型别偷懒。
你如果能把上面这五条记牢,写 WDL 的时候 80% 的类型错误都能提前避免。我个人建议每次写新 workflow 前,先花 5 分钟把变量表列出来——类型、默认值、是否可选,画个小表格。这习惯帮我省过好几次通宵 debug 的夜。