Claudiu Persoiu

Blog-ul lui Claudiu Persoiu


Collecting Hack

without comments

Stamp-Collection

Something I didn’t approach in my last blog is collections. Hack comes with a variety of collections for organizing data.

Data structures represent a fundamental part of a programming language, because they will determine the information flow in the application.

PHP up to version 5 had a single type for data collections, called “array”. This data type can have three uses: arrayhash table, or a combination of the two.

To facilitate the construction of new structures, a number of  iteratori were introduced in PHP 5. Unfortunately, the resulting structures had the purpose of accessing objects in a similar fashion with arrays.

Not until PHP 5.3 data structures like  SplStack and many others that are truly different were introduced.

However, structures like vectors and tuples were never natively introduced. They can be built, but it is neither simple, nor intuitive.

HHVM’s Hack comes with a different approach, a series of  native collections that are ready to be used.

Collection types

The list of collections is:

  • Vector – indexed list of items,
  • Map – dictionary type hash table,
  • Set – list of items that only stores unique values
  • Pair – a particular vector case that only has two elements.

Vector, Map, and Set also have immutable (read-only) equivalents. These are:  ImmVectorImmMap and ImmSet. The purpose of these data types is to expose the information for reading purposes and not allow modifications. An immutable collection can be directly generated using the constructor, or using the methods: toImmVector, toImmMap and respectively toImmSet.

Even more, there are a series of abstract classes to help easily implement similar structures:

 

Vector

The advantage of a vector is that it will always have the keys in sequence and the order of the elements is not going to change. When it comes to arrays, there isn’t any simple way to check if it should behave as a hash table or as a vector. For vectors, unlike for hash tables, the key value is not relevant, only the sequence and the number of elements are important.

Let’s take an example:

<?hh

function listVector($vector) {
     echo 'Listing array: ' . PHP_EOL;
     for($i = 0; $i < count($vector); $i++) {
          echo $i . ' - ' . $vector[$i] . PHP_EOL;
     }
}

$array = array(1, 2, 3);

listVector($array);

// eliminating an element from the array
unset($array[1]);

listVector($array);

The result will be:

Listing array:
0 - 1
1 - 2
2 - 3
Listing array:
0 - 1

Notice: Undefined index: 1 in ../vector.hh on line 6
1 -

The reason is very simple: count returns the real number of elements, but the index is not guaranteed sequential. When the second element of the array was removed, the number of elements was reduced by one, but the index with value 1 was no longer set and the last index is equal to the size of the array, so it will never be reached.

Let’s take the same example but using a vector:

<?hh
… 
$vector = Vector{1, 2, 3}; 

listVector($vector); 

// eliminating an element from the vector
$vector->removeKey(1); 

listVector($vector);

Like we anticipated, the result is:

Listing array:
0 - 1
1 - 2
2 - 3
Listing array:
0 - 1
1 - 3

It is worth mentioning that “unset” can not be used, because it is not a key to be eliminated, but the element itself, and the next value in the vector will take its’ place.

Another important thing to mention is that when an index doesn’t exist, an exception of the type “OutOfBoundsException” will be thrown.

Some examples that will trigger the exception above:

<?hh 

$vector = Vector{1,2,3,4}; 

// it will work because the key with value 1 exists 
$vector->set(1, 2);

// it will not work because the key with value 4 doesn't exist yet
$vector->set(4, 5);

// it will not work for the same reason as above
$vector[4] = 5;

// for addition only method that don't provide the key work
$vector[] = 5;

// or
array_push($vector, 5);

For accessing elements, the “OutOfBoundsException” problem remains the same. For instance, if the index 10 doesn’t exist:

var_dump($vector[$unsetKey]);

Another more special case is when the element doesn’t exist, but the method “get” is used:

var_dump($vector->get($unsetKey));

The example above will not generate an error, but the result will be “null” when the key doesn’t exist. I find this strange, because an element with the value null can exist in the vector, and the result will be the same.

To avoid the confusion between undefined elements and elements that are null, there is a special method to check if the key exists:

var_dump($vector->containsKey($unsetKey));

Removing elements from the vector is done with:

$vector->remove($key);

Or to remove the last element:

$vector->pop();

Map

In a hash table, unlike a vector, the order of the elements is not very relevant, but the key-value association is very important. For this reason, a Map is also called a “dictionary”, because you can easily get from a key to a value, since they are “mapped”, hence the name “Map”.

The HHVM implementation will also retain the order in which the elements were introduced.

In PHP, the equivalent of a Map is an associative array.

Unlike Vector, Map needs a key that will permanently be bind with the element, even if new values are added or removed from the collection.

The functions array_push or array_shift will not work for Map, because a key is not sent and the key-value association would not be controlled:

<?hh 

$map = Map{0 => 'a', 1 => 'b', 3 => 'c'};

array_push($map, 'd');

array_unshift($map, 'e');

var_dump($map);

