compiler
compiler copied to clipboard
Reusable property hooks
As noted in the "Future Scope" section of the property hooks RFC, reusable package hooks are not part of the first RFC, but the authors @iluuu1994 and @Crell envision it being added later. Here are some ideas of how it could be implemented.
Swift
The concept here is called property wrappers. Here's an example from their documentation:
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
rectangle.height = 10 // Width is set to 10
rectangle.height = 24 // Width is set to 12
See https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers
Kotlin
The concept here is called property delegates. Here's an example of how these are implemented:
// Syntax
var name: String by NameDelegate()
// Compiled to
val name$delegate = NameDelegate()
var name: String
get() = name$delegate.getValue(this, this::name)
set(value) { name$delegate.setValue(this, this::name, value) }
See https://blog.kotlin-academy.com/kotlin-programmer-dictionary-field-vs-property-30ab7ef70531
Idea for PHP
Going down the same path as Kotlin (which the PHP RFC is inspired quite a bit from) but without introducing any new keywords, we could come up with the following:
class Environment {
// Syntax with "as" (doesn't conflict with syntax highlighting like "use" would)
public string $home as new ByLazy(fn() => getenv('HOME'));
// Compiled to the following which already works with PR #166 merged
private $__home_delegate= new ByLazy(fn() => getenv('HOME'));
public string $home {
get => $this->__home_delegate->get($this, (object)['value' => &$field]);
set => $this->__home_delegate->set($this, (object)['value' => &$field], $value);
}
}
The ByLazy
implementation is as follows:
class ByLazy {
public function __construct(private callable $init) { }
public function get($self, $property) {
return ($property->value??= [($this->init)()])[0];
}
public function set($self, $property, $value) {
$property->value= [$value];
}
}
Note: Using an array for a value will allow initializing the property to NULL.
See also
- https://github.com/xp-framework/compiler/pull/166
- https://www.swiftbysundell.com/articles/property-wrappers-in-swift/
- https://kotlinlang.org/docs/delegated-properties.html
- https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/
Proof of concept implementation
See https://gist.github.com/thekid/dc12c4c4f4cf3f971b7dbbf4a5cd83b4
The property object holds the following:
-
name
- property name as a string -
type
- type as a string, may beNULL
-
value
- a reference to the property value (as seen above)
Delegates
These could be added to a lang.delegate
package:
class ByLazy {
public function __construct(private callable $init) { }
public function get($self, $property) {
return ($property->value??= [($this->init)()])[0];
}
public function set($self, $property, $value) {
$property->value= [$value];
}
}
class InitOnly {
public function get($self, $property) {
return $property->value[0];
}
public function set($self, $property, $value) {
if ($property->value) throw new IllegalStateException('Can not be modified after initialization');
$property->value= [$value];
}
}
class Observable {
public function __construct(private callable $observer) { }
public function get($self, $property) {
return $property->value;
}
public function set($self, $property, $value) {
if (false === ($this->observer)($property->value, $value)) return;
$property->value= $value;
}
}
class Configured {
public function __construct(private Properties $config, private ?string $section= null) { }
public function get($self, $property) {
if (null === $property->value) {
$setting= preg_replace_callback('/([a-z]+)([A-Z])/', fn($m) => $m[1].'-'.strtolower($m[2]), $property->name);
$property->value= match ($property->type) {
'bool' => $this->config->readBool($this->section, $setting),
'int' => $this->config->readInteger($this->section, $setting),
'float' => $this->config->readFloat($this->section, $setting),
'string' => $this->config->readString($this->section, $setting),
'array' => $this->config->readArray($this->section, $setting),
};
}
return $property->value;
}
}
class Delegate {
private static $INITONLY;
public static function initonly() { return self::$INITONLY??= new InitOnly(); }
}
Lazy example
class Environment {
public string $user as new ByLazy(function() {
Console::writeLine('Getting environment variable');
return getenv('USER');
});
}
$env= new Environment();
isset($argv[1]) && $env->user= $argv[1];
Console::writeLine($env->user); // Gets env var unless initialized above
Console::writeLine($env->user); // Prints cached copy
Init only example
class Person {
public string $name as Delegate::initonly();
public function __construct($name) { $this->name= $name; }
}
$person= new Person($argv[1]);
try {
$person->name= 'Modified';
} catch (IllegalStateException $e) {
Console::writeLine('Caught expected ', $e);
}
Console::writeLine($person->name);
Observabe example
class Employee {
public Money $salary= new Money(0, Currency::$EUR) as new Observable(function($old, $new) {
if ($old->compareTo($new) < 0) {
Console::writeLine('Prevented salary cut from ', $old, ' -> ', $new, '!');
return false;
}
Console::writeLine($this->name, '\'s salary changing from ', $old, ' -> ', $new);
});
public function __construct(private $name) { }
}
$emp= new Employee('Test');
$emp->salary= new Money(100_000, Currency::$EUR); // Test's salary changing ...
$emp->salary= new Money(90_000, Currency::$EUR); // Prevented salary cut ...
Console::writeLine($emp->salary); // 100,000.00 EUR
Configuration example
title=Test
os[]=Windows
os[]=MacOS
os[]=Un*x
new-window=true
class Preferences {
public string $title as $this->configured;
public array $os as $this->configured;
public bool $newWindow as $this->configured;
public function __construct(private Configured $configured) { }
}
$pref= new Preferences(new Configured(new Properties('config.ini')));
Console::writeLine('Title "', $pref->title, '"'); // Title "Test"
Console::writeLine('OS ', $pref->os); // OS ["Windows", "MacOS", "Un*x"]
Console::writeLine('New window? ', $pref->newWindow); // New window? true
It would be great to have a builtin Property
class instead of the stdClass created by the (object)
cast. I thought of using the ReflectionProperty
class here, too.
For some added safety, delegates could be forced to implement a builtin ReadProperty
interface if they only implement get
, and ReadWriteProperty
if they wish to implement both get
and set
.