Carl de Billy
Tout ce que vous voulez de Carl, ou presque.
Carl de Billy
Navigation
  • Carl, le gars…
  • Parcours professionnel
You are here: Home › Développement › Extracteur de mots en C# à l’aide de Reactive Extensions (Rx)

Extracteur de mots en C# à l’aide de Reactive Extensions (Rx)

2012-10-04 | Filed under: Développement, Reactive Extension and tagged with: C#, IObservable, Observable, Reactive Extension, Rx

Le contexte

Voyez-vous, je suis responsable de la formation avancée donnée aux nouveaux développeurs qui arrivent chez nventive.  Là, je dis nouveaux, mais sachez qu’ils ne sont normalement pas nouveaux en programmation.  Il m’arrive même souvent d’affirmer que nventive possède un niveau technique anormalement élevé pour une simple entreprise de développement informatique.

Pendant cette dure journée pour les nouveaux, il est d’usage quand ils sortent de la salle que le reste de l’équipe leur demande s’ils veulent toujours travailler pour nous.  À date, personne n’a encore démissionné après cette épreuve initiale.

Cette formation consiste principalement à les familiariser avec la technologie Reactive Extensions de Microsoft.  Elle n’est pas simple à aborder, mais je prétends avoir trouvé une manière plutôt efficace pour en expliquer les subtilités.

La requête

Suite à cette formation qui a eu lieu hier, précisément, un des nouveaux (Étienne AKA « Razor ») est venu me voir pour me demander comment je m’y prendrais pour faire un extracteur de mots à partir d’une source observable de caractères.

J’ai trouvée l’idée bonne et je me suis même dit que ça constituait un excellent prétexte pour faire une entrée sur ce blog qui moisit tranquillement sur mes serveurs…

Notre au lecteur : si vous n’êtes pas programmeur ou si vous ne connaissez pas Reactive Extensions, il est possible que le reste de cet article vous semble un tas de mots qui ne fait aucun sens.  Je m’en excuse d’avance.

La stratégie

Tout d’abord, la source de données est nécessairement exprimable sous la forme suivante :

IObservable<char> chars;

Le résultat à obtenir devra normalement être du type suivant :

IObservable<string> words;

Les critères d’acceptation que je me donne sont les suivant :

  • Les seules données qui peuvent être mises en state sont les caractères d’un mot non-complété.  Ces données sont spécifiques à une subscription en particulier.
  • Aucun processus d’accumulation de caractères ne doit avoir lieu s’il n’y a pas de subscription active.
  • Aucun scheduler : tout le processus doit avoir lieu sur le thread où les caractères arrivent.  L’utilisateur pourrait toujours faire un .ObserverOn() s’il désire changer le scheduler, mais aucune décision de ce genre n’est prise dans l’implémentation.
  • Il doit être possible pour l’appelant de spécifier le discriminant entre un caractère qui fait parti d’un mot et d’un autre qui ne l’est pas.  Par défaut, on va utiliser le char.IsLetterOrDigit() pour effectuer cette séparation.
  • Le résultat doit être de qualité professionnelle.  Plus qu’un code d’exemple, il doit avoir tout ce qu’il faut pour être utilisable en production.

C’est la subscription à ce dernier qui va lancer tout le processus d’écoute et d’accumulation de caractères dans le but de former des mots.

Les Method Extensions

La solution sera la mise en place de la method extension GetWords(this IObservable<char> source) qui se met sur un observable de caractères et permet d’en sortir des observables de chaînes de caractères (string).  Voici les déclarations :

	public static IObservable<string> GetWords(this IObservable<char> source)
	{
		// Use a default discriminator for non-word characters
		return source.GetWords(c => !char.IsLetterOrDigit(c));
	}

	public static IObservable<string> GetWords(
		this IObservable<char> source,
		Func<char, bool> wordSeparatorSelector)
	{
		return Observable.Create<string>(
			observer => new WordExtractorSubscription(
				observer, source, wordSeparatorSelector));
	}

Noter ici l’utilisation de Observable.Create().  Cette méthode, fournie par le framework Reactive Extensions, permet de créer un observable qui va gérer automatiquement les abonnements (subscriptions) tout en déléguant toute la mécanique de gestion de l’observer à une méthode lambda fournie (mise en gras dans le code qui précède).

L’extracteur de mots