Will generate the following result:

Warning: Invalid operand type was used: array_push expects array(s) or collection(s) in ../map.hh on line 5

Warning: array_unshift() expects parameter 1 to be an array, Vector, or Set in ../map.hh on line 7
object(HH\Map)#1 (3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [3]=>
  string(1) "c"
}

As you can see, the elements were not added and each of the cases generated a Warning.

The actual insert can be done using:

<?hh 

$map = Map{0 => 'a', 1 => 'b', 3 => 'c'};

// adding an element using the array syntax
$map['new'] = 'd';

// adding an element using the method provided by the structure
$map->set('newer', 'e');

var_dump($map);

The result will be:

object(HH\Map)#1 (5) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [3]=>
  string(1) "c"
  ["new"]=>
  string(1) "d"
  ["newer"]=>
  string(1) "e"
}

Unlike Vector, because the element is closely linked with the key, unset is a viable method for removing an element:

unset($map[$key]);

The structure also has a method for removing the element with a particular key:

$map->remove($key);

For this case, none of the options will generate an error, if the key is not set.

The “OutOfBoundsException” exception is also found here for keys that are not defined, and just like for Vectors, there is a method to test if the key exists:

$map->contains($key);

Similarly to Vector, there is a method that will return true if the key exists and null if not:

$map->get($key);

To make sure that a “OutOfBoundsException” will not be raised, a loop over a Map should not be done using “for” , but rather “foreach”.

Because the vector’s method “pop” does not use a key, it isn’t present in the Map structure.

Set

Set has the purpose of keeping the values unique. For this structure, the values are restricted to the scalar types: string and integer.

The interface for this structure is much simpler than Vector and Map, because the purpose is a lot more limited.

For Sets the key can not be accessed, but it is relevant in a special way.

Let’s take an example to illustrate this:

<?hh
$set = Set{'a', 'b', 'c'}; 

foreach($set as $key => $val) {
     echo $key . ' - ' . $val . PHP_EOL;
}

The result will be:

a - a
b - b
c - c

The key and the value are identical, a clever way to keep unicity.

However, the process is transparent, fact that allows adding elements without a need for a key:

<?hh

$set = Set{'a', 'b', 'c'};

array_push($set, 'd');

array_unshift($set, 'e');

$set[] = 'f';

var_dump($set);

There will be a result similar to the one from vectors:

object(HH\Set)#1 (6) {
  string(1) "e"
  string(1) "a"
  string(1) "b"
  string(1) "c"
  string(1) "d"
  string(1) "f"
}

Even though new values can be added using the “[]” operator, they can’t be referenced using this operator:

<?hh

$set = Set{'a', 'b', 'c'};

echo $set['a'];

It will generate the following error:

Fatal error: Uncaught exception 'RuntimeException' with message '[] operator not supported for accessing elements of Sets' in ../set.hh:5
Stack trace:
#0 {main}

For removing elements only the native method (remove) and methods that don’t require a key can be used:

<?hh 

$set = Set{'a', 'b', 'c', 'd'}; 

array_pop($set); 

array_shift($set); 

$set->remove('b');

var_dump($set);

The result will be:

object(HH\Set)#1 (1) {
  string(1) "c"
}

Unlike Vector and Map, the “remove” method will receive the value to be removed, not the key.

For Set there isn’t any access key for elements, therefore about all we can do is to check if an element exists, using “contains”:

$set->contains($value);

The method will return a bool showing if the element exists or not.

Pair

A pair is a collection with two elements. It can’t have more or fewer. Just like in Vectors, the elements are indexed using a key that in this particular case can have only two values 0 and 1.

There aren’t a lot of things to be said about this data structure, because the elements can not be removed, added or replaced. This is the reason why it doesn’t have an immutable equvelent, because the structure itself is not flexible:

<?hh 

$pair = Pair{'a', 'b'}; 

foreach($pair as $key => $val) {
     echo $key . ' - ' . $val . PHP_EOL;
}

The result will be:

0 - a
1 - b

A very simple structure for a very simple purpose.

Common ground

Almost all structures presented above have few common methods and behaviors. Almost all, because Set and especially Pair are more restrictive through their nature and lack some features which Vector and Map have.

Filter

It’s a filtering function that comes from functional programming. The purpose is to filter a data structure and to generate a new one of the same type. The exception is Pair, because of the number of elements restriction. The equivalent in PHP is array_filter.

Vector and Map have two methods: filter and filterWithKey. These methods take an argument of type “callable”, in other words a function:

<?hh 

$vector = Vector{'a', 'b', 'c', 'd', 'e'}; 

// eliminate the element with value 'a' 
$result = $vector->filter($val ==> $val != 'a');

// eliminate every other element using the key
$result2 = $vector->filterWithKey(($key, $val) ==> ($key % 2) == 0);

var_dump($vector);
var_dump($result);
var_dump($result2);

The result will be:

object(HH\Vector)#1 (5) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
  [3]=>
  string(1) "d"
  [4]=>
  string(1) "e"
}
object(HH\Vector)#3 (4) {
  [0]=>
  string(1) "b"
  [1]=>
  string(1) "c"
  [2]=>
  string(1) "d"
  [3]=>
  string(1) "e"
}
object(HH\Vector)#5 (3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "c"
  [2]=>
  string(1) "e"
}

As you’ve noticed, the result of the “callable” function is treated as a bool and according to this the elements are added to the resulting structure.

Map has an identical behavior with Vector, the only difference is in the nature of the keys.

Something interesting is that a collection can also be immutable, because the operation doesn’t modify the original structure, but the it will also have the type of the original structure:

<?hh 

$vector = Vector{'a', 'b', 'c'}; 

$vector = $vector->toImmVector();

// eliminate the element with value 'a'
$result = $vector->filter($val ==> $val != 'a');

var_dump($vector);
var_dump($result);

The result will be:

object(HH\ImmVector)#2 (3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}
object(HH\ImmVector)#4 (2) {
  [0]=>
  string(1) "b"
  [1]=>
  string(1) "c"
}

 

Pair also has the same functions as Vector and Map, but the behavior is not identical, because of the fact that Pair can only have 2 elements, no less, no more. For this reason, when a Pair is filtered, the result will be ImmVector, a similar structure with Pair but with a variable number of elements:

<?hh 

$pair = Pair{'a', 'b'}; 

// eliminate the element with value 'a'
$result = $pair->filter($val ==> $val != 'a'); var_dump($result);

The resulting structure will be:

object(HH\ImmVector)#3 (1) {
  [0]=>
  string(1) "b"
}

Set only has the “filter” method, because, as was demonstrated earlier, the keys are identical with the values. If it had had a method with keys, it would have worked the same.

Map

Another function coming from functional languages is “Map”. This aims to modify the values of a structure using a function, the resulting structure having the type of the source. In PHP, the equivalent is array_map.

Similarly with filter, Vector and Map have the common methods: “map” and “mapWithKey”. In this case also, they take a “callable” as an argument:

<?hh 

$vector = Vector {'a', 'b', 'c'}; 

$result = $vector->map($val ==> $val . $val);

$result2 = $vector->mapWithKey(($key, $val) ==> str_repeat($val, 1 + $key));

var_dump($vector);
var_dump($result);
var_dump($result2);

The result will be:

object(HH\Vector)#1 (3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}
object(HH\Vector)#3 (3) {
  [0]=>
  string(2) "aa"
  [1]=>
  string(2) "bb"
  [2]=>
  string(2) "cc"
}
object(HH\Vector)#5 (3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(2) "bb"
  [2]=>
  string(3) "ccc"
}

The result of the “callable” function is the new value of the element in the structure.

Just like with “filter”, an immutable collection will result in a new immutable collection.

Also similar with “filter” is the fact that the “map” function applied to a Pair will result in an ImmVector:

<?hh 

$pair = Pair{'a', 'b'}; 

$result = $pair->map($val ==> $val . $val);

var_dump($result);

Will result in:

object(HH\ImmVector)#3 (2) {
  [0]=>
  string(2) "aa"
  [1]=>
  string(2) "bb"
}

Conversion

Some of the elements can be converted to different types:

from \ ro Vector Map Set Pair Array
Vector x x x x
Map x x x x
Set x x x
Pair x x x x
Array x x x x

There are several structural restrictions to the table above:

1. Any structure that is getting converted to Set must only contain scalar values of type int and string:

(Map{})->add(Pair {'a', new stdClass()})
    ->toSet();

Will generate the error:

Fatal error: Uncaught exception 'InvalidArgumentException' with message 'Only integer values and string values may be used with Sets' in …

2. When a Map is converted to any other structure, except array, it will loose the keys in most cases.

The conversion from an array to other structures is done using:

$vector = new Vector ($array);

Beside Pair, all structures above have a single parameter that implements Traversable for the constructor.

Conclusions

Hack brings a new perspective over the most popular data type in PHP. Facebook’s reason is a simple one, optimization. If you have a consistent behavior, that particular structure can be optimized. In PHP that’s not exactly possible, because of the fact that an array in PHP can be any type of collection.

From the data structure point of view, I find it interesting to have this kind of data types. In frameworks, there are usually structures that emulate the behavior of the collections introduced by Hack. For instance in an ORM, a collection of objects is usually represented as a vector, because the purpose is to iterate over its’ values. An object that represents the values of the fields from a table will be a Map like structure, because the value of the field is closely related to the field name.

I find it very interesting not only that now we have this structures, but also that we have the interfaces to implement new ones.

I hope Hack will influence PHP to bring purpose specific structures into the language.

Written by Claudiu Persoiu

30 May 2014 at 11:50 AM

Posted in PHP

Tagged with , , , , , , ,

Leave a Reply