Type Aliasing ¶
Table of Contents ¶
- Type Aliasing
- Opaque Type Aliasing
- Opaque Type Aliases with Constraints
- Examples
- Summary
Many programming languages allow existing types to be redefined as new type names. The C language has typedefs. OCaml has type abbreviations. PHP even has rudimentary mechanism with its class_alias() function.
Hack and HHVM are offering two ways to redefine type names: type aliasing and opaque type aliasing. Here is the syntax for each.
Type Aliasing
<?hh
type MyInt = int;
function foo(MyInt $mi): void {}
Opaque Type Aliasing
<?hh
newtype MyInt = int;
function foo(MyInt $mi): void {}
Type aliases are declared outside of classes at the top-level. Also, the grammar on the RHS of the declaration is the same as would be put for type annotations on a parameter or return type. For example, take this function signature:
<?hhfunction foo((int, int) $x): void {}
A type alias that would replace (int, int) in foo() would look like:
<?hh
newtype Point = (int, int);
function foo(Point $x): void {}
While type aliasing and opaque type aliases are very similar, there is a key difference around assignment compatibility to the original type. Unlike type aliases, Hack restricts opaque type aliases in a manner that only the file that defined the opaque type alias is allowed to access the underlying implementation.
Type Aliasing ¶
Type aliasing allows the redefining of an existing type name, but still refers to the existing type's underlying implementation. For example:
File1.php
<?hh
type NotSoSecret = int;
File2.php
<?hhrequire_once "File1.php";
function not_so_secrets_are_ints(NotSoSecret $x, NotSoSecret $y): NotSoSecret {
return $x + $y;
}
function main_ta(): void {
echo not_so_secrets_are_ints(4, 5);
}
main_ta();
The above example will output:
9
NotSoSecret is a pure alias for an int and has the all the underlying capability of an int.
Opaque Type Aliasing ¶
Opaque type aliasing bear some similarity to normal type aliases. There is a key difference, however. Unlike type aliases, Hack restricts opaque type aliases such in a manner that only the file that defined the opaque type is allowed to access the underlying implementation. For example:
File1.php
<?hh// File1.php
newtype SecretID = int;
function modify_secret_id(SecretID $sid): SecretID {
return $sid - time() - 2042;
}
function main_ot1(): void {
echo modify_secret_id(44596);
}
main_ot1();
File2.php
<?hh// File2.php
require_once "File1.php";
function try_modify_secret_id(SecretID $sid): SecretID {
return $sid + time() + 2000;
}
function main_ot2(): void {
try_modify_secret_id(44596);
}
main_ot2();
The file where SecretID is defined allows int operations such as add and subtract to be performed. However, in any other file Hack will throw an error when trying to use int operations on something declared as a SecretID
The above example will output:
File "File2.php", line 7, characters 14-17: Typing error File "File2.php", line 7, characters 14-17: This is an int/float because this is used in an arithmetic operation File "File2.php", line 6, characters 31-38: It is incompatible with an object of type SecretID
Note, however, the above code in File2.php will run fine in HHVM since at runtime SecretID is indeed just an int.
Opaque Type Aliases with Constraints ¶
Normally, an opaque type alias does not allow the underlying representation of its type to be accessed (modulo the file in which the opaque type alias is defined). This is still true in principle. However, Hack has added the ability to add type constraints to opaque type aliases. Opaque type aliases with constraints still don't allow access to the underlying representation of the type alias, but they do allow access to the representation represented by the constraint. This is best illustrated with an example.
ot_constaints_transaction_1.php
<?hh // strict
newtype TransactionActive<T as Transactable> as T = T;
abstract class Transactable {
/*
* Begin a Transaction. You must do this before you can commit or
* rollback a transaction.
*/
public static function begin<T as Transactable>(T $t): TransactionActive<T> {
$t->implBeginTransaction();
return $t;
}
/*
* Rollback, or commit. These two functions require that the type
* system has added a `TransactionActive' annotation in the form of
* a newtype---it is a statically-verifiable precondition (to the
* degree that the code using this class is in strict mode).
*/
public static function rollback<T as Transactable>(
TransactionActive<T> $t
): void {
$t->implRollback();
}
public static function commit<T as Transactable>(
TransactionActive<T> $t
): void {
$t->implCommit();
}
// Subclass for this transaction-oriented class must implement
// these.
protected abstract function implBeginTransaction(): void;
protected abstract function implRollback(): void;
protected abstract function implCommit(): void;
}
ot_constraints_transaction_2.php
<?hh// Copyright 2004-present Facebook. All Rights Reserved.
// Comment out if testing outside a normal environment
// where the files are autoloaded.
// require_once "ot_constraints_transaction_1.php";
class MySqlDB extends Transactable {
protected function implBeginTransaction(): void {}
protected function implRollback(): void {}
protected function implCommit(): void {}
}
function open_mysql(): MySqlDB {
return new MySqlDB();
}
function ot_transact_main(): void {
$db = open_mysql(); // Returns MySqlDB which extends Transactable
$db = Transactable::begin($db);
// $db now has type TransactionActive<MySqlDB>.
/* Other code here */
Transactable::rollback($db); // If anyone removes the
// begin() call, Hack will
// throw an error.}
The code above should look relatively familiar in the context of opaque type aliasing. Using newtype, an opaque type alias is declared called TransactionActive. However, this time the alias has a T constraint on it. In theot_transact_main() function in ot_constraints_transaction_2.php, $db is first set to an instance of MySqlDB (which, in turn, sets T to be MySqlDB). Then, $db is set to the TransactionActive type alias via the call toTransactable::begin(). Only at this point are the calls to rollback() and commit allowed by Hack. Even though$db is technically a Transactable (by being an instance MySqlDB), and, at runtime, could successfully call a method such as rollback(), Hack will disallow this. Calling begin() with a Transactable such as $db type alias forces all instances of T (in this case, MySqlDB) to be a TransactionActive in order to call those methods and use the underlying representation of T. Note here that the underlying representation of the type alias and the constraint are both the same, T.
Examples ¶
Here are some further examples on using type aliases and opaque type aliases.
Opaque Type Aliases and Construction ¶
Here is an example of using a opaque type aliases that defines the representation of a point on the (x,y) plane.
File1.php
<?hh // strict
newtype Point = (int, int);
function create_point(int $x, int $y): Point {
return tuple($x, $y);
}
function distance(Point $p1, Point $p2): float {
$dx = $p1[0] - $p2[0];
$dy = $p1[1] - $p2[1];
return sqrt($dx*$dx + $dy*$dy);
}
Here is the test code:
Test.php
<?hh
require_once "File1.php";
function main_tap(): void {
$p1 = create_point(3, 4);
$p2 = create_point(5, 6);
echo distance($p1, $p2);
}
main_tap();
Since a opaque type alias was used, the underlying implementation of Point (i.e., tuple) cannot be accessed outside of the file it was defined. Thus, it is important that mechanisms are added to be able to construct a representation of the opaque type. In this case, createPoint() was defined to be able to do this construction. This is the HHVM output from running the above test code:
The above example will output:
2.8284271247462
Reducing Conversion Errors ¶
This example demonstrates how opaque type aliases can help reduce conversion errors. Taking a class that deals with seconds, minutes and hours, the "old" way of creating such a class may have been:
<?hh
class TypeDefsConv {
public static function funcForSeconds(int $s): void {}
public static function funcForMinutes(int $m): void {}
public static function funcForHours(int $h): void {}
public static function convertSecondsToMinutes(int $s): int {
return $s/60;
}
public static function convertMinutesToHours(int $m): int {
return $m/60;
}
}
While a careful reading and understanding of the code is pretty clear what values should be passed to each method, and the developer may be able to create some internal checks to rule out bad values, anything can be passed to these functions. They are just ints after all. Minutes may be passed to funcForSeconds(), for example. Now look at the same class implemented using opaque type aliases:
File1.php
<?hh
newtype Seconds = int;newtype Minutes = int;newtype Hours = int;
class TypeDefsConv {
public static function funcForSeconds(Seconds $s): void {}
public static function funcForMinutes(Minutes $m): void {}
public static function funcForHours(Hours $h): void {}
public static function convertSecondsToMinutes(Seconds $s): Minutes {
return (int)($s/60);
}
public static function convertMinutesToHours(Minutes $m): Hours {
return (int)($m/60);
}
}
Any file outside of the one where the opaque type alias is declared and defined will not be able use the underlying representation to pass in errant values to the methods. Thus, to the outside world, Seconds are actually seconds and notints. Here is test code for the above that should produce a type error:
Test.php
<?hh
require_once "File1.php";
class UseTypeDefsConv {
protected Seconds $s = 0;
public function foo(): void {
$this->s = 464;
$m = TypeDefsConv::convertSecondsToMinutes($this->s);
echo $m;
TypeDefsConv::funcForMinutes($m);
}
}
function main_tdc(): void {
$utdc = new UseTypeDefsConv();
$utdc->foo();
}
main_tdc();
The above example will output:
ot_conversion_errors_2.php:7:13,19: Wrong type hint ot_conversion_errors_2.php:7:13,19: This is an object of type Seconds ot_conversion_errors_2.php:7:26,26: It is incompatible with an int
Again, the above code will run correctly in HHVM. It just won't type check correctly through the Hack typechecker.
Generics ¶
Type aliases and opaque type aliases can be used successfully with generic types. Suppose a Vector<Vector<T>>. That looks like a matrix. And it makes perfect sense to redefine the name of Vector<Vector<T>> as a Matrix<T>.
File1.php
<?hh
type Matrix<T> = Vector<Vector<T>>;
function bar_ta_gen<T>(Matrix<T> $x): void {
var_dump($x);
}
File2.php
<?hh
require_once "File1.php";
function foo_ta_gen(Vector<Vector<float>> $x): void {
bar_ta_gen($x); // Vector<Vector<float>> is identical to Matrix<float>}
function foo_ta_gen_main(): void {
$vecvec = Vector {Vector {1.0, 2.0}, Vector {3.0, 4.0}};
foo_ta_gen($vecvec);
}
foo_ta_gen_main();
Phantom Types ¶
Take this piece of code that includes a type alias:
<?hh
// A simplified serialization api:type Serialized<T> = string;
class FooPhantom {}
function serialize_phantom<T>(T $t): Serialized<T> {
return serialize($t);
}
function unserialize_phantom<T>(Serialized<T> $s): T {
return unserialize($s);
}
// Using the api:function main_phantom(): void {
$x = new FooPhantom();
var_dump($x);
// $serialized is a Serialized<Foo>, aka "string"
$serialized = serialize_phantom($x);
var_dump($serialized);
// we now know the type of $y must be Foo
$y = unserialize_phantom($serialized);
var_dump($y);
}
main_phantom();
The above example will output:
object(FooPhantom)#1 (0) { } string(22) "O:10:"FooPhantom":0:{}" object(FooPhantom)#2 (0) { }
This example shows what is called a "phantom" type. The reason for this is that the right hand side of the typedef does not explicitly mention the type parameter T used on the left hand side of the typedef. In reality, Serialized<T> is just a string. However, the typechecker carries around the information about what type a use of the typedef had originally. For example, calling serialize_phantom() with a FooPhantom allows the typechecker to remember that a FooPhantomwas the original type. Thus a call to unserialize_phantom() will return a FooPhantom().
Enums ¶
Opaque type aliases can be used to mimic C++ style enumerations.
File1.php
<?hh
newtype Color = int;
class ColorEnum extends Enum<Color> {
const Color BLUE = 1;
const Color RED = 2;
const Color GREEN = 3;
public static function getColor(Color $color) {
switch ($color) {
case 1: return "0000ff";
case 2: return "ff0000";
case 3: return "00ff00";
}
}
File1.php
<?hh
require_once "File1.php";
function give_me_a_box(Color $color): :ui:box {
return <ui:box color={ColorEnum::getColor($color)}>
This is really a silly example. I hope you will not actually write code like this...
</ui:box>;
}
Summary ¶
Type aliasing and opaque type aliasing provide PHP developers a clean way to redefine type names (a la C typedefs). Type aliases are just that, aliases. There are no restrictions to the access of the existing type on which the alias was created. Opaque type aliases have more restrictions. As far as the Hack typechecker is concerned, access to the existing type can only occur in the file where the opaque type alias is defined. However, at runtime, HHVM will still consider an opaque type alias to be the underlying type at run time as far as operations, etc. are concerned.
No comments:
Post a Comment