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:

  1. Relative paths — paths starting with ./ or ../ are resolved relative to the importing file. The .bl extension is added automatically.
  2. Standard library — paths like "bio/io" resolve to the built-in standard library modules.
  3. Installed plugins — module names that match installed plugins in ~/.biolang/plugins/ are loaded from the plugin system.
  4. BIOLANG_PATH — directories listed in the BIOLANG_PATH environment 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:

ModuleContents
ioread_text, write_text, csv, write_csv, tsv, write_tsv, read_json, write_json
bio/iofasta, fastq, read_fasta, read_fastq, bam, read_bam, vcf, read_vcf, bed, read_bed, gff, read_gff
bio/seqgc_content, reverse_complement, transcribe, translate, kmers, find_motif
bio/alignalign, index_reference, pileup, coverage
bio/variantcall_variants, filter_variants, annotate, merge_vcf
bio/intervalinterval, overlaps, merge_intervals, intersect, subtract
mathabs, ceil, floor, round, sqrt, pow, log, log2, log10, exp, sin, cos, pi
statsmean, median, stdev, variance, cor, ttest, chi_square, fisher_exact
stringupper, lower, trim, split, join, replace, contains, starts_with, ends_with
collectionssort, reverse, unique, flatten, zip, enumerate, chunk, window
osenv, args, cwd, path_join, path_exists, glob, exec
httpget, 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/")
}