Cet article a fait l'objet d'une traduction automatique.
Le programmeur polyglotte
Mélanges de langages
Ted Neward
Contenu
Programmation polyglot
Polyglot dans pratiques
Le coût derrière le choix
Temps avant le Web et client-serveur de programmation, il était courant pour une application entière à écrire dans une langue au-dessus d'une plate-forme unique ou un système unique.Considérez, par exemple, la plate-forme FoxPro omniprésents, une agrafe de programmation d'applications depuis des années.Il fourni une langue de format de l'interface utilisateur et bibliothèque, un accès aux données et format de stockage, ainsi qu'une collection de routines à charge la bibliothèque traditionnel, comme mathématiques et Comptabilité.Une langue et une plate-forme regroupées en un seul environnement FoxPro est un exemple classique de comment un programmeur peut apprendre un langage/plate-forme et de rester réussi et, plus important encore, gainfully utilisé.
Puis fournie spécialisation, nouvelles langues et des outils, chacun ayant un but particulier et spécifique.Les bases de données relationnelles est devenu la norme, et SQL est devenu le langage utilisé pour y accéder et de les modifier.Développement de client GUI initialement déplacée à la procédures langages comme C et Pascal, puis, avec l'avènement de l'orientation de l'objet fournie C++ et Delphi.Langages tels que Perl et Python est devenu puissants outils de l'administration d'un système d'exploitation de la console, systèmes plus UNIX, extension les capacités trouvées dans les environnements de commande tels que les environnements soirée et Korn.Puis fournie le Web et avec ce code HTML, CSS et JavaScript est devenu les langues de choix pour l'affichage d'interface utilisateur.
Le monde de langage de programmation a jamais vu un instant silencieux, mais dernières années, un grand nombre de langues est éclatée dans la scène programmation, y compris procédures langues comme Ruby, Windows PowerShell et le Python et fonctionnelles langages tels que F # et Scala.Langages existants sont l'obtention des nouvelles fonctionnalités, telles que les données interrogation installations Microsoft a ajouté à C# avec LINQ et maintenant une nouvelle phase de développement de langage de programmation a commencé à dans lequel nouvelles langues personnalisés appelés langues propres au domaine (DSL) sont développées pour les tâches spécifique ou les applications.Pour un exemple d'une ligne DSL, voir la colonne de poste de service à partir de l'édition de menu de lancement Visual Studio 2008 de MSDN Magazine (msdn.microsoft.com/magazine/cc164250).
Partout où que vous activez, nouvelles langues sont dépilant haut.Tandis qu'antérieures aux programmeurs multilingues certainement à, le terme « polyglot programmation » provient avec de Neal Ford décembre 2006 blog valider (memeagora.blogspot.com/2006/12/polyglot-programming.html), n'intitulé pas est « Polyglot programmation ».
Programmation polyglot
OK, donc nous avons établi qu'il y a beaucoup de langages quelque et il est probablement une langue particulièrement adaptée aux chaque problème, que vous devez adresse.Le développeur .NET performante doit comprendre de tout leur organisation.Qui sera l'objectif de cette colonne.
Envisagez un des problèmes plus courants dès aujourd'hui.Les développeurs sont en cours invités à adapter leurs programmes, les sites Web et services, en particulier, à un nombre supérieur et supérieur d'utilisateurs que jamais.Clients en cours bénéficient l'accès à leurs propres informations de compte directement.Cela signifie qu'où une application peut ont eue Échelle à quelques centaines utilisateurs (disons, afin d'augmenter le nombre d'appel Centre employé utilisateurs pendant une spurt croissance), désormais la même application doit adaptée à potentiellement des milliers, voire millions d'utilisateurs.
Développeurs travaillent sur le site Web de gestion d'obtenir la sécurité des threads et de bonnes performances.Verrouillage simplement chaque méthode afin de sérialiser l'accès via la totalité du système n'est pas une bonne solution car la mise à il pas l'échelle.Gérer correctement simultanéité d'accès aux données, surtout que l'application évolue, aucun feat simple ; il conserve les développeurs même senior des nuit au plus tard.Même avec la publication de nouveaux outils comme PEX (research.microsoft.com/projects/pex) et CHESS (research.microsoft.com/projects/chess) pour effectuer une analyse de code statique et exécuter des tests permutations d'unités pour découvrir les bogues multithreading, vous êtes toujours requis pour contrôler l'accès concurrentiel vous-même et à un niveau relativement faible, l'aide de relevés en c# « verrou » ou la simultanéité différents-contrôle classes de System.Threading.
Êtes-vous vers le défi ?La bonnes sont vos compétences de simultanéité d'accès aux données ?Rapide, quelle est la différence entre System.Threading.Monitor, System.Threading.Mutex et System.Threading.Semaphore ?
Ici, vous devez commencer à voir la valeur d'un choix de langue particulière.Par exemple, ce type de code complexe serait plus simple d'écrire et conserver si vous avez choisi un langage fonctionnel tel que F #, dont tendencies générales vers immuabilité et zéro effets côté empêchera le besoin de contrôle d'accès concurrentiel explicite, ou un langage propres au domaine, écrit en Ruby, qui a été conçue pour masquer les détails d'accès concurrentiel des développeurs l'utiliser.
Pour rendre cette idée plus concrete, imaginez qu'une application Web doit effectuer une opération généralement synchrone, comme effectuer des e / S basée sur les fichiers (ou une base de données ou service Web appel).Normalement, la chose à faire à partir de code C# plus simple sera d'ouvrir le fichier via un classique des instructions lire le contenu et les stocker à un tableau d'octets, puis fermez le fichier, de manière similaire à ceci :
byte[] pixels = null
BinaryReader br =
new BinaryReader(new FileStream(filename, FileMode.Open)); pixels = br.ReadBytes(4096);
Il peut être simple, mais il est également horribly sérialisé. Aucun traitement supplémentaire ne peut se produire dans ce thread pendant le fichier opération d'E / S. (Pour un fichier simple d'E / S opération, il est probablement pas une préoccupation majeure au moins pas jusqu'à ce que le nombre de ces récupère les opérations très volumineux, qui peut se produire lorsque le site essaie d'évoluer.) Il serait préférable de lire cette fichier à l'aide les opérations asynchrones disponibles via le pool de threads CLR, comme vous le voir dans la figure 1 .
Figure 1 lire le fichier asynchrone
delegate byte[] AsyncOpenMethod(string filename);
static byte[] AsyncOpen(string filename)
{
byte[] pixels = null;
using (BinaryReader br =
new BinaryReader(new FileStream(filename, FileMode.Open)))
{
Pixels = br.ReadBytes(4096);
}
}
static void AsyncOpenTheFile(string filename)
{
byte[] pixels = null;
AsyncOpenMethod aom = new AsyncOpenMethod(Class1.AsyncOpen);
IAsyncResult iar = aom.BeginInvoke(filename, null, null);
while (iar.IsCompleted == false)
{
// Do something?
}
pixels = aom.EndInvoke(iar);
}
Mais le code qui était simple avant est maintenant code qui uniquement un développeur mère peut adore, clairement concerne beaucoup plus que simplement lecture d'un fichier. Examinez maintenant écrire une routine similaire dans F # aspect :
async {
use inStream = File.OpenRead(filename)
let! pixels = inStream.AsyncRead(4096)
}
Je ne veux pas plonger trop profondément dans comment le code F #, fait mais le permettent. expression indique au compilateur F # pour générer l'expression comme une expression asynchrone et la méthode AsyncRead est un F # silencieusement tacks aux classes dérivé de System.IO.Stream standard à une fonctionnalité de langage F # appelée méthodes d'extension. Il lit le fichier en mode asynchrone efficacement et exporte les résultats dans le tableau d'octets sur le côté gauche de l'expression, toutes les sans aucun code supplémentaire nécessaire du développeur.
Nous allons cette placé dans un contexte réelle. Dans le cadre d'un cycle de maintenance nocturne, un service Web devez effectuer des copies d'un nombre de fichiers de stockage plus facilement en mode hors connexion pour effectuer des sauvegardes. Le code de F # pour effectuer cette copie de fichier, tous les complètement asynchrone, ressemble à la figure 2 .
Copies de sauvegarde figure 2 Création
#light
open System.IO
let CopyFileAsync filename =
async {
use inStream = File.OpenRead(filename)
let! pixels = inStream.AsyncRead(4096)
use outStream = File.OpenWrite(filename + ".back")
do! outStream.AsyncWrite(pixels)
}
let tasks = [ for i in 1 .. 10 -> CopyFileAsync("data" + i.ToString()) ]
let taskResults = Async.Run (Async.Parallel tasks)
Ici, à la fois la lecture et écriture sont effectuées en mode asynchrone et la collection entière de 10 tâches (une pour chaque fichier de données) est également être traitée en mode asynchrone. Vous envisagez que cela prendrait à faire dans Visual c#, qui serait plutôt écriture ? (Risque Coble va en nettement plus en détail sur F # et asynchrones programmation dans » Asynchrone facile : créer des applications simultanées à partir d'expressions F # simples« dans le 2008 octobre émettre de MSDN Magazine .
Polyglot dans pratiques
Dans la pratique, interopérabilité entre F c# et Visual c# (ou tout autre langage CLR est relativement simple, une fois que la « forme » au code (quelle langue la transforme en à L'IL niveau) dans les deux langues est bien comprise. Sur la surface cela semble assez facile, après tout, comment différents peut un appel de méthode être entre deux langages, étant donné que la spécification du langage commun (CLS) détermine les problèmes ou difficile, tels que sélection élective paramètre, de types de base et de classement des octets de nombreuses ?
Nous allons mettre que le test. Prendre certaines F simple # pour démarrer avec du code, nous allons compiler et transformer en une DLL et d'utiliser ILDasm (intermediate language désassembleur, ou Reflector, n'importe quel outil vous vous sentez plus à l'aise avec) pour examiner quoi il ressemble. Vous pouvez ensuite Dégradez aux expressions F # plus complexes, telles que le code de flux de travail asynchrone Coble chance présentées dans son article octobre 2008 qui je L'AI expliqué précédemment.
Pour commencer avec prendre un code simple F #
let x = 2
Si nous supposons qu'il vit dans la valeur par défaut « bibliothèque F # » projet fichier appelé Module1.fs, L'IL de La figure 3 (avec quelques éléments commentée hors pour conserver la liste IL brève) sera généré pour elle.
Figure 3 L'IL derrière permettre x = 2
.assembly Library1
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.
FSharpInterfaceDataVersionAttribute::.ctor(int32, int32,
int32) = ( 01 00 01 00 00 00 09 00 00 00 06 00 00 00 00 00 )
// ...
}
.class public abstract auto ansi sealed beforefieldinit Module1
extends [mscorlib]System.Object
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.
CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.
FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 )
.method public static int32 get_x() cil managed
{
// Code size 6 (0x6)
.maxstack 4
IL_0000: ldsfld int32 '<StartupCode$Library1>'.$Module1::x@3
IL_0005: ret
} // end of method Module1::get_x
.method private specialname rtspecialname static
void .cctor() cil managed
{
// Code size 13 (0xd)
.maxstack 3
IL_0000: ldc.i4.0
IL_0001: stsfld native int '<StartupCode$Library1>'.$Module1::_init
IL_0006: ldsfld native int '<StartupCode$Library1>'.$Module1::_init
IL_000b: pop
IL_000c: ret
} // end of method Module1::.cctor
.property int32 x()
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.
CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.
FSharp.Core.SourceConstructFlags) = ( 01 00 09 00 00 00 00 00 )
.get int32 Module1::get_x()
} // end of property Module1::x
} // end of class Module1
.class private abstract auto ansi sealed beforefieldinit
'<StartupCode$Library1>'.$Module1
extends [mscorlib]System.Object
{
.field static assembly native int _init
.custom instance void [mscorlib]System.Runtime.CompilerServices.
CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
.field static assembly initonly int32 x@3
.method private specialname rtspecialname static
void .cctor() cil managed
{
// Code size 8 (0x8)
.maxstack 3
IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: stsfld int32 '<StartupCode$Library1>'.$Module1::x@3
IL_0007: ret
} // end of method $Module1::.cctor
} // end of class '<StartupCode$Library1>'.$Module1
Si le code à L'IL suivre, vous remarquerez deux choses ; tout d'abord, que le nom « x » à partir du code F # est lié au niveau CLS comme propriété statique d'une classe appelée Module1 et la seconde, que sa valeur initiale de 2 n'est pas lié sous forme de constante, mais est initialisée lors de l'assembly est chargé, via le constructeur de type (.cctor) de la classe de StartupCode $Library1 généré le compilateur. En d'autres termes, tandis que vous pourriez être tenté à penser de x comme une valeur constante que le compilateur peut en ligne, le compilateur choisit de présenter sous forme d'une propriété statique.
Cela signifie que, puis que l'accès à cette liaison, x, nécessitera code C# quelque chose comme suit :
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("F#'s x = {0}", Module1.x);
}
}
}
Ainsi, beaucoup, donc bien. X est une liaison assez simple mais en tant que tel, vous devez attendez à une question simple d'y accéder. Quelque chose d'un peu plus complexe pourrait présentent certains problèmes trickier, donc Supposons faire quelque chose simplement une touche plus compliquée pour vérifier le mappage de F # à CLS toujours fait opportun.
Pour le code F #
let add a b = a + b
le compilateur génère L'IL supplémentaire dans la classe « Module1 » dans la DLL F # compilé :
.method public static int32 'add'(int32 a,
int32 b) cil managed
{
// Code size 5 (0x5)
.maxstack 4
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: add
IL_0004: ret
} // end of method Module1::'add'
C'est presque précisément la version C# de la fonction aspect, donc il est pas un lot qui doit être indiqué ici. L'appel il est également trivial.
Mais un des points forts de F # est qu'il traite des fonctions que les valeurs de première classe, et cela devient plus visible (et plus difficile) lorsque vous démarrez la création de fonctions qui prennent les fonctions que les arguments, telle que la suivante équivalent de la intégrée F # bibliothèque carte fonction, qui prend comme arguments d'une liste, ainsi que par une fonction à appliquer à chaque élément de la liste et renvoie une nouvelle liste contenant les résultats :
let mymap (l : 'a list) (f : 'a -> 'b) = List.map f l
Il s'agit un moyen particulièrement "fonctionnel" de traitement d'une liste : plutôt que l'itération à travers élément par élément, une langue fonctionnelle est une fonction en tant que paramètre et l'applique chaque élément dans la liste, génération soit un seul résultat (appelé une opération « pliage ») ou une nouvelle liste contenant les résultats de chaque (notre « mappage »).
Notez dans la figure 4 comment ceci prenne un peu complexe plutôt de IL après compilation. Là encore, le fait qu'il correspond à une méthode statique publique sur la classe Module1 n'est surprenant ; ce qui rendra ce difficiles à manipuler de C# pas, cependant, est que la méthode statique prend listes F # (c'est-à-dire, type-paramétrée instances de Microsoft.FSharp.Collections.List) que les types entrées et de retour, ainsi que par une fonction (signification, une instance d'une instance dually-type-paramétrées de Microsoft.FSharp.Core.FastFunc) comme la deuxième entrée.
Figure 4 L'IL pour la fonction de mappage
.method public static class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<!!B>
mymap<A,B>(class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<!!A> l, class [FSharp.Core]Microsoft.FSharp.Core.
FastFunc'2<!!A,!!B> f)
cil managed
{
// Code size 11 (0xb)
.maxstack 4
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldarg.0
IL_0003: tail.
IL_0005: call class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<!!1>
[FSharp.Core]Microsoft.FSharp.Collections.
ListModule::map<!!0,!!1>(class [FSharp.Core]Microsoft.
FSharp.Core.FastFunc'2<!!0,!!1>,
class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!!0>)
IL_000a: ret
} // end of method Module1::mymap
Cela va pour exiger des grave code C# pour fonctionner correctement. Pour que cet exemple, je veux du code C# pour prendre une collection d'entiers et les convertit dans leurs formulaires chaîne correspondante. (Le fait que peut faire ce assez facile de dans C# est sans pertinence ici, je dois savoir comment effectuer les opérations simples avant que je peut résoudre au plus complexe.) Pour appeler ce à partir de Visual c# signifie que plusieurs choses allez doivent se produire avec succès : la collection d'entrée est nécessaire d'être converties en un type de liste F #; la fonction appliquée à chaque élément doit être convertie dans un F # "FastFunc" instance ; et la liste F # renvoyée doivent être converties en type C# permet, sinon utilisé dans son formulaire F # natif directement.
Le code C# est aurez besoin de ces types F #, et donc la première étape sera pour ajouter la appropriée F # assembly référence, dans ce cas, FSharp.Core.dll. Construction d'une liste F #, n'est pas comme construction d'une liste en c#, plutôt que de passer dans la collection C# via un constructeur, F # suppose que listes sont créés les utilisant l'opérateur « cons, est une méthode statique sur la classe < > liste F #. En d'autres termes, le code F #
let l1 = [1; 2; 3;]
transforme en L'IL plutôt laid illustré La figure 5 .
La figure 5 laisser l1 = [1; 2; 3;]
IL_0000: ldc.i4.1
IL_0001: ldc.i4.2
IL_0002: ldc.i4.3
IL_0003: call class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<!0> class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<int32>::get_uniq_Empty()
IL_0008: newobj instance void class [FSharp.Core]Microsoft.FSharp.
Collections.List'1/_Cons<int32>::.ctor(!0,
class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!0>)
IL_000d: newobj instance void class [FSharp.Core]Microsoft.FSharp.
Collections.List'1/_Cons<int32>::.ctor(!0,
class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!0>)
IL_0012: newobj instance void class [FSharp.Core]Microsoft.FSharp.
Collections.List'1/_Cons<int32>::.ctor(!0,
class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!0>)
Cela signifie que pour convertir un tableau C# dans une liste F # adapté pour passer de à la méthode mymap, j'avoir plusieurs fois appeler la méthode Cons statique sur la liste, en transmettant le nouvel passez chaque fois ; Notez que parce que l'élément ajoutés équivaut à la tête de liste, conserver la liste F # dans le même ordre que le tableau en c#, J'AI devez commencer à la fin de la matrice et en arrière :
int[] scores = {1, 2, 3, 4, 5};
var fs_scores = Microsoft.FSharp.Collections.List<int>.get_uniq_Empty();
for (int i = scores.Length-1; i >= 0; i--)
{
fs_scores = Microsoft.FSharp.Collections.List<int>.Cons(scores[i],
fs_scores);
}
Déjà cela devient un élément d'une difficulté. Obtention d'une instance du type FastFunc de F # est encore plus fastidieux, car la FastFunc tapez, comme le noyau type System.Delegate, n'est pas réellement destiné à être instanciés par les programmeurs mais plutôt traitées par le compilateur F #. Généralement, lors de la création d'une instance de ce code F #, le compilateur génère une classe interne qui hérite de FastFunc, quelque chose que procéder en c#, mais qui nécessiterait beaucoup plus de travail.
Soudain appelle cette fonction F # semble trop de travail pour l'avantage dérivé de façon.
Le tout de cette représente pour illustrer est que le mappage d'une langue à la plate-forme sous-jacente est essentiel pour décider où et comment utiliser langues d'une manière polyglot. Quelque chose qui est trivial faire à l'intérieur de F #, pouvez par exemple, activer des difficile faire directement à partir de Visual c#. Cela ne signifie pas abandonner sur l'idée de polyglotism, cela signifie simplement que vous devez choisir avec soin les langues que vous utilisez ensemble et pour les besoins. Du point de vue plus positive, envisagez, parmi les exemples de de chance octobre article avec code supplémentaire au bas (voir figure 6 )
Figure 6 le code plus les modifications d'origine
open System
open System.IO
open Microsoft.FSharp.Control.CommonExtensions
#nowarn "057"
let aCountSpace filename size =
async {
let space = Convert.ToByte ' '
use stream = File.OpenRead (filename)
let bytes = Array.create size space
let! nbytes = stream.ReadAsync (bytes,0,size)
let count =
bytes
|> Array.fold_left (fun acc x -> if (x=space) then acc + 1 else acc) 0
return count
}
let aCounted (files : FileInfo array) =
files
|> Array.map (fun f -> aCountSpace (f.FullName) (int f.Length))
|> Async.Parallel
|> Async.Run
Notez que j'ai modifié légèrement. Toutefois, la seule modification apportées ici était de spécifier l'argument « fichiers » dans la méthode « aCounted » doit être un tableau de FileInfo quelque chose qui a été déduit par le compilateur F # dans le code d'origine de chance. La signature « aCounted » IL ressemble maintenant à
.method public static int32[] aCounted(class [mscorlib]System.IO.FileInfo[] files) cil managed
qui rend très trivial pour appeler de C#, comme dans :
int[] fileCounts =
Module1.aCounted(new DirectoryInfo(@"C:\Projects\Test").GetFiles("*.txt"));
foreach (var ct in fileCounts)
{
Console.WriteLine("Count = {0}", ct);
}
Encore cette nouvelle version, aussi simple que pour appeler, conserve la capacité de l'exécution complète asynchrone dont chance question en octobre.
Notez que l'utilisation de C# ici est purement arbitraire, basé entièrement sur le fait que je vous sentez plus à l'aise dans C# que dans Visual Basic ou C + c++ / CLI ; avoir dit, il ne devrait pas être difficile de voir le même code aspect dans une de ces langues.
Le coût derrière le choix
Il est toujours un coût adoptant une approche polyglot, bien sûr. La première calvaire principale est assez évident pour afficher, étant donné que nous avez déjà exécuté dans ce, les développeurs qui souhaitent effectuer l'utilisation des différentes langues sur un projet doit comprendre comment ces langues mapper sur la plate-forme sous-jacente et par la suite pour eux. Cela signifie que simplement compter sur le compilateur pour rendre les choses droite est plus fonctionnent.
Les développeurs devrez voir comment le compilateur mappe les constructions de langage à la plate-forme sous-jacente (dans ce cas, le CLR) et comment autres langues chercher ces structures. La bonne nouvelle dérivée du fait que les langues ne modifier beaucoup dans le temps, même pour les langues que nous ont vu certaines équipes assez radical décennie la dernière, tels que C# ou Visual Basic, donc une fois que vous avez appris des mappages de langue, cette information reste relativement constante.
Le deuxième problème dérive de la première, déboguer un programme polyglot peut être plus difficile à déboguer un monoglot une, le fait que maintenant le développeur owing simplement à devez parler deux ou plusieurs langues, plutôt que simplement sur le bouton. Dans le cas du code que j'ai présenté, par exemple, débogage nécessitera définissant un point d'arrêt dans le code C# et parcourant, éventuellement dans le code F # et le dos à nouveau. Bien des égards, ce n'est pas nouveau — F # est qu'une langue plus sur à la liste des langues que vous avez besoin de connaître.
Dans certains magasins de développement, les développeurs travaillent sur le C# du code simplement confiance (soit par déclaration soit via foi de tests d'unité) le code F # fonctionne comme publié, et étape sur rather than étape dans le code F#, simplement comme beaucoup de développeurs Visual Basic et C# faire lorsque parcourant un code qui appelle dans non gérés bibliothèques C ou C++.
Avenir colonnes, j'examinerai autres méthodes qu'approches polyglot créer de programmation plus simple et au autres langues et leurs avantages au programmeur polyglot.
Envoyez vos questions et commentaires à Ted à polyglot@Microsoft.com.
Ted Neward est consultant principal avec ThoughtWorks, un conseil international spécialisé dans les systèmes d'entreprise fiable et souple. Il a écrit de nombreux livres, est un architecte MVP Microsoft, INETA et formateur PluralSight. Atteindre Ted à Ted@tedneward.com, ou lisez son blog à blogs.tedneward.com.