-
Notifications
You must be signed in to change notification settings - Fork 0
Writing a Library
A Laplace library is a directory of Stan functions plus a small manifest. If you already keep a folder of functions you reuse, you are most of the way there: nothing has to be rewritten, moved, or duplicated.
This page covers the whole process, from an existing folder of .stan files to a library someone else can install with one command.
Take a directory of Stan function definitions that already work as ordinary Stan, and add one file called laplace.toml. It is now a Laplace package.
my-library/
kernels.stan
laplace.toml
All .stan and .laplacelib files in the directory are read, sorted by filename and concatenated, so you can split a large library across several files however you like. .stan files are passed through verbatim. .laplacelib files are the library dialect, covered in section 6.
name = "gaussian_process"
version = "0.1.0"
exports = ["rbf_cov", "matern32_cov", "latent"]name is what users type in pkg::func() calls and in their library { } block. It is independent of the directory name and of the repository name, and you can change it freely.
Choose it carefully, because it becomes part of every compiled symbol: gaussian_process::rbf_cov compiles to gaussian_process__rbf_cov. That means the name has to be a valid Stan identifier. Do not use hyphens. A repository can be called laplace-gaussian_processes, but the package inside it should be gaussian_process.
version should follow semantic versioning, since users depend on your library through ranges such as ^0.1.
exports lists the functions callable from outside the package.
Only functions named in exports can be called as pkg::func from another package or from a model. This is separate from documentation: a function with a full @laplace comment block that is missing from exports is still private, and a function in exports that has no doc comment is still callable.
Only exported functions get the pkg__ prefix in compiled output. Private helpers keep their original names, and Stan has a single flat function namespace, so if two packages in the same build both define a private softplus, the build fails with an error naming both packages. It does not silently emit two definitions.
Give private helpers distinctive names. A short prefix of your own is the simplest approach, which is why laplace-survival names its internals srv_expm1_div rather than expm1_div.
Every name in exports must actually exist in the package's source. If it doesn't, builds that use your library fail.
Documentation is a comment block directly above the function, opening with a line that reads @laplace:
// @laplace
// @brief Squared exponential (RBF) covariance matrix.
// @param x Vector of input locations.
// @param alpha Marginal standard deviation of the GP.
// @param rho Length-scale of the GP.
// @return An N x N positive semi-definite covariance matrix.
// @example gaussian_process::rbf_cov(x, 1.0, 0.5)
// @math
// k(x, x') = \alpha^2 \exp(-\|x - x'\|^2 / (2\rho^2))
matrix rbf_cov(vector x, real alpha, real rho) {
return gp_exp_quad_cov(x, alpha, rho);
}Recognised tags are @brief, @param (one per parameter), @return, @example and @math. These are plain Stan comments, so stanc ignores them and your file stays valid Stan on its own.
Document every export. This is what users see from laplace doc pkg::func, and it is what travels into their compiled .stan file, since the comments are spliced in along with the code. Two things worth getting right:
- Write
@examplewith the package name your manifest actually declares. It is easy to writetransformation::standardizein a library whosenameistransformations, leaving users with an example that doesn't resolve. - One
@paramper parameter, with names matching the signature.
Rather than writing the manifest by hand, run laplace init inside the package directory. It guesses name from the directory name, sets version = "0.1.0", and fills exports with every @laplace-documented function, printing which functions were included and which were left out:
$ laplace init
wrote laplace.toml for `mypkg`
included 1 exported function: add_one
note: 1 undocumented function left out of exports (add manually if this guess is wrong): helper
It refuses to overwrite an existing laplace.toml.
laplace init has three rough edges today. None of them blocks publishing a library, but knowing about them saves confusion. Fixes are tracked on Implementation Plans.
-
It only scans
.stanfiles.buildandinstallread.laplacelibfiles too, butinitdoes not, so a library written in the library dialect produces an emptyexportslist. The message in that case says no@laplace-documented functions were found, which reads as "your doc comments are wrong" when the real problem is that no files were read at all. The expected fix is forinitto use the same source-reading path asbuildandinstall, and to distinguish "no files found" from "no documented functions found". In the meantime, write theexportslist by hand. -
Overloaded functions are repeated.
initwrites one entry per signature, so four overloads of the same name produce four identical entries and an inflated count in its output. This is harmless — membership tests ignore duplicates, and renaming is idempotent — so an existing manifest does not need cleaning up. The expected fix is to deduplicate before writing. -
Hyphens in the directory name are not sanitized. Running
initinmy-library/guessesname = "my-library", which is not a valid Stan identifier. Edit the name by hand. The expected fix is to sanitize the guess.
One other thing to know as an author: laplace doc currently prints only the first overload of a name, so users cannot discover the other signatures from the CLI. Until that is fixed, list overloads explicitly in your README.
A library can build on another library. Two things change.
The manifest gains a [dependencies] table, with the same syntax a project uses:
name = "regression"
version = "1.0.0"
exports = ["centre"]
[dependencies]
stats = "^1.0"And the source file gets the .laplacelib extension, so it can carry a library { } block and namespaced calls:
// regression.laplacelib
library {
import stats
}
// @laplace
// @brief Centre a vector on its mean.
// @param x The vector to centre.
// @return `x` minus its mean.
vector centre(vector x) {
return x - stats::mean_(x);
}.laplacelib is a relaxed dialect: bare function definitions, an optional functions { } wrapper, and an optional library { } block. The model-shaped blocks (data, transformed data, parameters, transformed parameters, model, generated quantities) are rejected with a clear error, because a library provides functions to a model rather than being one.
A package can mix .laplacelib and plain .stan files freely. The .stan ones are ordinary Stan, passed through verbatim, and cannot import anything.
Your imports are private. A model that depends on regression does not thereby get access to stats, and your package may only call packages listed in its own [dependencies]. There is no re-export mechanism.
Users install libraries straight from a git repository, so publishing means putting the package somewhere they can point at and tagging it.
If the package sits at the repository root, next to .git, users need nothing extra. If it sits below the top level — beside a README, or as one of several packages — they pass --subdir. The layout used across the existing Laplace libraries puts the package in a laplace/ directory:
laplace-survival/
README.md
laplace/
laplace.toml
survival.laplacelib
Then a user adds it with:
laplace add survival \
--git https://github.com/mlatinov/laplace-survival --tag 0.1.0 --subdir laplaceA tag is a pointer to a commit, and Laplace reads the repository exactly as that commit left it. If you tag 0.1.0 and then move or add laplace.toml, every user of that tag gets an error saying the package has no laplace.toml, even though the file is plainly visible on the repository's main page.
So cut the tag last, after the manifest is committed at the path consumers will pass to --subdir. If you need to correct a published tag:
git tag -d 0.1.0
git push origin :refs/tags/0.1.0
git tag 0.1.0
git push origin 0.1.0Moving a published tag changes what existing users resolve to, so prefer cutting a new version once anyone else is depending on yours.
-
nameis a valid Stan identifier, with no hyphens, and reads well as a prefix inpkg::func. - Every function in
exportsexists in the source. - Every export has an
@laplacedoc block, with@paramnames matching the signature and@exampleusing the real package name. - Private helpers have distinctive names unlikely to collide with another library's.
-
versionfollows semantic versioning. - A README lists what the library covers, including all overloads, since
laplace docshows only the first. - A test model builds against the library and passes
stanc, for example withlaplace build model.laplace --validate. - The tag is cut after the manifest is committed, and
laplace add --git ... --tag ... --subdir ...works from a clean directory.
Once that passes, open an issue on the [laplace repository](https://github.com/mlatinov/laplace) to have your library listed on Ecosystem.
- Language Guide: the syntax your users will be writing
- Packages and Dependencies: how versions resolve
- Implementation Plans: known issues and what is coming next