Vous l’avez probablement deviné, tout se passe dans la classe WordExtractorSubscription.  Cette classe a la responsabilité de gérer l’état d’un abonnement réactif (Subscription) et de traiter les caractères les uns après les autres tout en accumulant les caractères jusqu’à ce qu’un mot complet soit formé.  Le fonctionnement en détails est le suivant :

  1. L’accumulateur (ici un List<char>) est initialisé avec une capacité de départ à 12 caractères. Ça devrait suffir pour la majorité des cas et il pourra grossir en cas de besoins.
  2. Dès la construction, on crée un abonnement _innerSubscription vers la source de caractères, dans le but d’activer la source de caractères.  La classe WordExtractorSubscription agit à titre d’observer dans ce cas-ci.  Ce qui lui permet de recevoir les OnNext, OnError et OnCompleted et de les traiter en conséquence.
  3. Pour chaque caractère qui arrive (via le OnNext), on valide s’il s’agit d’un caractère qui est considéré comme faisant parti d’un mot ou pas.
    A) S’il appartient à un mot, on l’accumule.  Aucun autre traitement n’est nécessaire.
    B) S’il n’appartient pas à un mot, ça veut dire que ce qui est accumulé à date (s’il y en a) est considéré comme un mot et doit être envoyé à l’observer de destination.
  4. Si on reçoit un OnError, on le repasse simplement à notre observer et, suivant la grammaire Reactive Extension, notre abonnement est mort : on peut donc tout cleaner sans autre forme de procès.
  5. Si on reçoit un OnCompleted, on peut le repasser à notre observer, mais auparavant, il faut d’abord valider s’il y a des caractères accumulés et les notifier comme étant le dernier mot.  Toujours selon la grammaire Reactive Extension, l’abonnement est échu et on peut tout nettoyer.

Voici le code de la classe WordExtractorSubscription :

private class WordExtractorSubscription : IDisposable, IObserver<char>
{
	private readonly IObserver<string> _observer;
	private readonly Func<char, bool> _wordSeparatorSelector;
	private readonly IDisposable _innerSubscription;

	private readonly List<char> _accumulatedChars = new List<char>(10);
	private volatile bool _isDisposed;

	public WordExtractorSubscription(
		IObserver<string> observer,
		IObservable<char> source,
		Func<char, bool> wordSeparatorSelector)
	{
		_observer = observer;
		_wordSeparatorSelector = wordSeparatorSelector;
		_innerSubscription = source.Subscribe(this);
	}

	public void Dispose()
	{
		if(_isDisposed)
		{
			return;
		}

		_innerSubscription.Dispose();
		_isDisposed = true;
	}

	public void OnNext(char value)
	{
		if (!_wordSeparatorSelector(value))
		{
			// this character is good, accumulate it
			lock(_accumulatedChars)
			{
				_accumulatedChars.Add(value);
			}

			return; // prevent completition of accumulate word
		}

		// Here, we got a non-word character,
		// so need to send accumulated chars as a word
		CompleteWord();
	}

	private void CompleteWord()
	{
		string word;
		lock (_accumulatedChars)
		{
			// Any accumulated chars ?
			if (!_accumulatedChars.Any())
			{
				return; // nope
			}

			// Create the word string
			word = new string(_accumulatedChars.ToArray());

			// Clear accumulation list
			_accumulatedChars.Clear();
		}

		_observer.OnNext(word);
	}

	public void OnError(Exception error)
	{
		// This subscription is dead now
		Dispose();

		// Send the error to observer
		_observer.OnError(error);
	}

	public void OnCompleted()
	{
		// Send any completed word before finishing
		CompleteWord();

		// Terminates everything
		Dispose();

		// Send completed to observer
		_observer.OnCompleted();
	}
}

Le test

Afin de valider que tout fonctionne, il importe de tout faire sur un test unitaire.  Le test suivant devrait faire le travail rapidement.  Il est incorporé à la solution complète de la section suivante.

La solution complète

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Reactive.Linq;

namespace RxWordMachine
{
	[TestClass]
	public class CharExtensionsFixture
	{
		[TestMethod]
		public void TestGetWords()
		{
			// This is the input
			const string sourceText = @"
			This-is-THE*string we
			---------------
			want to GET
			====>as!!a!/result!!";

			// Convert the input as observable of chars
			IObservable<char> chars = sourceText.ToObservable();

			// Get an observable of words
			IObservable<string> words = chars.GetWords();

			// Execute everything and get the result as an array of strings
			var result = words.ToEnumerable().ToArray();

			// Create a result string who join all words
			var resultString = string.Join(" ", result);

			Assert.AreEqual(
				"This is THE string we want to GET as a result",
				resultString); // it works!
		}
	}

	public static class CharExtensions
	{
		public static IObservable<string> GetWords(this IObservable<char> source)
		{
			// Use a default discriminator for non-word characters
			return source.GetWords(c => !char.IsLetterOrDigit(c));
		}

		public static IObservable<string> GetWords(
			this IObservable<char> source,
			Func<char, bool> wordSeparatorSelector)
		{
			return Observable.Create<string>(
				observer => new WordExtractorSubscription(
					            observer, source, wordSeparatorSelector));
		}

		private class WordExtractorSubscription : IDisposable, IObserver<char>
		{
			private readonly IObserver<string> _observer;
			private readonly Func<char, bool> _wordSeparatorSelector;
			private readonly IDisposable _innerSubscription;

			private readonly List<char> _accumulatedChars = new List<char>(10);
			private volatile bool _isDisposed;

			public WordExtractorSubscription(
				IObserver<string> observer,
				IObservable<char> source,
				Func<char, bool> wordSeparatorSelector)
			{
				_observer = observer;
				_wordSeparatorSelector = wordSeparatorSelector;
				_innerSubscription = source.Subscribe(this);
			}

