Last active
July 5, 2024 17:50
-
-
Save chuckadams/f25f3324e1a116c0bea2d2fec1f5a231 to your computer and use it in GitHub Desktop.
Thoughts on a PHP module system
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//////////////// | |
module My\Stuff\MyModule; // implies “namespace My\Stuff\MyModule” | |
export class Foo { } | |
export function foo() { } | |
function bar() { } // not exported | |
export string $message = “your ad here”; | |
export const BEST_NUMBER = 69; | |
//////////////// | |
module Other\Stuff\OtherModule ; | |
// import specific items. Specific syntax like ordering is obviously not set in stone. | |
import foo as my_foo from My\Stuff\MyModule; | |
my_foo() | |
//// module objects, in the const namespace (like classes afaik) | |
import My\Stuff\MyModule as mod; | |
mod::Foo $foo = new mod::Foo(); // static things use static syntax | |
mod::BEST_NUMBER; // consts also being static things | |
mod->foo(); // functions become instance methods | |
mod->message; // variables become props | |
//// Above is the most basic stuff. Nice-to-have features below. | |
//// dynamic imports | |
$modname = “My\\Stuff\\MyModule”; // yeah, friggin backslashes i know | |
$modref = import($modname); | |
$modref->foo(); | |
//// ::module literals | |
$modname = My\Stuff\MyModule::module; | |
$modref = import($modname); | |
// My\Stuff\MyModule::module doesn’t *have* to be just a string like ::class is, | |
// but should probably stringify the same for interoperability. | |
// If ::module were fancier than a string, then perhaps auto-importing... | |
$modref = My\Stuff\MyModule::module; | |
$modref->foo(); // -> and :: operators trigger auto-import | |
//// Module metadata (not specified: how to declare or set it) | |
// you didn't think we'd use _global_ functions to manage a _module_ system, did you? | |
$meta = PHP\Module\Metadata::module; | |
$mm = My\Stuff\MyModule::module; | |
$meta = $meta->load($mm); | |
// alternatively, just this: | |
$meta = PHP\Module\Metadata->load(\My\Stuff\MyModule::module); | |
$meta->version; | |
$meta->source; | |
$meta->bytecode; // ok maybe not :) | |
$meta->get('user-defined-key', 'defaultval'); | |
//// random thoughts on a package system | |
$pkg = $meta->package; | |
$pkg->version; | |
$pkg->license; | |
$pkg->author->name; | |
$pkg->author->url; | |
// Now for the money shot. The syntax is definitely up for grabs here. | |
import Symfony\Component\Messenger as m in "my-forked/messenger" with ['version' => '>=2.4']; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//// parameterized modules | |
module Greeter; | |
export readonly var $name; // let's resurrect 'var' for declarations | |
\PHP\Module\Lifecycle->on_import(fn(array $args) => $name = $args['name']); | |
export function hello() { | |
echo "hello $name!\n"; | |
} | |
// on_import is arbitrary code. the sky is the limit, as is the deepest level of hell. | |
module App; | |
import * as greeter from Acme\Greeter with ['name' => "Jeff"]; | |
greeter->hello(); | |
$dude = greeter->name; // also exported | |
//// importing a legacy namespace as a module, then using that module as a namespace | |
import * as msgr from Symfony\Component\Messenger; | |
export $send = fn(msgr\MessageBusInterface $bus, ...$args) => $bus->dispatch(...$args) | |
//// anonymous modules. with parameters too, why not. it's basically a function with 162% More Fun. | |
$greeter = module(string $name, Writer $out) { | |
// no special class member syntax, it's all top-level PHP in here. | |
$name === 'donnie' and throw new STFUException(); | |
export $greet = fn() => $out->printLine("hello $name!"); | |
}; | |
$g = $greeter("dude", $some_out_thingie); | |
$g->greet(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for all the feedback! I'm juggling a few other tasks ATM but I wanted to address a few general points, make some edits to the files up top, then put together some more detailed responses to some of the questions and points below.
Correct, nothing is accessible unless exported. In MyModule, bar() is private to the module, or as I put it simply, "not exported". Same sort of package-private semantics though, everything in the module's namespace (modules are basically reified namespaces) can access it, nothing outside it can. I avoid saying "package-private" since I'm trying to keep the concept of "packages" with all their metadata like versions and source repos separate from the simpler reified namespaces that are modules.
The
import
andexport
keywords would only work within a module and be a syntax error anywhere else, much like trying to usepublic
andprivate
outside of a class.This was of course a total brainfart with a now ironic comment, and it's deleted from the latest revision. Nested namespaces aren't a thing, and while they could perhaps become a thing through bracket syntax, I'm not proposing it. So just assume we're doing
module My\Stuff\MyModule
from now on.It's a good question. Interoperability between modules and namespaces is a goal, but it's also a big can of worms. I'm thinking the behaviors should be something like this:
import
a namespace as a module.namespace
, but not undermodule
. Somewhat analogous to the behavior of classes vs structs in C++.Part of me wants to say they would merge the same way namespaces do now, using the above semantics for auto-exporting namespaces promoted to modules... but I'm now leaning toward just forbidding it, or at least having the module only see what was declared under
module
.We're definitely in the tall grass here and could spend days hammering out the details of module/namespaceinterop. That certainly needs to be done, but I'll be handwaving away much of it for the rest of the reply except of course where it's the only issue at hand.
I'll just note that while anyone can declare any namespace anywhere, PHP already prevents redefining functions (a behavior I hate BTW), so it's not like there isn't already a potential runtime landmine in doing so.
3500 is actually not a whole lot all told. As Theoretical BDFL, I decree that PHP now have more context-sensitivity in its parser (lexer really) and that the keywords are only recognized within a module. Within a module, BC breaks are served for breakfast. There, all solved :)
There's a case for using the
public
andprivate
keywords instead ofexport
and the default of not exporting. I still think modules should be private by default, but I guess I could hold my nose and usepublic
to mean export. There's still themodule
andimport
keywords to deal with though.Overall, this omelette recipe calls for breaking a few eggs, so I want to make it worth it.
I'm now strongly leaning toward disallowing
Mod\foo()
or using a module like a namespace at all, and I've deleted such uses from the file. It now looks like this insteadI'm somewhat attached to imports being in the constant namespace for a few reasons:
readonly
but that's extra noise and is specific to classes at any rate. It could be repurposed, but that seems like extra work for actually negative benefit.The backslashes are a passing annoyance for me. Things like variables always being global are what summon my inner vengeance demon, and the global function namespace is always a rich mine of WTFs.
All the examples should work when concatenated into a single file, and if multiple files are involved, then autoloading gets involved. include/require/require_once don't even enter into my vocabulary anymore: the only place I ever want to deal with raw source files is inside of an autoloader, and even then only as a necessary evil.
A module's metadata should be loadable without importing the module, yes. Thinking
\My\Stuff\MyModule::module
would be some sort of lazy proxy that turns into the real deal on demand, but where it's not necessary to do so for just metadata. I gleefully handwave away further details and assume modules under \PHP have sufficient magic to pull it off :)Stuff like
$meta->file
and$meta->source
would be built-in, no declaration required. Not sure about$meta->version
, which probably belongs in a package anyway. Maybe aMyModule.meta.php
file alongside the main one, but that would get cluttered really fast. Maybe usingdeclare()
and only parsing the file for declarations? Or maybe just import the module, possible side effects be damned.A subclass might be better, but I don't want to have to deal with the prospect of autoloading a user-defined metadata class in what might be the middle of an autoload operation itself. I think it ultimately depends the specifics of how metadata gets declared.
Modules should be able to take advantage of the existing autoloading infrastructure with very few changes, so Composer should be able to autoload them. I'd want a new entry point to the autoloader for modules, but
spl_autoload_module
could start off as an alias tospl_autoload
.I'm not enamored with how primitive autoloading is in PHP, but insofar as it can have literally any side effect, it gets the job done better than a more restrictive API would. Enhancements to autoloading are definitely on the table, but I'd definitely want the input of the Composer devs.
Hehehe, obviously not for Version 1. I've also changed it to use
as
instead ofimport *
I'm thinking the callback should take any type actually, I just used an array for convenience.
That's what the hook does. Or are you saying it should be exposed within the module as
__ARGS__
or something and let the top level deal with it? In perl, you process import args in theimport
sub ... there could be an__import
magic function, but that would likely be frowned upon, even if scoped to the module.As I mentioned above, I prefer to wrangle actual files only at the low level of the autoloader. But you can still do it with
require_once
if you want: just return an anonymous module and you're all set.That is a namespace already, this is just an example of importing a namespace as a module. I'm backpedaling on using the namespace separator (that is, the bloody backslash) after a module to access values, but it seems types would have to keep using it.
The idea of modules is to have a controlled scope, with hopefully as little extra overhead as possible
I would absolutely love for PHP to have real lexical scope in every block, but unless scopes specifically opted in to such mechanics, I can see it breaking the whole world. An anonymous module is that opt-in, an imperfect solution for an imperfect world.