Blog Arolla

Inspiration fonctionnelle – Irrelevantable, partie 1

Tentons d’exprimer quelque chose de plus que Nullable<T>

Ce billet est le deuxième d’une série sur la façon dont la programmation fonctionnelle, et la formation F# à laquelle j’ai récemment assisté, m’apportent de l’inspiration dans mon travail quotidien en C#. Ce post présentera une manière de construire un type générique Irrelevantable<T> en C#, et comment exprimer la même chose en F#.

J’ai récemment eu à faire face à une situation dans laquelle une valeur, disons un prix par exemple, pouvait avoir 3 états différents :

  1. Il pouvait tout d’abord s’agir d’un prix connu et valide,
  2. Il pouvait être absent,
  3. Il pouvait être sans objet : pour une situation donnée, le prix ne pouvait ni avoir une valeur, ni ne pas en avoir, ce prix n’avait tout simplement aucun sens.

Pour gérer les cas de la présence ou de l’absence de valeur, si on est dans le cas d’un type par référence, alors la valeur null est suffisante, et si on est dans le cas d’un type par valeur on peut utiliser le type Nullable<T>. Mais comment gérer le statut “adapté / sans objet” ? Comme j’avais à gérer ce cas pour un certain nombre de caractéristiques dans mon projet, j’ai voulu construire un objet structure et réutilisable. Comme j’étais confronté à plusieurs types de données différents, je voulais également que mon nouveau type soit générique. La réponse que j’ai trouvé a été de construire ma propre structure an Irrelevantable<T>, au comportement assez proche de la structure Nullable<T>.

public struct Irrelevantable<T>

Comme pour Nullable<T>, j’ai fais le choix d’utiliser une structure (et non une classe), de manière à ce que les instances de celle-ci soient copiées par valeur, et non par référence. J’ai ensuite défini les membres, propriétés et le constructeur :

private bool isRelevant; public bool IsRelevant { get { return isRelevant; } } private T value; public T Value { get { if (!this.IsRelevant) throw new InvalidOperationException( "This Irrelevantable<T> instance" + "is irrelevent."); return this.value; } } public Irrelevantable(bool isRelevant, T value) { this.isRelevant = isRelevant; this.value = value; }

L’étape suivante a été de surcharger les méthodes de bases pour la implémenter la comparaison :

public override int GetHashCode() { return this.IsRelevant ? this.value.GetHashCode() : -1; } public override bool Equals(object other) { if (!this.IsRelevant) { if (other is Irrelevantable<T>) { return !((Irrelevantable<T>)other).IsRelevant; } return false; } else { if (other is Irrelevantable<T>) { return this.value.Equals( ((Irrelevantable<T>)other).value); } return this.value.Equals(other); } }

De cette manière, deux objets “sans objet” sons égaux s’ils ont le même type sous-jacent, et deux objets “pertinents” ont la même valeur s’ils contiennent deux valeurs égales.

Ensuite, j’ai défini les opérateurs de conversion implicite et explicite, afin de faciliter l’utilisation du type :

public static implicit operator Irrelevantable<T>(T value) { return new Irrelevantable<T>(true, value); } public static explicit operator T(Irrelevantable<T> value) { return value.Value; }

Enfin, j’ai ajouté une propriété statique pour représenter la valeur “sans objet” :

public static Irrelevantable<T> Irrelevant { get { return new Irrelevantable<T>(false, default(T)); } }

Le type défini précédemment permet d’écrire désormais des expressions telles que celles-ci :

Irrelevantable<string> someString = "Some data"; Irrelevantable<string> irrelevantString = Irrelevantable<string>.Irrelevant; Irrelevantable<int?> noInt = null; Irrelevantable<int?> someInt = 10; Irrelevantable<int?> irrelevantInt = Irrelevantable<int?>.Irrelevant;

Ayant fait le choix de rendre implicite la conversion des types standards vers Irrelevantable, et la conversion inverse explicite (comme c’est également le cas pour le type Nullable), on obtient un comportement identique en convertissant vers et depuis les types Nullable, Irrelevantable and object :

object obj = 1; int? nullable = 2; Irrelevantable<int> irrelevantable = 3; int fromObj = (int)obj; int fromNullable = (int)nullable; int fromIrrelevantable = (int)irrelevantable;

Pour être honnête, le code C# sur lequel ce billet est basé a été écrit bien avant que je puisse écrire la même chose en F#. Mais que je suis retombé sur ce code après ma formation, j’ai immédiatement pensé : “Ce serait tellement plus simple en F# !”.

L’implémentation F# sera traitée dans le billet suivant, mais elle va être assez courte. J’essaierai donc de développer un rapide exemple d’utilisation.

@pirrmann

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *