Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reusable property hooks #167

Open
thekid opened this issue May 19, 2023 · 2 comments
Open

Reusable property hooks #167

thekid opened this issue May 19, 2023 · 2 comments

Comments

@thekid
Copy link
Member

thekid commented May 19, 2023

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

@thekid
Copy link
Member Author

thekid commented May 19, 2023

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 be NULL
  • 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

@thekid
Copy link
Member Author

thekid commented May 21, 2023

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.

@thekid thekid added the php label Mar 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant