November 18, 2012

XE3: Adding properties to a set

[Français]
Delphi XE3 has a very interesting new feature: the record helpers.

Despite his name which reference a specific data type (record), a record helper can be applyed to most standard data types as well, include the sets.

Using a record helper, we will be able to add properties and methods to sets.

In this artcile, I will show you how it works using a simple case: adding a property to a set allowing the developer to convert the set to an integer or to assign a set with an integer.

You'll ask: Is it really useful in the real world ?

Yes it is! Here is the story which led me to this feature. I was working on a driver for a input/output card (I/O card). This card was driving an industrial machine. Most I/O card have "registers" which are used to program the hardware feature. Each bit in a register has a specific function such as turning on a relay or starting a moter, or reading a limit switch. Such a register can easily been represented by a Delphi set. Each bit correspond to a value in the enumeration which is the base of the set. When you read or write a register in the I/O card, you don't read or write a set but an integer. You have to add some code to transfrom the integer to/from the set so that inside the whole Delphi program, things are readable and easy to handle.

So this is the story. Of course I won't make things complex here and will translate this real world situation to something easier to grasp for anyone not accustomed to industrial control. I will replace the register by a basket with some fruits.

The basket is somewhat special: it can contain zero or more fruits, but at most one fruit of each kind taken from a fruit list.

Translated to Delphi, this gives:
type
 TFruit = (frApple, frPear, frApricot, frCherry);
 TFruits = set of TFruit;
Having those declarations, we can create variables and handle the set of fruits:
var
  Basket : TFruits;
begin
  Basket := [frCherry, frPear];
  Basket := Panier + [frApple];
  if frPear in Basket then
     Memo1.Lines.Add('Basket contain a pear');
end;
This is actually standard Pascal as it has always existed. You'll find a lot of article on the net which will describe the set usage using Pascal.
But Delphi XE3 has much more features than standard Pascal! The one wi'll speak in a moment is the possibility to add methods and properties to a set. I will show you now how to apply this feature to convert our fruit basket into an integer and the reverse.

Let's be clear: there are a lot of ways doing this kind of conversion. I will use thoe one which is the most respectful of the language because it doesn't make any assumption about how the compiler internal represents a set.

The goal is to be able to write this kind of code:

var
   Basket : TFruits;
   N : Integer;
begin
   Basket := [frCherry, frPear];
   // Convert to an integer
   N := Basket.Integer;
   // Convert from an integer
  Basket.Integer := 5;
end;

Look at the notation: "Basket.Integer". It is just like the Basket variable is a class and this class has a property named Integer. But Basket is not a class, it is a set.

Here is how to do that:

type  
  TFruitsHelper = record helper for TFruits
  strict private
    function  ToInteger : Integer;
    procedure FromInteger(Value : Integer);
  public
    property Integer : Integer read  ToInteger
                               write FromInteger;
  end;

implementation

function TFruitsHelper.ToInteger : Integer;
var
   F : TFruit;
begin
   Result := 0;
   for F := Low(TFruit) to High(TFruit) do begin
     if F in Self then
       Result := Result or (1 shl Ord(F));
   end;
end;

procedure TFruitsHelper.FromInteger(Value: Integer);
var
  F : TFruit;
begin
  Self := [];
  for F := Low(TFruit) to High(TFruit) do begin
    if (Value and (1 shl Ord(F))) <> 0 then
      Self := Self + [F];
  end;
end;


That's it! The code assign a single bit into the integer to each of the enumerated type element. To do that, I have used:

  1. A loop for F := Low(TFruit) to High(TFruit) do used to scan the enumeration items without ever specfying any identifier;
  2. The intrinsic function Ord() which returns the index of the element in the enumerated type (frApple=0, frPear=1 and so on);
  3. The operator shl which shift a number to the left. The shithed number is 1 here. The result is a bit set to 1 at the position in the integer given by the index.
  4. The operator in which tells if a given enumeration element is or isn't present in the set.
  5. The operator and which I use to mask the interger bits to know if one specific bit is 1 or not.