			public void Dispose()
			{
				if (_isDisposed)
				{
					return;
				}

				_innerSubscription.Dispose();
				_isDisposed = true;
			}

			public void OnNext(char value)
			{
				if (!_wordSeparatorSelector(value))
				{
					// this character is good, accumulate it
					lock (_accumulatedChars)
					{
						_accumulatedChars.Add(value);
					}

					return; // prevent completition of accumulate word
				}

				// Here, we got a non-word character,
				// so need to send accumulated chars as a word
				CompleteWord();
			}

			private void CompleteWord()
			{
				string word;
				lock (_accumulatedChars)
				{
					// Any accumulated chars ?
					if (!_accumulatedChars.Any())
					{
						return; // nope
					}

					// Create the word string
					word = new string(_accumulatedChars.ToArray());

					// Clear accumulation list
					_accumulatedChars.Clear();
				}

				_observer.OnNext(word);
			}

			public void OnError(Exception error)
			{
				// This subscription is dead now
				Dispose();

				// Send the error to observer
				_observer.OnError(error);
			}

			public void OnCompleted()
			{
				// Send any completed word before finishing
				CompleteWord();

				// Terminates everything
				Dispose();

				// Send completed to observer
				_observer.OnCompleted();
			}
		}
	}
}

Optimisations possibles

Le code est assez optimal.  Cependant, il y a un petit risque d’utilisation intensive de la mémoire s’il advenait à accumuler un mot particulièrement long : la taille de l’accumulateur pourrait devenir importante et la mémoire prise ne sera pas libérée tant que l’abonnement ne sera pas terminé.

Conclusion

Il est relativement facile, lorsque l’on connait bien Réactive Extension, d’en faire joujou et de se monter une boite à outils d’utilitaires de la sorte.  Ils sont hautement réutilisable et particulièrement performants.

Did you like this article? Share it with your friends!

Tweet

Written by cdebilly

2 Responses to "Extracteur de mots en C# à l’aide de Reactive Extensions (Rx)"

  1. David (Dr.Rx) dit :
    2012-10-06 à 00:52

    Pas vraiment plus beau, juste pour le loisir de ne le faire que avec des opérateurs Rx 😀

    public static IObservable GetWords(
    	this IObservable source,
    	Func wordSeparatorSelector)
    {
    	return Observable
    		.Create(observer => 
    			{
    				var src = source.Select(c => wordSeparatorSelector(c) ? default(char?) : c).Publish();
    				src
    					.Window(src.Where(c => c == null))
    					.SelectMany(wordChars => wordChars.Where(c => c != null).Select(c => (char)c).ToArray())
    					.Where(wordChars => wordChars.Length > 0)
    					.Select(wordChars => new string(wordChars))
    					.Subscribe(observer);
    
    				return src.Connect();
    			});
    }
    

    (Et je te laisse le plaisir d’expliquer à quoi servent Publish et Connect)

    Répondre
    1. cdebilly dit :
      2012-10-06 à 07:46

      En effet. Par contre, l’idée ici était de démontrer comment faire un opérateur Rx.

      Mais je me doutais bien qu’il y avait moyen de le faire de cette manière. Par contre, avoue que ton code est moins sexy que le mien! 😉

      Répondre

Laisser un commentaire Annuler la réponse

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Catégories

  • .NET
  • Développement
  • Internet
  • Javascript
  • NFC
  • Non catégorisé
  • Open-Source
  • Reactive Extension
  • Silverlight
  • Téléphonie
  • Windows 10
  • Windows 8
  • Windows Phone 8
  • Windows Server 2012
  • WPF

Archives

  • février 2018
  • janvier 2015
  • juin 2014
  • novembre 2013
  • octobre 2013
  • avril 2013
  • février 2013
  • novembre 2012
  • octobre 2012
  • avril 2012
  • septembre 2011
  • juillet 2011
  • février 2011
  • janvier 2011
  • novembre 2010
  • septembre 2010
  • août 2010

Bookmark or Pin us!

Carl de Billy supports many popular operating systems!

Supported platforms include:

  • FavIcons for desktop and mobile browsers
  • Windows 8 and Windows Phone 8.1 Live Tiles
  • iOS Home Screen Icons
  • iOS WebApp

Tags

.Net Framework 3g ADSL autocomplete C# Communauto contrat numérique css foursquare hadopi HoloLens internet IObservable jquery Microsoft Montréal ndef nfc nventive Observable Pixel shader RAID Reactive Extension Roslyn Rx rxjs semantic silverlight smartposter Storage Spaces styling tags Teksavvy telephonie Uno videotron Visual Studio web Windows 8 Windows 10 Windows Phone WP7

Navigation

  • Inscription
  • Connexion
  • Flux des publications
  • Flux des commentaires
  • Site de WordPress-FR

Mastodon

© 2025 Carl de Billy

Powered by Esplanade Theme and WordPress