第三章:语法基础——变量声明与类型系统
写WDL和写普通脚本有个很大的不同——你得先跟引擎说清楚:我要用什么东西了,这东西长什么样。
我个人习惯,把WDL的类型系统看成是“给数据贴标签”。标签贴对了,引擎才知道怎么处理这份数据。标签贴错了,运行时就等着报错吧。
3.1 变量声明的基本语法
WDL的声明语法很直观,格式就这一条:
(类型) (变量名) = (表达式或值)
举个例子:
Int count = 5
String name = "sample"
File ref_fasta = "/reference/hg38.fa"
这里要注意三点:
- 类型和变量名之间用空格隔开。别漏了。
- 等号两边有无空格都行。但为了可读性,我建议统一加空格。
- 每行一个声明。WDL不支持在一行里声明多个变量。
3.2 基本数据类型详解
WDL的数据类型不算多,但每个都有自己的脾气。下面我逐一说说。
Int — 整数
就是整数,正负都行,范围?64位有符号整数,放心用。
Int year = 2025
Int negative = -42
Int sum_result = 100 + 200
我曾经犯过一个低级错误:把样本数量写成了字符串。结果下游任务做数学运算时直接崩溃。嗯,从那以后我对数字类型格外敏感。
Float — 浮点数
带小数点的数。注意精度,别指望它做金融计算。
Float coverage = 30.5
Float ratio = 0.85
Float pi_approx = 3.14159
Float(my_int_value)。
String — 字符串
文本数据。用双引号包裹,这是硬性要求。
String sample_id = "NA12878"
String path = "/data/raw/WGS_001_R1.fastq.gz"
String description = "This is a long description\nwith a newline"
一个常见的坑:路径拼接。千万别用加号直接拼字符串,像这样:
String base_dir = "/data/samples/"
String full_path = base_dir + sample_id + ".bam" # WDL不支持!
正确的做法是用字符串插值,后面会讲到。
File — 文件路径
这绝对是WDL里最重要的类型之一。它代表一个文件路径,可以是本地的,也可以是云存储上的。
File input_bam = "/path/to/sample.bam"
File reference_fasta = "/reference/hg38.fa"
File index_file = reference_fasta + ".fai" # 这里的加号是字符串拼接?不是!
等等,上面第三个例子你可能会疑惑。实际上,File类型不支持直接用加号拼接。正确的做法是用字符串操作:
File index_file = reference_fasta + ".fai" # 会报错!
# 正确方式
String ref_str = "${reference_fasta}"
File index_file = ref_str + ".fai"
我在项目中遇到过最典型的错误是:把File当作字符串处理,试图用加号拼接路径。记住,File和String是两回事。
Array — 数组
数组是一组相同类型的数据。声明时要指定元素类型。
Array[String] samples = ["NA12878", "NA12879", "NA12880"]
Array[Int] depths = [30, 40, 50, 35]
Array[File] bam_files = ["sample1.bam", "sample2.bam", "sample3.bam"]
数组的索引从0开始。访问元素用方括号:
String first_sample = samples[0] # "NA12878"
Int second_depth = depths[1] # 40
Map — 映射(字典)
键值对结构。键和值的类型都要声明。
Map[String, Int] sample_depth = {
"NA12878": 35,
"NA12879": 42,
"NA12880": 38
}
访问Map用方括号,键作为索引:
Int depth_of_12878 = sample_depth["NA12878"] # 35
说实话,Map我用得不多。大部分场景用Array就够了。但在需要快速查找的场景下,Map确实更方便。
Pair — 对偶
两个值的组合,两个值可以是不同类型。
Pair[String, Int] sample_info = ("NA12878", 35)
String sample_name = sample_info.left
Int sample_depth = sample_info.right
我个人觉得Pair有点鸡肋。能用的时候基本都能用Array或Map替代。但有时候确实省事,比如返回两个关联值。
3.3 类型系统速查表
下面这张表总结了所有基本类型,方便你速查:
| 类型 | 描述 | 示例声明 | 常见用途 |
|---|---|---|---|
| Int | 64位有符号整数 | Int count = 100 |
计数、索引、大小 |
| Float | 双精度浮点数 | Float ratio = 0.85 |
比例、阈值、度量 |
| String | UTF-8文本 | String id = "s001" |
标识符、路径片段 |
| File | 文件路径 | File bam = "x.bam" |
输入/输出文件 |
| Boolean | 真/假 | Boolean flag = true |
条件控制 |
| Array[T] | 元素为T的数组 | Array[String] arr = [] |
批量数据、多样本 |
| Map[K,V] | 键值为K-V的映射 | Map[String,Int] m = {} |
字典式查找 |
| Pair[L,R] | 左右两个值 | Pair[String,Int] p = ("a",1) |
返回两个关联项 |
3.4 可选类型
WDL还支持一种特殊的类型修饰:可选类型。用问号表示:
String? maybe_name = "exists"
String? empty_name = null
Int? maybe_count = null
可选类型的意思是“这个变量可能没值”。当你需要处理“可能缺失”的数据时很有用。
我曾在一个生产环境里遇到过这种场景:某个样本的质控报告可能不存在(如果质控失败)。如果不用可选类型,你还得额外传一个标志位。用可选类型就清爽多了:
File? qc_report = "/path/to/report.html" # 如果存在
# 或者
File? qc_report = null # 如果不存在
if (defined(qc_report)) {
String report_path = select_first([qc_report])
}
3.5 类型安全与类型推断
WDL是强类型语言。这意味着:
- 声明了Int就不能赋String
- 类型转换必须显式
- 函数参数类型必须匹配
但是,WDL也支持类型推断。如果你在声明时直接赋值,引擎会自动推断类型:
# 以下写法等价
Int x = 5
# 也可以写成
x = 5 # 推断为Int
我个人的建议是:显式声明类型。虽然推断很智能,但显式声明让代码的可读性更好,别人一看就知道变量是干啥的。
3.6 本章知识结构
下面这张图总结了变量声明和类型系统的核心脉络:
这张图把变量声明到类型安全的脉络串起来了。你写WDL的时候,按这个流程走一遍,基本不会出大问题。