Done...

Notes:
  1. This code assume the set is small enough to be contained in an integer (32 bit). You can easily change it to support 64 bit integer. My initial target was to represent an I/O card register and they almost always fit in an integer.
  2. Internally Delphi compiler is using a single bit to represent an enumeration item inside the set. Knowing that, it is possible to change the code to take advantage of it. It is faster but dependent on the internal implementation and could be broken in a future release. The code could look like this:
  function TFruitsHelper.ToInteger: Integer;
  begin
    {$IF SizeOf(TFruits) = SizeOf(Byte)}
      Result := PByte(@Self)^;
    {$ELSEIF SizeOf(TFruits) = SizeOf(Word)}
      Result := PWord(@Self)^;
    {$ELSEIF SizeOf(TFruits) = SizeOf(Integer)}
      Result := PInteger(@Self)^;
    {$ELSE}
      {$MESSAGE FATAL 'TFruits cannot be represented as an integer'}
    {$IFEND}
  end;
--
François Piette
Embarcadero MVP
http://www.overbyte.be



November 17, 2012

XE3: Ajouter des propriétés aux ensembles

[English]
Delphi XE3 dispose d'une nouvelle fonctionnalité particulièrement intéressante dès lors qu'on veut bien y regarder de plus près. Je veux parler de l'assistance d'enregistrements ("record helper" en anglais).

Malgré son nom qui évoque un type de donnée spécifique, les enregistrements, l'assistance d'enregistrements s'applique aussi à beaucoup de type de données intrinsèques de Delphi dont les ensembles (construction "set of ...").

L'assistance d'enregistrement va nous permettre d'ajouter à un ensemble une ou plusieurs propriétés ou méthode.

L'exemple simple que je vais développer dans cet article concerne l'ajout d'une propriété à un ensemble. Propriété qui va permettre d'initialiser une ensemble depuis un nombre entier, ou d'obtenir un nombre entier à partir d'un ensemble.

Vous me direz : "A quoi cela peut-il bien servir dans le monde réel ?".

C'est simple... J'ai été amené à écrire ce code parce que je travaillais sur un programme qui pilotais une carte d'entrée/sortie (E/S). Cette carte d'E/S dispose de registres dont chaque bit a une signification particulière. Représenter cela en Delphi est assez facile, c'est l'exemple même du cas d'application d'un ensemble : on donne un nom symbolique et parlant à chaque bit; un registre étant finalement un ensemble de bit, et bien nous avons une adéquation parfaite entre la construction "set of" de Delphi et le registre.

Je vais vous faire grâce des détails de ma carte d'E/S et vais basculer sur un exemple plus simple mais tout à fait équivalent. Je vais parler de fruits (les bits du registre) et d'un panier de fruits (Le registre lui-même). Panier spécial puisqu'il peut contenir plusieurs fruits, mais seuelemnt zéro ou un fruit d'un type déterminé.

En Delphi, cela donne:

type
  TFruit  = (frPomme, frPoire, frAbricot, frCerise);
  TFruits = set of TFruit;

Ayant ces deux déclarations, on peut créer des variables et manipuler l'ensemble TFruits:

var
  Panier : TFruits;
begin
  Panier := [frCerise, frPoire];
  Panier := Panier + [frPomme];
  if frPoire in Panier then
    Memo1.Lines.Add('Il y a une poire dans le panier');
end;


Tout cela, c'est très bien et classique, cela existe depuis toujours en Pascal et vous trouverez de nombreux articles qui expliquent en long et en large la manipulation des ensembles en Pascal.

Mais Delphi XE3 fait bien plus que cela ! Vous pouvez ajouter des methodes et propriétés aux ensembles. Voyons comment sur le cas concret que j'évoquais plus haut: transformer l'ensemble en un nombre entier et vice versa.

