第三章:语法基础——变量声明与类型系统

写WDL和写普通脚本有个很大的不同——你得先跟引擎说清楚:我要用什么东西了,这东西长什么样。

我个人习惯,把WDL的类型系统看成是“给数据贴标签”。标签贴对了,引擎才知道怎么处理这份数据。标签贴错了,运行时就等着报错吧。

3.1 变量声明的基本语法

WDL的声明语法很直观,格式就这一条:

(类型) (变量名) = (表达式或值)

举个例子:

Int count = 5
String name = "sample"
File ref_fasta = "/reference/hg38.fa"

这里要注意三点:

  • 类型和变量名之间用空格隔开。别漏了。
  • 等号两边有无空格都行。但为了可读性,我建议统一加空格。
  • 每行一个声明。WDL不支持在一行里声明多个变量。
技巧:声明时最好同时初始化。虽然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
注意:WDL中Float和Int不会自动转换。如果你把一个Int传给期望Float的参数,引擎会报类型不匹配。我建议显式转换: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
核心概念:Array在WDL中常用于批处理。比如你有100个样本,把它们的bam文件放在一个Array[File]里,然后通过scatter并行处理。这是WDL并行能力的基石。

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  # 如果不存在
关键注意:使用可选类型时,一定要检查是否为null再使用。直接对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 变量声明与类型系统概览 变量声明 声明语法 类型 + 名称 + 值 基本数据类型 Int / Float / String / File / Boolean 复合数据类型 Array / Map / Pair 可选类型 (?) null 安全 类型安全与推断 💡 关键总结 • 显式声明类型,代码可读性更高 • 可选类型用于处理可能缺失的数据,记得检查 null • File 和 String 是两回事,别搞混

这张图把变量声明到类型安全的脉络串起来了。你写WDL的时候,按这个流程走一遍,基本不会出大问题。

我的小习惯:每写一个变量声明,我都问自己三个问题:1)类型对不对?2)要不要可选?3)初始值有没有?三问之后,代码基本稳了。