Modules
BioLang's module system lets you organize code into reusable units. Modules are loaded
with import statements, support selective imports, and use pub
visibility to control what is exported.
Import Syntax
The import statement brings names from a module into the current scope:
# Import specific names
import { fastq } from "bio/io"
import { align, index_reference } from "bio/alignment"
import { gc_content, reverse_complement } from "bio/sequence"
# Import the entire module as a namespace
import "bio/io" as io
let reads = io.fastq("sample.fq.gz")
# Import all exported names (use sparingly)
import * from "bio/stats"
# Import from a local file
import { my_function } from "./helpers.bl"
import { PipelineConfig } from "../config.bl"
Module Resolution
BioLang resolves module paths in the following order:
-
Relative paths — paths starting with
./or../are resolved relative to the importing file. The.blextension is added automatically. -
Standard library — paths like
"bio/io"resolve to the built-in standard library modules. -
Installed plugins — module names that match installed plugins
in
~/.biolang/plugins/are loaded from the plugin system. -
BIOLANG_PATH — directories listed in the
BIOLANG_PATHenvironment variable are searched last.
# Standard library module
import { csv } from "io" # Built-in I/O
# Bio-specific standard library
import { fasta } from "bio/io" # Bio file I/O
import { gc_content } from "bio/seq" # Sequence operations
# Relative import
import { helper } from "./utils.bl" # Same directory
import { config } from "../config.bl" # Parent directory
# Plugin import
import { run_blast } from "blast" # Plugin: ~/.biolang/plugins/blast/
Creating Modules
Any .bl file is a module. Use pub to mark functions, types,
and variables as part of the module's public API:
# File: qc.bl
# Public — accessible to importers
pub fn check_quality(reads, min_q = 20) {
let passed = reads |> filter(|r| mean_phred(r.quality) >= min_q)
let fail_rate = 1.0 - len(passed) / len(reads)
{
passed: passed,
total: len(reads),
fail_rate: fail_rate
}
}
pub fn check_contamination(reads, reference) {
let unmapped = reads |> filter(|r| !r.is_mapped)
let contamination_rate = len(unmapped) / len(reads)
{
unmapped_count: len(unmapped),
rate: contamination_rate
}
}
# Private — internal helper, not exported
fn compute_threshold(scores) {
let mu = mean(scores)
let sd = stdev(scores)
mu - 2.0 * sd
}
pub let VERSION = "1.0.0"
Using the module:
# Import specific exports
import { check_quality, check_contamination } from "./qc.bl"
let qc = check_quality(reads, min_q = 30)
print(f"Pass rate: {1.0 - qc.fail_rate}")
# compute_threshold is not accessible — it is private
Module Namespacing
# Import as namespace to avoid name collisions
import "./qc.bl" as qc
import "./analysis.bl" as analysis
let quality = qc.check_quality(reads)
let results = analysis.run_analysis(reads)
# Useful when two modules export the same name
import "bio/stats" as bio_stats
import "math/stats" as math_stats
let bio_mean = bio_stats.mean(quality_scores)
let math_mean = math_stats.mean(test_scores)
Re-exports
Modules can re-export items from other modules to create a unified public API:
# File: bio/mod.bl — umbrella module
pub import { fasta, fastq, read_fasta, read_fastq } from "bio/io"
pub import { gc_content, reverse_complement, translate } from "bio/seq"
pub import { align, index_reference } from "bio/alignment"
pub import { call_variants, filter_variants } from "bio/variant"
# Now consumers can import everything from one place:
# import { read_fasta, gc_content, align } from "bio"
Standard Library Modules
BioLang includes a comprehensive standard library organized into namespaces:
| Module | Contents |
|---|---|
io | read_text, write_text, csv, write_csv, tsv, write_tsv, read_json, write_json |
bio/io | fasta, fastq, read_fasta, read_fastq, bam, read_bam, vcf, read_vcf, bed, read_bed, gff, read_gff |
bio/seq | gc_content, reverse_complement, transcribe, translate, kmers, find_motif |
bio/align | align, index_reference, pileup, coverage |
bio/variant | call_variants, filter_variants, annotate, merge_vcf |
bio/interval | interval, overlaps, merge_intervals, intersect, subtract |
math | abs, ceil, floor, round, sqrt, pow, log, log2, log10, exp, sin, cos, pi |
stats | mean, median, stdev, variance, cor, ttest, chi_square, fisher_exact |
string | upper, lower, trim, split, join, replace, contains, starts_with, ends_with |
collections | sort, reverse, unique, flatten, zip, enumerate, chunk, window |
os | env, args, cwd, path_join, path_exists, glob, exec |
http | get, post, put, delete (for API integrations) |
Module Caching
Modules are cached by their canonical file path. If the same module is imported from multiple files, it is loaded and evaluated only once. Circular imports are detected and rejected at load time:
# File: a.bl
import { helper } from "./b.bl" # Loads and caches b.bl
# File: c.bl
import { helper } from "./b.bl" # Uses cached version of b.bl
# Circular import — ERROR:
# File: x.bl
# import { foo } from "./y.bl"
# File: y.bl
# import { bar } from "./x.bl" # Error: circular import detected
Plugin Modules
Plugins installed in ~/.biolang/plugins/ are automatically available as
importable modules:
# Install a plugin
# $ bl add blast
# Import from the plugin
import { blastn, blastp, make_db } from "blast"
let results = blastn(
query = "query.fasta",
database = "nt",
evalue = 1e-10
)
# List available plugins
# $ bl plugins
Conditional Imports
# Import only if available
let plotting = try { import "plot" } catch _ { None }
if plotting != None {
plotting.scatter(x_values, y_values, title = "Results")
} else {
print("Plotting not available — install with: bl add plot")
}
# Feature-gated imports
import { read_bam } from "bio/io"
let has_samtools = try { import "samtools" } catch _ { None }
fn sort_bam(path: String) -> Result[String] {
match has_samtools {
Some(st) => st.sort(path),
None => Err("samtools plugin required — run: bl add samtools")
}
}
Project Structure
A typical BioLang project organizes modules by domain:
my-analysis/
main.bl # Entry point
config.bl # Configuration
lib/
qc.bl # Quality control
alignment.bl # Alignment pipeline
variant.bl # Variant calling
report.bl # Report generation
data/
samples.csv
results/
# main.bl
import { load_config } from "./config.bl"
import { run_qc } from "./lib/qc.bl"
import { run_alignment } from "./lib/alignment.bl"
import { call_and_filter } from "./lib/variant.bl"
import { generate_report } from "./lib/report.bl"
let config = load_config("config.yaml")
let samples = csv("data/samples.csv")
for sample in samples {
let qc = run_qc(sample, config.qc)
let aligned = run_alignment(sample, config.alignment)
let variants = call_and_filter(aligned, config.variant)
generate_report(sample, qc, variants, "results/")
}