This repository serves as a demonstration of using the new T programming lanaguage created by Bruno Rodrigues to orchestrate a data simulation pipeline. The original version of the pipeline was built with the {targets} R package and featured in the R/Pharma 2023 workshop Leveraging {targets} and {crew} to simulate clinical trials. You can view the workshop example {targets} pipeline at the wlandau/rpharma2023-pipeline repository.
Note: The T lanaguage has been designed to be compatible with AI-assisted development. To put this claim to the test, I used Claude Opus 4.7 through Open Router and opencode inside Visual Studio Code to assist wtih the conversion process. You can view the entire transcript of my prompts in the agent_session_notes/session-full-t-pipeline-dev.md document in this repository. From my initial prompt to completion, the total cost was approximately $7.00 (still less than the typical cost of a take-out meal!)
- Install the Nix package manager following the appropriate vignette from the
{rix}documentation site:
- Clone this repository to your computer and enter the reproducible environment:
nix develop- Run the analysis:
t run src/pipeline.tThis will generate a Quarto report based on the template report.qmd in this repository.
src/pipeline.t— Main T pipeline (port of the legacy targets DAG)report.qmd— Quarto report consuming theresultsnodetargets_legacy_code/— Original Rtargetsproject (copied from aforementinoed workshop repository, and not version-controlled here)data/— Input data files (read-only)outputs/— Generated artifacts (e.g.results.csvwritten viapipeline_copy)pipeline-output/— Local mirror of built Nix artifacts (created bypipeline_copy())tests/— Test files
Legacy target (_targets.R) |
T node (src/pipeline.t) |
|---|---|
tar_map_rep(simulations) |
sim_strong_700, sim_strong_800, sim_null_700, sim_null_800 (R nodes) |
tar_target(results) |
simulations (bind_rows) + results (group/summarize) |
tar_quarto(report) |
report (Quarto node rendering report.qmd) |
Scale is controlled by the seq_len(...) literals inside each sim_*
node (default: 100 sims/scenario). Bump them to 10 000 to match the
legacy 25 batches \u00d7 400 reps configuration.
After a successful build, the rendered HTML is at:
pipeline-output/report/artifact/report.html
Open it in a browser to view the results table.
This project was ported from a targets pipeline (see wlandau/rpharma2023-pipeline). The notes below capture the steps and pitfalls encountered during the migration, so future ports go faster.
Before writing any T code, map out the legacy DAG on paper:
- Targets (
tar_target,tar_map_rep,tar_quarto, …): each becomes one or more T nodes. - R functions in
R/: these get inlined intorn(...)node bodies, or — for larger projects — moved tosrc/R/*.Rand pulled in withrn(script = "..."). - Scenarios / branching:
tar_map_reppatterns translate to explicit per-scenariorn()nodes, one per branch. T pipelines are more verbose here but the DAG stays auditable. - Quarto reports:
tar_quarto(report)becomesnode(script = "report.qmd", runtime = Quarto).
Declare every R package the legacy pipeline library()-s into
[r-dependencies]:
[r-dependencies]
packages = ["dplyr", "tibble", "arrow", "jsonlite"]arrowis required for any R node that exchanges DataFrames with T viaserializer = ^arrow.jsonliteis pulled in by T's R bridge for cross-language metadata.- For Quarto reports, add
quartoandwhichto[additional-tools].
After editing tproject.toml, run t update and re-enter
nix develop so the new packages are on PATH and the Quarto tlang
filter is symlinked into _extensions/.
The fastest path is one node at a time. Build often, fix the first error, repeat. The list below documents the syntactic gotchas we hit — all of them produced cryptic error messages that take a moment to unpack.
-- Wrong: T tries to parse R as T expressions
sim_node = rn(command = { lm(y ~ x, data) })
-- Right: <{ ... }> is a RawCode block treated as opaque text
sim_node = rn(command = <{ lm(y ~ x, data) }>)-- Wrong: TypeError ("String literals are not allowed for `serializer`")
rn(command = <{ ... }>, serializer = "arrow")
-- Right
rn(command = <{ ... }>, serializer = ^arrow)The published docs sometimes show "arrow"; the T parser in version
0.52.0 rejects strings. The error message tells you exactly what
symbol to use.
-- Wrong: T complains a single strategy is ambiguous
simulations = node(
deserializer = ^arrow,
command = bind_rows(sim_a, sim_b, sim_c, sim_d)
)
-- Right
simulations = node(
deserializer = [
sim_a: ^arrow, sim_b: ^arrow, sim_c: ^arrow, sim_d: ^arrow
],
command = bind_rows(sim_a, sim_b, sim_c, sim_d)
)T's lexical analyzer scans <{ ... }> blocks for identifiers that
match other pipeline nodes and wires them as dependencies. If your R
code has results <- do.call(...) and your pipeline also has a node
called results, T thinks the R node depends on results — creating
a phantom cycle. Rename the local R variable (out, df_local, …).
Comments (-- or #) and string literals inside RawCode are stripped
by the analyzer, so node names mentioned in comments are safe.
-- Wrong: T expects $col on the LHS of mutate/summarize too
|> mutate(reject = $p_value < 0.05)
|> summarize(success = mean($reject))
-- Right
|> mutate($reject = to_integer($p_value < 0.05))
|> summarize($success = mean($reject, na_rm = true))Also worth knowing: arrange() takes one column and an optional
direction. arrange($a, $b) is interpreted as "sort by $a with
direction $b" and fails. Use arrange($a, "desc"), not
arrange($a, $b).
Quarto report wiring is mostly implicit, with two non-obvious details.
T does a sed substitution of read_node("name") over the entire
.qmd source, including prose. Don't mention read_node("results")
in markdown paragraphs — it will be rewritten to a Nix-store path in
the rendered output. Use a different phrasing in the prose ("the
results node", with backticks but without parentheses).
After substitution, the chunk literally contains a string like
"/nix/store/.../results/artifact". To get the DataFrame back, wrap
in deserialize(...):
results = deserialize(read_node("results"))
results
For R chunks, the T R helper package
provides a real read_node() function that handles deserialization
implicitly — but pure {t} chunks need the explicit deserialize call.
Two debugging fallbacks that proved more reliable than read_log():
-
Inspect
_pipeline/pipeline.nix— T-level errors that occur during Nix derivation generation get baked into this file as literal error strings.rg Error _pipeline/pipeline.nixsurfaces them before Nix even tries to build. -
Read the error artifact directly — when a node fails inside its sandbox, the error is serialized to
/nix/store/<hash>-pipeline_output/<node>/artifact. The file is mostly binary, but the human-readable error message appears as plain text within it:cat /nix/store/<hash>-pipeline_output/<failed_node>/artifact # or strings /nix/store/<hash>-pipeline_output/<failed_node>/artifact
The
pipeline_outputpath is printed byt runon a failed build.
build_pipeline(p) -- builds all nodes into the Nix store
pipeline_copy() -- mirrors artifacts into ./pipeline-output/Without pipeline_copy(), every artifact (including the rendered HTML
report) lives only in /nix/store/... and is read-only. pipeline_copy()
makes them browsable from the project root.
Dependencies are managed declaratively via tproject.toml.
To add a new dependency:
- Add it to the
[dependencies]section oftproject.toml:[dependencies] my-pkg = { git = "https://github.com/user/my-pkg", tag = "v0.1.0" }
- Run
nix develop— the package is automatically fetched - Commit
tproject.toml
No imperative install commands — flake.nix reads tproject.toml directly.
This project includes support for the T Language Server (LSP).
- Configure your editor following the Editor Support Guide.
- Always launch your editor from within the
nix developenvironment (or usedirenv).
Once active, you'll get autocompletion for T functions, variables, and DataFrame columns (via $).
MIT