Experimental Features: Read Only

This page describes the readonly experimental feature.

What is it?

readonly is a keyword that prohibits mutability on Objects and their properties.

Enabling readonly

In your relevant file(s), add <<file:__EnableUnstableFeatures('readonly')>>.

How does it work?

When an Object has the readonly keyword applied (e.g. private readonly Foo $x;, there are two new constraints on that Object.

  • Readonlyness: Object properties can not be modified (i.e. mutated).
  • Deepness: All nested properties are readonly.

Readonlyness

Object properties can not be modified (i.e. mutated).

function test(readonly Foo $x) : void {
  $x->prop = 4; // error, $x is readonly, its properties can not be modified
}

Deepness

All nested properties are readonly.

class Bar {
  public function __construct(
    public Foo $foo,
  )
}

class Foo {
  public function __construct(
    public int $prop,
  )
}

function test(readonly Bar $x) : void {
  $foo = $x->foo;
  $foo->prop = 3; // error, $foo is readonly
}

How is it different from Coeffects?

Coeffects affects an entire function (and all the functions it calls), whereas readonly affects values / expressions.

Applications

This is a list of all of the valid locations of the keyword.

Parameters and return values

Parameters and return values of any callable can be marked readonly.

function foo(readonly Foo $x): readonly Bar {
  return $x->bar;
}

A readonly parameter signals that the function/method will not modify that parameter; a readonly return type signals that the function returns a readonly reference to an object that can not be modified.

Static and regular properties

Static and regular properties can be marked readonly and, when applied, can not be modified.

class Foo {
  private readonly Bar $bar;
  private static readonly Bar $static_bar;
}

Other restrictions

Accessing a readonly property (i.e. a property that’s marked readonly at the declaration, not accessing a property off of a readonly object) requires readonly annotation.

class Foo {
  public function __construct(
    readonly Bar $bar,
  )
}

function test(Foo $f): void {
  $bar = readonly $f->bar; // this is required
}

And, class properties can not be set to readonly values unless they are readonly properties.

class Foo {
  readonly Bar $ro_prop;
  Bar $mut_prop;
}

function test(
  Foo $x,
  readonly Bar $bar,
) : void {
  $x->mut_prop = $bar; // error, $bar is readonly but the prop is mutable
  $x->ro_prop = $bar; // ok
}

Lambdas and function type signatures

readonly is allowed on inner parameters and return types on function typehints.

function call(
    (function(readonly Bar) : readonly Bar) $f,
    readonly Bar $arg,
   ) : readonly Bar {
   return $f($arg);
}

Expressions

readonly can appear on expressions.

function foo(): void {
  $x = new Foo();
  $y = readonly $x;
}

Functions / Methods

readonly can appear as a modifier on instance methods, signaling that $this is readonly.

class C {
  public readonly function bar(): Bar {
    return new Bar();
  }
  public readonly function foo() : void {
    $this->prop = 5; // error, $this is readonly.
  }
}

Other restrictions

Readonly objects can only call readonly methods.

class C {
  public readonly function bar(): Bar {
    return new Bar();
  }
  public readonly function foo() : void {
    $this->prop = 5; // error, $this is readonly.
  }
}

Readonly values can not be passed to a function that takes mutable values.

function takes_mutable(Foo $x): void {
  $x->prop = 4;
}

$z : readonly
takes_mutable($z); // error, takes_mutable's first parameter
                   // is mutable, but $z is readonly

And, functions can not return readonly values unless they are marked to return readonly.

function returns_mutable(readonly Foo $x): Foo {
  return $x; // error, $x is readonly
}

Closures and function types

A function type can be marked readonly: (readonly function(T1): T). Denoting a function/closure as readonly adds the restriction that the function/closure captures all values as readonly:

function readonly_closure_example(): void {
  $x = new Foo();
  $f = readonly () ==> {
    $x->prop = 4; // error, $x is readonly here!
  };
}

One way to make sense of this behavior is to think of closures as objects with an __invoke function (which is how HHVM implements all closures), and whose properties are the values it captures. A readonly closure is then defined as a closure whose __invoke function is annotated with readonly.

Readonly closures affect Hack’s type system, where readonly closures are subtypes of their mutable counterparts. Specifically, a (readonly function(T1):T2) is a strict subtype of a (function(T1): T2).

readonly (function (): T) versus (readonly function(): T): references vs. objects

A (readonly function(): T) may look very similar to a readonly (function(): T), but they are actually different. The first denotes a readonly closure object, which at definition time, captured readonly values. The second denotes a readonly reference to a regular, mutable closure object:

function readonly_closures_example2(
  (function (): T) : $regular_f,
  (readonly function(): T): $ro_f,
) : void {
  $ro_regular_f = readonly $regular_f; // readonly (function(): T)
  $ro_f; // (readonly function(): T)
  $ro_ro_f = readonly $ro_f; // readonly (readonly function(): T)
}

Since calling a mutable closure object can modify itself (and its captured values), a readonly reference to a regular closure cannot be called.

function readonly_closure_call(
  (function (): T) : $regular_f,
  (readonly function(): T): $ro_f,
) : void {
  $ro_regular_f = readonly $regular_f; // readonly (function(): T)
  $ro_regular_f(); // error, $ro_regular_f is a readonly reference to a regular function
}

But a readonly closure object can have readonly references and call them, since they cannot modify the original closure object on call:

function readonly_closure_call2(
  (function (): T) : $regular_f,
  (readonly function(): T): $ro_f,
) : void {
  $ro_regular_f = readonly $regular_f; // readonly (function(): T)
  $ro_regular_f(); // error, $ro_regular_f is a readonly reference to a regular function
  $ro_ro_f = readonly $ro_f; // readonly (readonly function(): T)
  $ro_ro_f(); // safe
}

Interactions with Coeffects

If your function only has the ReadGlobals capability (i.e. is marked read_globals) it can only access class static variables if they are wrapped in a readonly expression:

function read_static()[read_globals]: void {
  $y = readonly Foo::$bar; // keyword required
}
function read_static()[policied]: void {
  $y = readonly Foo::$bar; // keyword required
}

Calling a readonly function

Calling a function or method that returns readonly requires wrapping the result in a readonly expression.

function returns_readonly(): readonly Foo {
  return readonly new Foo();
}

function test(): void {
  $x = readonly returns_readonly(); // this is required to call returns_readonly()
}

Converting to non-readonly

Sometimes you may encounter a readonly value that isn’t an object (i.e. a readonly int, due to the deepness property of readonly). In those cases, instead of returning a readonly int, you’ll want a way to tell Hack that the value you have is actually a value type. You can use the function HH\Readonly\as_mut() to convert any primitive type from readonly to mutable.

Use HH\Readonly\as_mut() strictly for primitive types and value-type collections of primitive types (i.e. a vec of int).

class Foo {
  public function __construct(
    public int $prop,
  )

  public readonly function get() : int {
    $result = $this->prop; // here, $result is readonly, but its also an int.
    return HH\Readonly\as_mut($this->prop); // convert to a non-readonly value
  }
}