Do any languages specify package requirements in import / include statements?
88 comments
·January 20, 2025mst
In perl, you can do
use Some::Library v1.2.3;
and there's assorted tooling that will turn that into some sort of[0] centralised dependency spec that installers can consume.Also e.g. https://p3rl.org/lib::xi will automatically install deps as it hits them, and the JS https://bun.sh/ runtime does similar natively (though I habitually use 'bun install <thing>' to get a package.json and a node_modules/ tree ... which may be inertia on my part).
Th e perl use is a pure >= thing though, whereas I believe raku (née perl6) has
use Some::Raku::Library v1.*.*;
and similar but I'm really not at all an expert there.[0] it's perl so there's more than one although META.json and cpanfile are both supported by pretty much everything I recall caring about in the past N years
drweevil
Go goes at least part-way there. https://golangbyexample.com/go-mod-tidy/ https://matthewsetter.com/go-mod-tidy-quick-intro/ You write your module source. You then run go mod tidy. This reads your sources for imports and automatically creates the go.mod and go.sum files What's nice about this is that it ensures reproducible builds, so you should add those files to your revision control repo.
foobarbecue
Thanks! Sounds like it's time for me to try go.
BozeWolf
If you prefer a good package manager over a language which avoids you to be able to shoot yourself in the foot with nil pointers…
Package management is important, but other stuff might be more important.
That said: I enjoy go. Is it perfect? No! It feels like the old days with python. I expected a lot more from the golang type checker. Lots of basics are not there (reverse a string? Write your own function. Etc.)
earthboundkid
Python has None, which frequently caused me problems in production, so that's not different, except Go can at least tell between a string and nullable string pointer.
Reversing a string is not a basic operation. A) why would you ever need to do it in the real world? B) reversing Unicode is non-trivial due to composing characters. There are packages available for Go that implement grapheme segmentation. If you need it, you can import one.
flohofwoe
Deno can directly import in TS/JS from URLs, really nice for small 'shell scripts', but has some considerable downsides for bigger projects:
https://deno.com/blog/http-imports
PS: also Godbolt's C/C++ compilers can directly #include from URLs, I guess they run their own custom C preprocessor over the code before passing it on to the compilers:
sureIy
> Deno can directly import in TS/JS from URLs
FWIW, that's a native JS feature (minus the TS part).
iforgot22
Yeah, not sure what Deno has to do with this, other than they implemented the standard at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
sureIy
Deno is the only server that has done so though. Neither node nor bun support them.
junon
The HTTP imports astound me. Having little control over how bundlers fetch code randomly screams vulnerability vector. How people are okay with it is wild to me.
ibejoeb
You're right, of course, but there's little practical difference from doing `npm install` unless you're actually auditing the supply chain. It just automates a step.
christophilus
The difference is that you have a single file to audit with npm. With Deno, any file in your codebase might pull in a dependency.
cbm-vic-20
They should have at least have something like HTML Subresource Integrity[0], including a hash so at least changes to what comes back from the import hasn't changed.
junon
Yes, though it's not enforced when it should be.
Sharlin
Cargo people are working on single-file "Rust script" support that would allow embedding the necessary parts of the manifest (Cargo.toml) directly in the .rs file as special doc comments (so things are transparent to rustc): [0]
epage
What cargo is doing is something like
#!/usr/bin/env cargo
---
[dependencies]
regex = "1"
---
fn main() {
}
I think this is proposing something like #!/usr/bin/env cargo
#[version = "1"]
extern regex;
fn main() {
}
though `extern` has gone out of style and would instead be something like #!/usr/bin/env cargo
#[version = "1"]
use regex;
fn main() {
}
but that wouldn't isn't idomatic either and so you would have #!/usr/bin/env cargo
#[version = "1"]
use regex::RegexBuilder;
use regex::Regex;
fn main() {
}
which runs into the problem noted but in one file> I realize there would be downsides to this idea. For example, you have to figure out what happens if different versions of a requrement are specified in different files of the same package (in a sense, the concept of "package" starts to weaken or break down in a case like that). But in some cases, e.g. a single-file python script, it seems like it would be great.
To fully support this, we'd need a top-down compilation model like Zig so we could discover dependencies in the current project. Today, we have to do bottom-up compilation, knowing all dependencies a priori.
A downside to any of this is it is expensive to do any any dependency management
- Introspecting dependencies requires parsing every file in every part of your dependency tree
- Editing dependencies requires walking every file in your project
jrochkind1
You can already do that with ruby bundler (which perhaps inspired it)
https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
But I recognize that isn't quite what the questioner is asking, because at least in ruby you are still going to need "require" statements (unless you have an autoloader) to actually load the code, the inline `gemfile` specifies your dependencies, which is different than a require/import statement to actually load code from possibly one of those dependencies.
greener_grass
In F# scripts you can add dependencies directly from Nuget like so:
#r "nuget: NodaTime, 3.2.1"
open NodaTime
let now = SystemClock.Instance.GetCurrentInstant()
printfn "%A" now
neonsunset
One of the best features.
Just import everything! Nuget packages, local dlls, other F# script files, you name it. It's so good. No extra ceremony. And when you open it in VS Code with Ionide - you get full support of the language server, documentation and syntax highlighting to know that your script is correct.
Night and day difference with standard scripting languages.
greener_grass
> other F# script files
This is where it breaks down a bit.
Importing the same script file twice leads to errors, so you must manually ensure exactly once imports across your dependency tree of scripts.
This is where C/C++ code reaches for include guards, but F# does not have a define pre-processor command (unlike C#).
neonsunset
Fair. I think if this issue takes place - it's probably time to switch from script files to a full .NET project. Or package a distinct piece of functionality as such.
Pet_Ant
Groovy has Grapes. https://docs.groovy-lang.org/latest/html/documentation/grape...
Groovy is a Java variant that runs on the JVM that allows you to add dependencies as annotations. I believe it uses Maven in the back-end, but it's just so convenient for scripts etc.
vincnetas
i can tell from practice it's really useful for single file scripting in JVM world.
philomath_mn
Not exactly what you're talking about, but `uv` lets you specify dependencies in the header of scripts: https://docs.astral.sh/uv/guides/scripts/#declaring-script-d...
I think what you describe really only makes sense for a single file script. I _do not_ want to manage dependency hell within my own source files.
teroshan
This isn't `uv`-specific, this is part of PEP 723 – Inline script metadata https://peps.python.org/pep-0723/
lizmat
The Raku Programming language allows one to specify the required version, the required authority and API level:
use Foo::Bar:ver<0.1.2+>:auth<zef:name>:api<2>;
would only work if the at least version 0.1.2 of the Foo::Bar module was installed, authored by "zef:name" (basically ecosystem + nick of author), with API level 2.
Note that modules can be installed next to each other that have the same name, but different authorities and different versions.
Imports from modules are lexical, so one could even have one block use one version of a module, and another block another version. Which is handy when needing to migrate date between versions :-)
atenni
Python’s PEP 723 (Inline script metadata) has a section summarising why they couldn’t take this approach under “Why not infer the requirements from import statements?”
https://peps.python.org/pep-0723/#why-not-infer-the-requirem...
iforgot22
Javascript does. For example:
<script src=" https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js "></script>
Or using the new ES6 "import" syntax: <script type="module"> import lodash from 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/+esm' </script>
xigoi
That’s just the CDN’s convention, not a part of the language.
lolinder
Versions aren't a concept in most languages at all, so they're bound to be imposed by some external system, be that NPM or CDN.
It's part of the language that you can specify a complete URI in an import statement, and a URI for a library really should include a version number somehow.
iforgot22
Oh yeah I was only referring to how the language specifies which deps you need and where you get them, not the versioning too.
Veserv
You are mixing up program build arguments and program build parameters. In much the same way that a function has arguments, then you substitute actual parameters when calling it; you should view your build as having arguments, "imports", and parameters, "specific package versions", that you pass to the corresponding import.
Specifying a specific package version in your source directly would be like having a function with arguments, then removing one of those arguments and replacing it with a local variable with the same name that you hardcode to a specific value. It is a perfectly fine thing to do if that argument really should only ever have that specific value, but it is a fairly "fundamental" source code change; your function has fewer arguments and a hardcoded value now!
To be fair, as far as I am aware, no commonly used language seems to understand the distinction and syntactically distinguishes build arguments from build parameters. What you should have is a specific syntactic operation that specifies a argument that takes type "package", a separate, distinct syntactic operation that instantiates a specific package and binds it to a name, and a separate distinct syntactic operation that passes in a package instance to a argument. Then your build system is just instantiating specific packages and passing them as arguments to your files with imports.
In your case, you would then just be instantiating specific packages and assigning them to a "common" name. You would have no "imports" in this sense as you have no arguments, only "local variables", and thus the build system would need to do nothing as there are no "arguments" to your file. That or your build system still instantiates the packages, but as "global variables" assigned to names of your choosing, that you would then just reference in your contained file.
iforgot22
I don't see what's wrong with what OP is asking for. JS does this, but Python doesn't.
And about the parameters thing, often times your code will only work with a specific major version of some dependency. You could switch versions, but it'd require editing the code.
Veserv
There is nothing wrong with it. The point I am making is that “import” is overloaded to mean two distinct things in most languages: declaration of build “arguments” and supplying build “parameters”.
It is like defining a function, f(x) and then asking for how to make x = 5 always. Well, you get rid of the parameter and create a local variable in the function named x and set it to 5. That is a perfectly reasonable thing to do.
But, it would be utterly absurd to make function argument declaration look exactly the same as local variable declaration and initialization. You should have distinct syntax for those two very different operations.
Unfortunately, for build arguments and parameters, every commonly used language I am aware of either only supports one form and punts the other form to the build system or attempts to do both using the same form. That is why it is a mess.
As for versioning, that is actually a “type” problem. When you take a parameter in a function you are usually expecting it to conform to some type or interface guarantees. Build arguments should do the same, but again, I am not aware of any languages that actually manifest the “type” of a package that can be used in a build argument. So, we are stuck with the wonderful world of untyped build arguments that we shoehorn in with “type annotations” (i.e. version ranges) in the build system.
riffraff
ruby's bundler has an "inline" mode for this, it's mostly meaningful for single-file scripts, as you thought.
https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
When coding small programs in python, js, java, C++ it often feels to me that the dependency requirements list in pyproject.toml, requirements.json, maven.xml, CMakeLists.txt, contains information that is redundant to the import or include statements at the top of each file.
It seems to me that a reasonable design decision, especially for a scripting language like python, would be to allow specification of versions in the import statement (as pipreqs does) and then have a standard installation process download and install requirements based on those versioned import statements.
I realize there would be downsides to this idea. For example, you have to figure out what happens if different versions of a requrement are specified in different files of the same package (in a sense, the concept of "package" starts to weaken or break down in a case like that). But in some cases, e.g. a single-file python script, it seems like it would be great.
So, are there any languages whose standard installer / dependency resolvers download dependencies based on the import or include statements?
Has anyone hacked or extended python / setuptools to work this way?