Disons-le d'emblée: il y a plusieurs manières de réaliser cette tâche de conversion. Je vais vous présenter celle qui est la plus respectueuse du langage et qui ne fait aucune supposition sur la manière dont les ensembles sont réprésentés en interne par le compilateur.

Ce que je veux pouvoir faire, c'est écire ce genre de code:

var
  Panier : TFruits;
  N      : Integer;
begin
  Panier := [frCerise, frPoire];
  // Conversion vers un entier
  N := Panier.Integer;
  // Conversion depuis un entier
  Panier.Integer := 5;
end;

Remarquez la notation: "Panier.Integer". C'est comme si la variable Panier était une classe et que cette classe avait une propriété "Integer". Mais Panier n'est pas une classe, c'est un ensemble ("Set of").

Voici comment il faut procéder:

type
  TFruitsHelper = record helper for TFruits
  strict private
    function  ToInteger : Integer;
    procedure FromInteger(Value : Integer);
  public
    property Integer : Integer read  ToInteger
                               write FromInteger;
  end;

implementation

function TFruitsHelper.ToInteger : Integer;
var
  F : TFruit;
begin
  Result := 0;
  for F := Low(TFruit) to High(TFruit) do begin
    if F in Self then
      Result := Result or (1 shl Ord(F));
  end;
end;


procedure TFruitsHelper.FromInteger(Value: Integer);
var
    F : TFruit;
begin
    Self := [];
    for F := Low(TFruit) to High(TFruit) do begin
        if (Value and (1 shl Ord(F))) <> 0 then
            Self := Self + [F];
    end;
end;


Et c'est tout...
Le code de conversion associe à chaque élément du type énuméré TFruit un bit dans l'entier. Pour cela, j'utilise
  1. Une boucle for F := Low(TFruit) to High(TFruit) do qui permet de parcourir la liste de l'énumération sans spécifier  aucun nom particulier;
  2. La fonction Ord() qui retourne l'ordre de l'élément dans la liste du type énuméré (frPomme vaut 0, frPoire vaut 1, et ainsi de suite);
  3. L'opérateur shl qui permet de décaler un nombre à gauche d'un certain nombre de positions. Le nombre que je décale est 1 et la position est l'ordre de l'élément. Il en résulte un bit à 1 à la position donnée par l'odre.
  4. L'opérateur in qui permet de savoir un des éléments de l'énumération est ou non présent dans l'ensemble.
  5. L'opérateur and qui me permet de masquer un bit dans le nombre entier pour savoir s'il est à zéro ou pas.
Et voilà...

Notes:
  1. Ce code ne fonctionne que si l'ensemble est suffisament petit pour tenir dans un nombre entier (32 bits). Il est facilement modifiable pour fonctionner avec un nombre de 64 bits. Mon objectif initial était de représenter des registres d'une carte d'entrées/sorties, donc aucun soucis car les registres sont généralement de 8, 16 ou 32 bits.
  2. En interne, le compilateur Delphi utilise déjà un bit pour chaque élément d'un ensemble. Sachant cela, il est possible de simplifier le code, mais à ce moment il devient dépendant de l'implémentation de Delphi, ce qui pourrait changer à l'avenir. Voici à quoi pourrait ressembler le code dans ce cas:
      function TFruitsHelper.ToInteger: Integer;
      begin

      {$IF SizeOf(TFruits) = SizeOf(Byte)}
          Result := PByte(@Self)^;
      {$ELSEIF SizeOf(TFruits) = SizeOf(Word)}

          Result := PWord(@Self)^;
      {$ELSEIF SizeOf(TFruits) = SizeOf(Integer)}
          Result := PInteger(@Self)^;
      {$ELSE}
             {$MESSAGE FATAL 'TFruits cannot be represented as an integer'}
      {$IFEND}

      end;

--
François Piette
Embarcadero MVP
http://www.overbyte.be