Happy New Year Internals!
One of the areas I have been agonizing over in 2019 has been constants — especially class constants — and Mark Randall's following comment from the other thread cause me to remember that I had been planned to ask the list to discuss for a while.
Begin forwarded message:
From: Mark Randall marandall@php.net
Subject: [PHP-DEV] Re: [RFC] "use global functions/consts" statement
Date: January 1, 2020 at 11:31:48 PM EST
To: internals@lists.php.netThat doesn't cover defined constants, but IMO we should be pushing those to be moved into class constants, simply because in the absense of function / constant / anything-but-class level autoloading, things might as well be in a readily available location.
I have found namespaced class constants as a great usable syntax for DRYing out code during refactoring related to typically hardcoded literal values, or when writing code initially. Once such example might be:
namespace Example;
class API {
const BASE_URL = 'http://api.example.com';
}
With this a developer can use like so:
namespace Example;
....
$api_url = sprintf( '%s/%s', API::BASE_URL, '/widgets' );
Instead of writing this 20 different places across a codebase:
$api_url = 'http://api.example.com/widgets';
The problem with using constants is that the value is still hardcoded and if we later want to change to pulling the data from a config file we have to change all the code that uses those constants.
Yes we could use methods instead, but frankly constants feel more natural for things that typically get hardcoded.
We all know that constants need to be, by definition, constant (a.k.a. immutable), but is there any reason we could not allow constants a one-time initialization via code? That way we would allow a developer to hardcode a constant and then later come back and convert that constant to be initialized from a configuration file without having to modify any code that uses the constant?
If others agree with this concern then three (3) approaches come to mind, either of which would satisfactory to me although the first would be best IMO. That said, I am open to any approach that allows constants to be one-time initialized with code.
Approach #1: Extend the const keyword to allow function-like syntax
Extend the constant keyword to support all the syntax supported by methods except parameters, but only run the code the first time the constant is accessed, on demand:
namespace Example;
class API {
const BASE_URL:string {
return Config::get_options('api_base_url');
}
}
$api_url = sprintf( '%s/%s', API::BASE_URL, '/widgets' );
Approach #2: Allow one-time assignment to a constant
Alternately we could allow one-time assignment to a constant, although this would be much less preferable because it would require the initialization to be explicitly called by a developer, and the developer would have to implement all their own on demand logic, or just initialize all classes on page load which most people would probably fall back to doing:
namespace Example;
class API {
const BASE_URL;
static function initialize() {
self::BASE_URL = Config::get_options('api_base_url');
}
}
API::initialize();
$api_url = sprintf( '%s/%s', API::BASE_URL, '/widgets' );
Approach #3:Magic methods!
A magic method could work too, but I know many others have reasons to dislike them no the least of which is lack of static typing. I include here more for completeness rather than as a serious suggestion:
namespace Example;
class API {
static function __const(string $const_name) {
$value = null;
if ( 'BASE_URL' === $const_name ) {
$value = Config::get_options('api_base_url');
} else {
trigger_error( sprintf("constant %s::%s has not been defined.",
get_called_class()
,
$const_name
));
}
return $value;
}
}
$api_url = sprintf( '%s/%s', API::BASE_URL, '/widgets' );
Thank you for considering and I look forward to your thoughts.
-Mike
There's also a fourth approach that does not involve any changes to
PHP: autoload + eval.
When PHP is looking for your class, in your autoload you load the
values from config, generate the class source and eval() it.
There's also a fourth approach that does not involve any changes to
PHP: autoload + eval.
When PHP is looking for your class, in your autoload you load the
values from config, generate the class source and eval() it.
Yes, I guess that would technically be possible.
But I think in this case — dynamically generated code, lack of type safety and inability to version control and statically analyze the code — the proposed solution would be worse than the fallback position of not using constants.
-Mike
The problem with using constants is that the value is still hardcoded
and if we later want to change to pulling the data from a config file
we have to change all the code that uses those constants.
It's possible to work around this by using a unique constant name
and define('unique_name', $dynamicExpression)
(and const MY_CLASS_CONST = \unique_name;)
Maybe php could add something along these lines:
local const X = sprintf('In %s', $dynamicValue);
// Not supported in functions or loops.
// Translates internally to:
// if (defined('some_globally_unique_constant123af') ||
// !define('some_globally_unique_constant123af', X)) { throw ...; }
// use some_globally_unique_constant123af as X;
class MyClass {
const PUBLIC = X;
}
- php still needs to prevent recursion such as
const A = count(B); const B = array_fill(1, A, 'value');
,
which is why the simplest approach would be one that eagerly evaluates dynamic expressions.
That's probably not the only way - maybe it could track all of the constants that
are in the process of being defined and cause an uncatchable fatal error if there was recursion. - Right now, reading the value of a class constant can already emit notices or throw an Error (undeclared constants)
the first time it gets evaluated - Any approach should throw for results that contain cycles, objects, or references,
the same waydefine()
warns.
The below ugly proof of concept shows that the php runtime can in a sense
defer evaluation of dynamic class constants to runtime (not recursively)
(assume errstr/errfile control what constant gets loaded).
This POC should not be used anywhere.
<?php
set_error_handler(function ($errno, $errstr) {
echo "Ignoring $errno: $errstr\n";
define(
'SOME_GLOBALLY_UNIQUE_CONSTANT',
sprintf('Hello, %s', $GLOBALS['globalVar'])
);
});
class MyTest {
const KEY = 'myKey';
const FOO = [
self::KEY => [[][0], SOME_GLOBALLY_UNIQUE_CONSTANT][1],
];
}
$globalVar = 'world';
echo "Fetching MyTest::FOO\n";
var_dump(MyTest::FOO);
/*
Output:
Fetching MyTest::FOO
Ignoring 8: Undefined offset: 0
array(1) {
["myKey"]=>
string(12) "Hello, world"
}
*/
The problem with using constants is that the value is still hardcoded
and if we later want to change to pulling the data from a config file
we have to change all the code that uses those constants.It's possible to work around this by using a unique constant name
and define('unique_name', $dynamicExpression)
(and const MY_CLASS_CONST = \unique_name;)
I do appreciate the attempt to come up with something that is a workaround.
That said, I definitely do not want to do is start using "clever" code in my implementations because — one day — someone else will have to maintain what I write.
Really, I was just asking for a discussion of potentially allowing for one-time initialization via code for constants and via a straightforward, obvious syntax and standardized syntax.
Effectively I am just asking for immutable static values in a class that can be access vis the <Class>::<ImmutableValue> syntax.
-Mike
The problem with using constants is that the value is still hardcoded
and if we later want to change to pulling the data from a config file
we have to change all the code that uses those constants.It's possible to work around this by using a unique constant name
and define('unique_name', $dynamicExpression)
(and const MY_CLASS_CONST = \unique_name;)I do appreciate the attempt to come up with something that is a workaround.
That said, I definitely do not want to do is start using "clever" code
in my implementations because — one day — someone else will have to
maintain what I write.Really, I was just asking for a discussion of potentially allowing for
one-time initialization via code for constants and via a
straightforward, obvious syntax and standardized syntax.Effectively I am just asking for immutable static values in a class
that can be access vis the <Class>::<ImmutableValue> syntax.-Mike
I agree with the use case, but not with the solution of piggybacking them onto constants. It seems to me it would be much cleaner to just... build that into a static method call with the derivation-and-caching logic behind the scenes. It's not complicated code.
class Stuff
{
protected $val;
public static function val() {
self::$val ??= computeStuff();
return self::$val;
}
}
Although property accessors might make it even nicer looking, if we can ever figure out how to make that performant. :-)
--Larry Garfield
Hi Larry,
Thanks for replying.
It seems to me it would be much cleaner to just... build that into a static method call with the derivation-and-caching logic behind the scenes. It's not complicated code.
That is exactly what I have been doing, and what I am finding suboptimal.
The problem is that, most of the time and especially when refactoring poorly written code, we just want a constant. One line in the class file, hard coded.
We ideally don't want to have to write a full functionr. So for five data elements, 5 easy to read lines of code with constants vs. 20 harder to read lines with functions, or at least 40 lines if we include PHPDoc for those functions, which is part of our standard for writing functions.
When we later want to be able to refactor to initialize with code we want to do so w/o having to modify the code that is already using the constants. But we many never actually need to do so. What I am asking for is the option.
Basically, in my view, a programming language is better if it can support refactoring a reusable unit of code to add additional functionality W/O requiring the code that uses the reusable element to be modified. Using your proposed approach we cannot start using constants and later move to functions unless we modify all code that uses those constants. That is Bad JuJu(tm) in my book.
OTOH, if PHP 8.x would allow functions w/o parameters to be called w/o parenthesis then your approach satisfy the use-case:
class Stuff {
protected $val;
public static function VAL() {
self::$val ??= computeStuff();
return self::$val;
}
}
echo Stuff::VAL; //<== if this could work then using functions would address the use-case.
I agree with the use case, but not with the solution of piggybacking them onto constants.
Let me put it more concretely. What I am asking for is this:
"Transitionally of reusable code when using constants that does not require modifying code that already uses the constant."
So using functions does not actually address the use-case I am identifying.
Is there some technical reason why you object to extending constants, or is it your objection more stylistic?
-Mike
...
OTOH, if PHP 8.x would allow functions w/o parameters to be called w/o
parenthesis then your approach satisfy the use-case:class Stuff {
protected $val;public static function VAL() { self::$val ??= computeStuff(); return self::$val; }
}
echo Stuff::VAL; //<== if this could work then using functions would
address the use-case.Hi everyone,
I just wanted to comment on the above - in my view we can not have
someclass::VAL be a constant or a function call of a function that has no
arguments. It is not that it just feels confusing but also may create
issues as a person may invoke multiple times someclass::VAL thinking that
this is a constant (cheap) while in fact it may be a method doing expensive
lookup. And my personal feeling is very much against mixing two completely
different things in this way. This reminds me of Matlab where A(4,5) may be
a function with two arguments, or a matrix lookup (things may have changed
since and may no longer be the case...).
And on a side note - the described problem I solve it in two ways:
- custom autoloader that invokes a initialization method that sets static
vars - more recently - a class generator (as mentioned above). This gives no
performance hit as Im using Swoole and it happens only at server start.
Thanks
Vesko
--
Vesko Kenashkov
Developer at AzonMedia https://azonmedia.com/ and Guzaba Framework
<https://github.com/AzonMedia/guzaba2
Hi Larry,
Thanks for replying.
It seems to me it would be much cleaner to just... build that into a static method call with the derivation-and-caching logic behind the scenes. It's not complicated code.
That is exactly what I have been doing, and what I am finding suboptimal.
The problem is that, most of the time and especially when refactoring
poorly written code, we just want a constant. One line in the class
file, hard coded.We ideally don't want to have to write a full functionr. So for five
data elements, 5 easy to read lines of code with constants vs. 20
harder to read lines with functions, or at least 40 lines if we include
PHPDoc for those functions, which is part of our standard for writing
functions.When we later want to be able to refactor to initialize with code we
want to do so w/o having to modify the code that is already using the
constants. But we many never actually need to do so. What I am asking
for is the option.Basically, in my view, a programming language is better if it can
support refactoring a reusable unit of code to add additional
functionality W/O requiring the code that uses the reusable element to
be modified. Using your proposed approach we cannot start using
constants and later move to functions unless we modify all code that
uses those constants. That is Bad JuJu(tm) in my book.OTOH, if PHP 8.x would allow functions w/o parameters to be called w/o
parenthesis then your approach satisfy the use-case:class Stuff {
protected $val;public static function VAL() { self::$val ??= computeStuff(); return self::$val; }
}
echo Stuff::VAL; //<== if this could work then using functions would
address the use-case.I agree with the use case, but not with the solution of piggybacking them onto constants.
Let me put it more concretely. What I am asking for is this:
"Transitionally of reusable code when using constants that does not require modifying code that already uses the constant."
So using functions does not actually address the use-case I am identifying.
Is there some technical reason why you object to extending constants,
or is it your objection more stylistic?-Mike
There's two broad reasons I think this is a bad idea.
-
Constants are one thing. Function calls are another. They serve different purposes. Trying to mutate them into one thing can only lead to confusion and lack of understanding about what is actually going on.
-
The approach you describe (of starting with constants everywhere and refactoring to method calls later)... I would never do and do not endorse. What you describe is basically "make globals nicer to work with", whereas I am 100% firmly in the camp of "if I could remove globals from the language entirely I would". Frankly, the use of constants for configuration is an anti-pattern to begin with; they should be used only for things that are truly constant. Honestly, I cannot recall the last time I used constants for anything other than giving some other compile-time value a nicer name. (Eg, DEFAULT_THING_VALUE or giving nice names to bit flags or something like that.)
For configuration, my answer is frankly "put your configuration behind a nice configuration object from the very beginning and then you won't have to refactor it later; Problem solved." You can use env vars for configuration, and wrap those into a nice object, possibly using one of the many DotEnv implementations that already exist to make them nicer to work with in development. That is superior in basically every conceivable way to semi-mutable globals passing themselves off as pseudo-constants.
I would love to see property accessors come back, which would have a side effect of making what you describe a little easier, but at no point is it pretending to be a compile time value when it isn't.
--Larry Garfield
I have a work in progress wrapper for allowing const-like syntax for global constants static const X = $dynamicExpression;
.
This can be seen in https://github.com/TysonAndre/php-src/pull/10
This is basically a wrapper around if (!defined(...)) { define(...); }
If anyone's planning to implement changes to the way constants can be declared,
then the POC might be worth looking at (e.g. to see what parts of the code would need to be changed),
even if their design and approach is different.
-
The issues this solves has overlap and differences with what Mike Schinkel requests here.
It's intended as a solution to limitations in different use cases that I've encountered/thought about (which this thread reminded me of).Notably, this deliberately can't refer to other dynamic "static const" values that haven't been declared.
-
This deliberately limits it to the top-level statements in files
-
Class constants below the global 'static const' can then use the eagerly evaluated const.
Reasons for me to implement the proof of concept this way:
-
defined()
/define() is already supported by the engine.
This is a slight improvement due to shorter syntax,
and being more certain the const is only set or used in one file. -
Eagerly initialized dynamic constants are easier to reason about in some ways (e.g. fetching the constant of an already fetched class is less likely to cause pauses or exceptions.)
This makes un-noticed errors in rare code paths less likely. -
Attempting to support function calls, property accesses, class constant accesses (e.g. inherited), and variables in class constants recursively and/or lazily
would probably break a lot of code/assumptions in the php engine.I might be mistaken about that, though - I'd be glad if it turned out to be easy to implement.
Declaring 'static const' as a top level statement of a function/closure/method might also be possible, but would require more work.
It's also worth mentioning that some expressions that definitely have constant values
can't be written (in a convenient way) because they use syntax that is prevented right now, such as function calls, closures, etc.
My preference would be to make it easier to use dynamic expressions, and recommend in coding guidelines that
certain types of expressions "SHOULD NOT" be used for constants that vary on a particular host.
(network calls, time, etc)
const DEFAULT_PAYLOAD = json_encode(['key' => 'value']);
const ALLOWED_ELEMENT_TYPES = array_merge(self::FIRST_TYPES, self::SECOND_TYPES);
const ELEMENT_LENGTHS = array_map('strlen', self::ELEMENTS);
For derive-once global constants, you may be looking for define.
That's a runtime operation and effectively boils down to a set-once global.
It doesn't help with constants defined in a class,
but for an already-global constant define already gives you a one-time set operation,
just not on-demand.
This POC adds an alternate syntax for a set-once global.
It prevents accidentally reusing the same global name in multiple files (e.g. with copy and paste),
and lets people looking at it know the constant is only used in that file.
Another use-case that just occurred to me is to support testing:
class Api {
const URL: string{
return $_ENV['EXAMPLE_API_URL'] ?? 'https://api.example.com';
}
}
That example would be written with the POC as:
static const API_URL = $_ENV['EXAMPLE_API_URL'] ?? 'https://api.example.com';
class Api {
const URL = API_URL;
}
Would it be worth expanding the ideas of programmatic constant
definition into a more general compile-time code execution approach?
It would work well with preloading introduced in 7.4, and could allow
some of the things frameworks are currently doing at runtime to be
done once at compile time (opcode generation time). The
work-in-progress language JAI has a very interesting compile time
execution system which could possibly be a source of inspiration.
Lots to reply to here, so I'm going to munge it into a single response.
Mike Schinkel said:
Let's consider the fact that is PHP had never had the
define()
and instead implemented theconst
keyword from the start, and only allowed compile time values. If PHP had done that, you would be arguing from a status quo bias the same thing about making that dynamic. And it currently is dynamic which IMO invalidates your argument that constants must be defined at compile time.
Do not put words in my mouth and presume to know what I would think in some hypothetical situation. That is rude and inappropriate.
You also repeatedly are accusing Rowan and I of "status quo bias" as though it were a bad thing. Let me be clear: I entirely agree with Zeev that in a language used by millions of people a "status quo bias" is a very good thing. The potential benefit of any improvement needs to substantially outweight any downsides, and the burden of proof is on the person/people proposing a change. There's a long list of things I'd like to see added to the language myself, but I'm well aware that the burden of proof is always on the proposer (in this case, you), as it should be.
Also Mike:
Many people would say the kind of global state you're talking about is an anti-pattern.
Well, Larry did, which is reality is an appeal-to-authority argument and not an argument on the merits.
So you call something an anti-pattern, then get on my case for "appeal to authority" for calling something an anti-pattern? Your argument style needs work, dude.
Global data is well-recognized across the industry as dangerous, and the larger the system the more dangerous it is. It violates pure functions, and pure functions are how you get any semblance of predictability in your code. I do not have the time or inclination to go through all the details of that here; you can find ample resources for that yourself.
(And for the record, "appeal to authority" is only a logical fallacy if the authority being appealed to is not a qualified authority on the topic at hand. Appealing to Tiger Woods's authority on golf clubs is not a logical fallacy because he really would have more knowledge and experience than most; appealing to his authority on baseball gloves would be a logical fallacy.)
I take it you have never used Pantheon hosting then? You create a named environment to match a Git branch.
Full disclosure: I work for Platform.sh, which has been doing environment-per-bit-branch longer than Pantheon has. I'm responsible for maintaining our public utility libraries for bridging environment variables into application configuration for environment-sensitive things like DB credentials and route names. So I am very intimately familiar with this problem space, probably moreso than most people. :-)
In my experience, systems that configure themselves via constants are the worst. I know some do, but it's the most annoying option to work with. It makes any sort of dynamic override (which you must do in a cloud hosting enviroment) a PITA. The best option are systems that are configured by env vars, and have a very clearly set of documented env vars to populate. Env vars are also globally readable but you're not tricked into thinking they're compiled out at compilation time (as things with const
are.) They're also by far the easiest for me to work with as the person writing that glue code.
Adding language features that make it easier for people to take the approach that is in my experience the most painful option is something I am not going to support. :-)
To the use cases, "I'm changing the API but don't want to change the API" (what you're calling evolving here) is not an argument that's going to carry weight with me. It's not a use case I feel is worth the confusion that comes with "Sometimes a constant is not a constant". The links you offer offer some other potential use cases that are worth considering, but "I made something a const when I shouldn't have" is not going to be convincing to me.
Enums are also not constants. They're sum types. They can be implemented using a collection of constants, but that's a mediocre implementation of them (even if a popular one).
To the list of links Mike offered, let me add Wikipedia:
https://en.wikipedia.org/wiki/Constant_(computer_programming)
So, to examine the full problem space using Wikipedia's terminology:
-
Macro constants. These are handled at compile time and do not exist at runtime. This is what PHP's const is currently, both global and class-based.
-
Dynamically valued constants. This is what Java and C++ have, and essentially what Javascript's const does. This supports dynamic code that runs a computation to produce a value, once. That value's scope is based on whatever that variable's scope would be otherwise. (Could be global, could be class static, could be local to a function.) It's unclear when that code is actually run, and likely varies by language; also, it's unclear if it's legal to have the computing code depend on runtime information. That may also vary by language; I'm not sure.
PHP's declare
statement is more akin to the latter, albeit hoisted to global scope. It's not computed on-demand but whenever the declare
line happens to be reached. There is no class-level equivalent.
Unlike the other languages listed, PHP also has a syntactic split between variables and constants. Variables have a $, constants do not. Whether that's good or bad is an academic question as it's clearly not going to change. That's in contrast to most other languages where there is no syntactic distinction, just the convention of capitalizing not-gonna-change things.
Dynamically valued constants are... I'll say similar to but not quite the same as an auto-memoized function. For one there's the () vs not; for another, an auto-memoized function may take parameters and thus memoize different return values. From one perspective one could look at dynamically valued constants as a zero parameter self-memoizing function with some syntactic sugar.
I can see a use for const variables, especially const class variables. The example in the Java link Mike provided is actually something I ran into just last week and it would have been very nice to have that ability. However, class constants in PHP right now are limited to Macro constants.
So the more general request here is for dynamically valued constants beyond the current declare
support. Which... I could potentially get behind. Self-memoizing functions is one way they could be implemented, if you don't mind (). (I don't.) That would also have a lot of other benefits.
The caveat in both cases is dependencies. Memoizing an impure function can lead to all sorts of time-dependent silliness. The same concern applies to dynamically valued constants. If their generation code is a pure function (however expressed), then cool, that's a safe and nice feature. If it has unpredictable dependencies, though, the behavior can be equally unpredicatable. That includes depending on $_GET or env vars. (Remember, PHP has plenty of users outside of shred-nothing requests, and with FFI and preloading hopefully more of them wil get used more often.)
So I could get behind dynamically valued constants and/or auto-memoizing functions. That would probably mean they still have a $ on them, which doesn't bother me. That clearly separates them visually from macro constants, which I like.
The concern is when you use impure functions to generate them, the results are unpredicatable and, depending on the circumstances, may be non-deterministic. The potential for hard to find bugs here is high; although the counter point is that using a function/method to emulate them today, which you can absolutely do, offers the exact same risk. So I am nervous here but not quite agahst, since... "status quo bias". ;-)
There's a lot of ways that could be done syntactically. One option is Tyson's proposal, although I would prefer to not pollute const
with it and use a declare, like so:
// This line already works.
declare('API_URL', $_ENV['EXAMPLE_API_URL'] ?? 'https://api.example.com');
// The change would just be allowing the const to lazy-populate here.
class Api {
const URL = API_URL;
}
Another option is to allow declare
in classes:
class Api {
declare string $URL = $_ENV['EXAMPLE_API_URL'] ?? 'https://api.example.com');
}
(Or something like that.)
The trick is when exactly these run, because they're impure functions so whether they run at code compile time or first-access could mean a dramatic difference in their resulting value. And that is precisely why I am very, very nervous about allowing impure functions to be cached, whatever the syntax on top of them. (Constant-like or not.)
Do not put words in my mouth and presume to know what I would think in some hypothetical situation. That is rude and inappropriate.
I do not mean to nor want to start any conflict here. I know I felt some of your phrasing rude and so I was probably writing in frustration. Hopefully we can both moderate as conflict is not good for any of us.
You also repeatedly are accusing Rowan and I of "status quo bias" as though it were a bad thing. Let me be clear: I entirely agree with Zeev that in a language used by millions of people a "status quo bias" is a very good thing.
You are misunderstanding the definition of "status quo bias." Here is part of the definition:
"Status quo bias is an emotional bias. The current baseline (or status quo) is taken as a reference point, and any change from that baseline is perceived as a loss. Status quo bias should be distinguished from a rational preference for the status quo ante, as when the current state of affairs is objectively superior to the available alternatives, or when imperfect information is a significant problem."
When I used that phrase I was specifically referring to the emotional reaction vs. the rational reaction. I understand and agree when rational arguments are used, but lately in our political climate people seem to often to start with emotional arguments. And calling something an anti-pattern or the alternative a "best practice" is not a rational argument; both of those are loaded words.
From: https://en.wikipedia.org/wiki/Status_quo_bias
But if I mischaracterized your objections I apologize.
So you call something an anti-pattern, then get on my case for "appeal to authority" for calling something an anti-pattern? Your argument style needs work, dude.
I notice you did not quote my full statement. I explicitly called out the irony of my own usage and made the distinction that I was not using the phrase to argue against someone else's proposal which is why I felt ok to use it, because I was not using it to block anyone else's needs.
Again, my goal is not conflict but hopefully improving PHP.
Global data is well-recognized across the industry as dangerous, and the larger the system the more dangerous it is.
Eh, not exactly. Global data that is mutable is bad. Immutable global data — especially constants — are not necessarily bad, at least per the highest voted answer which means the topic is definitely debatable:
https://stackoverflow.com/a/1265684/102699
And a constant that is initialized at runtime is still immutable through the life of the execution.
It violates pure functions, and pure functions are how you get any semblance of predictability in your code. I do not have the time or inclination to go through all the details of that here; you can find ample resources for that yourself.
I am very familiar with pure functions, you don't need to go through the detail with me.
That said, class methods in PHP are not pure functions and PHP has not embraced pure functions so I am not sure why you are using this your rational other than maybe you wish PHP was a pure-function language? Of course if it was limited to pure functions, we'd not be able to configuration from outside the program anyway.
(And for the record, "appeal to authority" is only a logical fallacy if the authority being appealed to is not a qualified authority on the topic at hand. Appealing to Tiger Woods's authority on golf clubs is not a logical fallacy because he really would have more knowledge and experience than most; appealing to his authority on baseball gloves would be a logical fallacy.)
When you said it was an anti-pattern you used passive voice and did not include a reference to any authority.
That said, I have been programming professionally for 30 years now and I have seen many best practices from "authorities" be promoted, and then later seen a tidal wave of developer sentiment argue against those same best practices, i.e. inheritance and then containment, as one example.
I've also seen authorities promote an idea and then watched as developers latched onto it and give it a life of its own, far outpacing what the authority orginally intended, e.g. Bob Martin and the single responsibility principle.
In my experience "best practice" and "anti-patterns" are too often used in debate even when they do not actually apply to the use-case, so I prefer to dispense with those words when trying to debate actual pros and cons of an approach.
I work for Platform.sh, which has been doing environment-per-bit-branch longer than Pantheon has.
That statement was in response to Rowan's comment, not yours.
In my experience, systems that configure themselves via constants are the worst.
Of course they are the worst! Because you can't initialize constants dynamically.
That is exactly what I am trying to change.
It makes any sort of dynamic override (which you must do in a cloud hosting environment) a PITA.
Exactly.
The best option are systems that are configured by env vars, and have a very clearly set of documented env vars to populate. Env vars are also globally readable but you're not tricked into thinking they're compiled out at compilation time (as things with
const
are.) They're also by far the easiest for me to work with as the person writing that glue code.
The word "tricked" is a pejorative and not helpful for our discussion.
What I read here is that you have a preference for using Env vars directly and I have a preference for encapsulating them into constants. I don't see that either of us are more "right" than the other, we just have different preferences.
I still have not heard a real reason why Api::URL is somehow worse to use than $config->api_url() other than your claim that the former is "global" and thus bad, which I challenge because the former would be immutable. This is one of the areas where I think the "anti-pattern" does not apply.
Adding language features that make it easier for people to take the approach that is in my experience the most painful option is something I am not going to support. :-)
Have you worked with other languages besides PHP where dynamically initialized constants were possible, and this caused problems?
To the use cases, "I'm changing the API but don't want to change the API" (what you're calling evolving here)
As you said above that it was rude to put words in your mouth, it is also rude to keep changing my words to better fit your argument. Let us both not do that.
"Sometimes a constant is not a constant".
What I proposed will always be a constant. Again, it would be immutable once set. The fact is it set at compile time does not make it constant, per the lin
From one perspective one could look at dynamically valued constants as a zero parameter self-memoizing function with some syntactic sugar.
Yes.
Although I tend to avoid using the term "memoizing" because most people I know have to look it up and then ponder over its meaning.
I can see a use for const variables, especially const class variables.
I assume you mean immutable variables that still use the $, like 'let' in Javascript?
Those would definitely be very useful, but unfortunately would not address the use-case I presented.
So the more general request here is for dynamically valued constants beyond the current
declare
support. Which... I could potentially get behind. Self-memoizing functions is one way they could be implemented, if you don't mind (). (I don't.) That would also have a lot of other benefits.
I am confused. This sounds like you are now agreeing with the need for dynamic constants?
BTW, the requirement for () means it would not address the use-case of allow code to be evolved.
The caveat in both cases is dependencies. Memoizing an impure function can lead to all sorts of time-dependent silliness. The same concern applies to dynamically valued constants. If their generation code is a pure function (however expressed), then cool, that's a safe and nice feature. If it has unpredictable dependencies, though, the behavior can be equally unpredicatable. That includes depending on $_GET or env vars.
Totally agree that people could write bad code. But there are a thousand ways they can already write bad code.
If a dynamic constant were to use $_GET or $_ENV it could be problematic, but it could also be coding in a manner than it would be completely robust, and making it robust is not hard.
(Remember, PHP has plenty of users outside of shred-nothing requests, and with FFI and preloading hopefully more of them wil get used more often.)
Maybe there is something here I am missing? Can you present a problem that dynamic initialization of a constant would create only a statically defined constant in these cases. I cannot think of one, but I will be honest and admit that does not mean there is not one.
So I could get behind dynamically valued constants and/or auto-memoizing functions. That would probably mean they still have a $ on them, which doesn't bother me. That clearly separates them visually from macro constants, which I like.
Having the $ requirement blocks my primary use-case, so it would bother me.
The potential for hard to find bugs here is high; although the counter point is that using a function/method to emulate them today, which you can absolutely do, offers the exact same risk.
And that's the key point. It really is no different from what we have today, except that what I proposed would provide evolvability.
The trick is when exactly these run, because they're impure functions so whether they run at code compile time or first-access could mean a dramatic difference in their resulting value. And that is precisely why I am very, very nervous about allowing impure functions to be cached, whatever the syntax on top of them. (Constant-like or not.)
When you say "cached," what are you thinking the lifetime would be? And how would that really be different from a static constant?
Are you concerned that the code could fail and leave the constant in an invalid state?
A potential way to address that is for a dynamic constant to require type hinting, and thus if it did not return the correct type it would fail with an easy to trace error output. And also disallow throwing errors outside of the constant. That would force developers to write robust dynamic constant code.
-Mike
snip
From: https://en.wikipedia.org/wiki/Status_quo_bias
But if I mischaracterized your objections I apologize.
I am not anti-change for anti-change's sake. Since I presume from earlier comments that you're familiar with my background in Drupal I think that is sufficient evidence that I am not against change as a concept. :-)
Let us move on.
It violates pure functions, and pure functions are how you get any semblance of predictability in your code. I do not have the time or inclination to go through all the details of that here; you can find ample resources for that yourself.
I am very familiar with pure functions, you don't need to go through
the detail with me.That said, class methods in PHP are not pure functions and PHP has not
embraced pure functions so I am not sure why you are using this your
rational other than maybe you wish PHP was a pure-function language?
Of course if it was limited to pure functions, we'd not be able to
configuration from outside the program anyway.
Yes, PHP doesn't have pure functions as an explicit thing. This makes me sad. :-) The point here is that when I call a function in PHP, I intuitively expect that it may not be pure, or may be context-sensitive. That may be desireable or not in context, but the () indicate "code will run, and code may do weird stuff". When I see a constant, I expect it to be, well, constant, deterministic, and have a small fixed performance impact. (In the case of const
, zero performance impact.)
For example:
const DEBUG_MODE = false;
function foo() {
if (DEBUG_MODE) {
// Do stuff.
}
}
PHP will currently (AIUI) inline the DEBUG_MODE value at compile time, notice that the code path is unreachable, and omit it from the compiled opcodes entirely. (I'm pretty sure it does that now, at least; if not, it certainly can be.)
If you use a define, however:
define(DEBUG_MODE, $_ENV['debug'] ?? false);
function foo() {
if (DEBUG_MODE) {
// Do stuff.
}
}
That optimization cannot happen. But just from looking at foo() I cannot tell which is going to happen. I view that as a negative.
I work for Platform.sh, which has been doing environment-per-bit-branch longer than Pantheon has.
That statement was in response to Rowan's comment, not yours.
I know; I included that as a resume so that my following points would carry more experiential weight. (And because we really were doing it before Pantheon, who is a competitor of ours, so there. :-) )
In my experience, systems that configure themselves via constants are the worst.
Of course they are the worst! Because you can't initialize constants
dynamically.
No, it's because you cannot override them. It makes the conditional logic around defining them more complicated. Specifically, with WordPress for example, I cannot just leave WP's default config script in place and tack ours on before/after it. I have to hack theirs up and replace it with one that checks every possible source for DB configuration information and then set it once, because it cannot then be overridden. They actually are set dynamically with declare(). The runtime dynamism isn't the issue. In this case it's actually the immutability that causes the problem, ironically enough. :-P
An example of where I can see problems: In some systems, $_GET is treated as mutable. It is, but modifying it is generally a bad idea, but people do it anyway. For instance, I've seen "clean URLs" implemented by modifying the $_GET variable and $_SERVER variable to mutate example.com/foo/bar into example.com/index.php?q=foo/bar. (Drupal 7 for instance did that.)
Now suppose you have a lazy-loading "constant" that uses $_GET. If it gets called before that conversion happens in some code paths but not others, its value will be unpredicatable. It may get built before $_GET['q'] is changed or after, which would change its value.
That's not something I ever expect a "constant" to do. I expect a constant to be constant from one request to another, even if it nominally isn't derived until runtime. Constants that change value are not constant.
(Is this a subjective and partially emotional interpretation? Of course it is. Much of language design, like any other design, is. But I don't think my subjective view here is unique.)
I can see a use for const variables, especially const class variables.
I assume you mean immutable variables that still use the $, like 'let'
in Javascript?
Yes. Or the static class property example with color from the Java URL you provided, which is a nice use case.
Those would definitely be very useful, but unfortunately would not
address the use-case I presented.
The specific use case you presented is not one I believe PHP should support. If I have not convinced you of my position on that yet, I don't think I am going to, nor are you going to convince me of yours.
So the more general request here is for dynamically valued constants beyond the current
declare
support. Which... I could potentially get behind. Self-memoizing functions is one way they could be implemented, if you don't mind (). (I don't.) That would also have a lot of other benefits.I am confused. This sounds like you are now agreeing with the need for
dynamic constants?
I am thinking aloud and allowing the available options to go where they logically go.
BTW, the requirement for () means it would not address the use-case of
allow code to be evolved.The caveat in both cases is dependencies. Memoizing an impure function can lead to all sorts of time-dependent silliness. The same concern applies to dynamically valued constants. If their generation code is a pure function (however expressed), then cool, that's a safe and nice feature. If it has unpredictable dependencies, though, the behavior can be equally unpredicatable. That includes depending on $_GET or env vars.
Totally agree that people could write bad code. But there are a
thousand ways they can already write bad code.
Certainly. But a good language nudges people toward writing good code, or at least not-bad code. (How hard it should nudge is a subject of much debate and is one of the reasons we have many languages.)
(Remember, PHP has plenty of users outside of shred-nothing requests, and with FFI and preloading hopefully more of them wil get used more often.)
Maybe there is something here I am missing? Can you present a problem
that dynamic initialization of a constant would create only a
statically defined constant in these cases. I cannot think of one, but
I will be honest and admit that does not mean there is not one.
See the $_GET['q'] example above for the sort of unpredictability I am concerned about. In a long-running process the potential for such issues is higher.
The trick is when exactly these run, because they're impure functions so whether they run at code compile time or first-access could mean a dramatic difference in their resulting value. And that is precisely why I am very, very nervous about allowing impure functions to be cached, whatever the syntax on top of them. (Constant-like or not.)
When you say "cached," what are you thinking the lifetime would be?
And how would that really be different from a static constant?
Any dynamic constant variable has a lifetime of its scope. If global, then it's the lifetime of the process.
Are you concerned that the code could fail and leave the constant in an
invalid state?
That wasn't what I was thinking of, although I suppose that's also a concern. I was more thinking of the inconsistent initialization time, as in my earlier example, leading to a constant that is variable request-to-request even if it's nominally constant-once-set within a specific request. That's a very different behavioral pattern from guaranteed consistent request-to-request. IMO they should remain separate.
--Larry Garfield
For example:
const DEBUG_MODE = false;
function foo() {
if (DEBUG_MODE) {
// Do stuff.
}
}PHP will currently (AIUI) inline the DEBUG_MODE value at compile time, notice that the code path is unreachable, and omit it from the compiled opcodes entirely. (I'm pretty sure it does that now, at least; if not, it certainly can be.)
If you use a define, however:
define(DEBUG_MODE, $_ENV['debug'] ?? false);
function foo() {
if (DEBUG_MODE) {
// Do stuff.
}
}That optimization cannot happen. But just from looking at foo() I cannot tell which is going to happen. I view that as a negative.
To be fair, you cannot tell in the first case either, unless your constant is in the same context as foo(), which it typically would not be.
You can track down DEBUG_MODE and know it is fixed, but if you ship the code for someone else to set DEBUG_CODE, you can't know what they will set too. So I am not seeing a big distinction other than with compiler optimization. And if the alternative is to use a function, that can't be optimized any better so it seems moot to me.
No, it's because you cannot override them. It makes the conditional logic around defining them more complicated. Specifically, with WordPress for example,...
Ah! You and I actually are on the same page there.
I have a project that is a local development environment (WPLib Box) and wp-config.php's structure has been a huge PITA for us. I am completely in sync with you here.
In fact I wrote an alternate wp-config to resolve that problem, but then had to task switch and I have not put any effort into evangelizing it:
Which brings up the point; it would be really great if we could redeclare declare() constants in PHP, for automated testing purposes.
Now suppose you have a lazy-loading "constant" that uses $_GET. If it gets called before that conversion happens in some code paths but not others, its value will be unpredicatable. It may get built before $_GET['q'] is changed or after, which would change its value.
I concur. However, if I do that in a function it will be the same issue.
I think it just comes down to us having a different value judgement. To me I don't see it being a problem if the bug is in a constant or a function — it is still a bug — but you clearly classify those differently.
That's not something I ever expect a "constant" to do.
To be fair, if constants were able to be dynamically initialized in PHP 8.0, you could change your expectation.
But as you say, that's not your preference.
(Is this a subjective and partially emotional interpretation? Of course it is. Much of language design, like any other design, is. But I don't think my subjective view here is unique.)
Maybe for PHP. But clearly the designers of Ruby, Javascript and Python thought differently, no?
The specific use case you presented is not one I believe PHP should support. If I have not convinced you of my position on that yet, I don't think I am going to, nor are you going to convince me of yours.
Fair enough. So if I were to create an RFP you'd get a vote "no", and sadly I will not get a vote. So it depends what others think.
Certainly. But a good language nudges people toward writing good code, or at least not-bad code. (How hard it should nudge is a subject of much debate and is one of the reasons we have many languages.)
I strongly agree with that.
I just disagree with your assertion that what I am proposing would result in "bad" code.
That wasn't what I was thinking of, although I suppose that's also a concern. I was more thinking of the inconsistent initialization time, as in my earlier example, leading to a constant that is variable request-to-request even if it's nominally constant-once-set within a specific request. That's a very different behavioral pattern from guaranteed consistent request-to-request. IMO they should remain separate.
Okay. Thank you for engaging. I think we have ended up having a well reasoned discussion and have laid out our respective positions succinctly. You feel strongly that inconsistent initialization would be a concept and I few that as just bad coding that would need to be fixed no matter if the code initialized a constant or if it ran through a function.
I guess we'll just have to see how others feel about the subject.
-Mike
P.S. another use-case I remembered for constants is poor-man's class annotations, which I actually use a lot. For example, I use a POST_TYPE constant in a class designed to load a WordPress post of the given type. It would be really nice to be able to initialize those types of constants differently based on testing vs. production.
Would it be worth expanding the ideas of programmatic constant
definition into a more general compile-time code execution approach?
It would work well with preloading introduced in 7.4, and could allow
some of the things frameworks are currently doing at runtime to be
done once at compile time (opcode generation time). The
work-in-progress language JAI has a very interesting compile time
execution system which could possibly be a source of inspiration.
That would certainly be interesting. I do not know enough what is possible there to opine.
It it were possible that might be the best of all worlds.
Anyone with experience in that area able to comment?
-Mike
Would it be worth expanding the ideas of programmatic constant
definition into a more general compile-time code execution approach?
That would certainly be interesting. I do not know enough what is possible there to opine.It it were possible that might be the best of all worlds.
Anyone with experience in that area able to comment?
Isn't that the same as pre-processors, which elicited this response when
I mentioned them before:
Oh God no! Pre-processors are definitely an anti-pattern, in my book.
I came to that conclusion sometime in the 80's and have yet to see
anything that changed my mind.
Perhaps the phrase "pre-processor" put you too much in mind of C / M4
style text macros? If so, that's not what I meant at all; what I meant
was some tool that evaluated some syntax during a "build" step,
producing valid PHP code.
That might be as simple as string substitution, but it might be special
code blocks running standard PHP, but run as a separate build step and
the results inline into the code. It could be as general as this:
namespace MACROS {
function getenv($var) {
return $_ENV[$var];
}
}
class API {
const URL=MACRO\getenv('API_URL');
}
Parse the code to an AST, allowing function calls with literal arguments
wherever a literal value would normally be allowed. Find all function
calls to something in the MACRO namespace, evaluate them, and substitute
the result into the AST. Then write the modified AST to a new PHP file,
and deploy that to your server.
Obviously easier said than done, but there are tools out there like
https://preprocess.io/ which do some quite powerful things with this
approach.
Regards,
--
Rowan Tommins (né Collins)
[IMSoP]
const DEBUG_MODE = false;
function foo() {
if (DEBUG_MODE) {
// Do stuff.
}
}PHP will currently (AIUI) inline the DEBUG_MODE value at compile time,
notice that the code path is unreachable, and omit it from the compiled opcodes entirely.
(I'm pretty sure it does that now, at least; if not, it certainly can be.)
It actually can't do that safely for global constants.
(But it can and does do that for class constants, within the class)
For example, if you declare a constant twice (declare() or const DEBUG_MODE =), then php emits a notice, but continues executing keeping the value the constant previously had.
You can use opcache's debug output to confirm that.
php -d zend_extension=opcache.so -d opcache.enable_cli=1
-d opcache.file_cache= -d opcache.opt_debug_level=0x20000 example.php
<?php
const DEBUG = false;
class TempClass {
const DEBUG = false;
public function main() {
if (DEBUG) { echo "In debug for global\n"; }
if (TempClass::DEBUG) { echo "In debug for class const\n"; } // optimized out
}
}
/*
L0 (7): T0 = FETCH_CONSTANT string("DEBUG")
L1 (7): JMPZ T0 L4
L2 (7): ECHO string("In debug for global
")
L3 (9): RETURN null
L4 (9): RETURN null
*/
A 'static const' syntax enforcing that constants can only be declared in one place would help in making these sorts of optimizations easier
(with changes to opcache, for expressions that can be evaluated at compile time)
But so would preloading in the future, since php could likely recompile functions with the constants known from the preload script
(maybe it does this already).
Would it be worth expanding the ideas of programmatic constant
definition into a more general compile-time code execution approach?
That would certainly be interesting. I do not know enough what is possible there to opine.It it were possible that might be the best of all worlds.
Anyone with experience in that area able to comment?
Isn't that the same as pre-processors, which elicited this response when I mentioned them before:
As I said it my most recent other reply I said I view pre-processors as text substitution. But you might have meant something different than text substitution?
What I think Robert was referring to — at least to me — was a form of pre-loading or recompiling into some form that could be referenced by the normal code, not an approach that would generate a new .PHP file. Something that can be linked via symbols at runtime.
Maybe something similar to preloading in PHP 7.4, but accessible by the developer like .htacess is accessible to the developer, vs. just accessible to the system administrator: https://stitcher.io/blog/preloading-in-php-74
But I don't want to fixate on my definitions; whatever we call them is fine as long as we both understand what we are talking about.
Parse the code to an AST, allowing function calls with literal arguments wherever a literal value would normally be allowed. Find all function calls to something in the MACRO namespace, evaluate them, and substitute the result into the AST. Then write the modified AST to a new PHP file, and deploy that to your server.
It is the writing of the new PHP file that I think would be a problematic. Again, from my other reply, such approaches are not composible and that is my objection to them.
-Mike
P.S. Please tell me you are not the guy who wrote Pre from preprocess.io, are you? :-o
I have a work in progress wrapper for allowing const-like syntax for global constants
static const X = $dynamicExpression;
.
This can be seen in https://github.com/TysonAndre/php-src/pull/10
Thanks for replying and including this. Nice work.
- Eagerly initialized dynamic constants are easier to reason about in some ways (e.g. fetching the constant of an already fetched class is less likely to cause pauses or exceptions.)
This makes un-noticed errors in rare code paths less likely.- Attempting to support function calls, property accesses, class constant accesses (e.g. inherited), and variables in class constants recursively and/or lazily
would probably break a lot of code/assumptions in the php engine.
Frankly my strong preference would be for on-demand initialization because otherwise each page load could end up running lots of code to calculate values that are never used on the particular page.
My preference would be to make it easier to use dynamic expressions, and recommend in coding guidelines that
certain types of expressions "SHOULD NOT" be used for constants that vary on a particular host.
(network calls, time, etc)const DEFAULT_PAYLOAD = json_encode(['key' => 'value']); const ALLOWED_ELEMENT_TYPES = array_merge(self::FIRST_TYPES, self::SECOND_TYPES); const ELEMENT_LENGTHS = array_map('strlen', self::ELEMENTS);
You and me both.
-Mike
I've also put together a different proof of concept adding support for named calls of global functions
(and only that, no other new allowed types of expressions) from within the expressions of constants.
(i.e. global constants, class constants, and parameter defaults).
I was considering putting together an RFC to allow function calls
once I'm finished implementing that,
and if no major unfixable issues are identified.
https://github.com/TysonAndre/php-src/pull/12 allows expressions such as:
<?php class X { const MESSAGE = sprintf('hello, %s', 'world'); }
This uses php's default behavior for evaluating constants,
which is delayed until the first use for class constants/param defaults,
and at the time of declaration for global constants.
This POC will throw if the constant would be invalid (same checks as define()
) (e.g. reject cycles, references, or objects),
and will also throw an Error if a function call tries to load the constant currently being defined.