Pages

Wednesday, March 26, 2014

Type Aliasing

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((intint$x): void {}
A type alias that would replace (int, int) in foo() would look like:
<?hh
newtype Point 
= (intint);
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 $xNotSoSecret $y): NotSoSecret {
  return 
$x $y;
}
 
function 
main_ta(): void {
  echo 
not_so_secrets_are_ints(45);
}
 
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<as Transactable> as T;
 
abstract class 
Transactable {
  
/*
   * Begin a Transaction.  You must do this before you can commit or
   * rollback a transaction.
   */
  
public static function begin<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<as Transactable>(
    
TransactionActive<T$t
  
): void {
    
$t->implRollback();
  }
  public static function 
commit<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 = (intint);
 
function 
create_point(int $xint $y): Point {
  return 
tuple($x$y);
}
 
function 
distance(Point $p1Point $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(34);
  
$p2 create_point(56);
  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->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.02.0}, Vector {3.04.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): {
  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 exampleI 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