Freigeben über


Dieser Artikel wurde maschinell übersetzt.

Programmiererpraxis

Multiparadigmatisches .NET, Teil 9: Funktionale Programmierung

Ted Neward

Ted NewardJederzeit eine Artikel-Serie nahe zweistellig, kommt einer der zwei Dinge passiert: entweder der Autor ist anspruchsvoll genug zu glauben, dass seine Leser wirklich Interesse an diesem Thema, dass viele Male hintereinander sind, oder er einfach zu doof ist zu kommen mit einem neuen Thema denken. Oder ich nehme an, manchmal das Thema nur verdient so viel Abdeckung. Egal welche hier der Fall ist sicher sind wir jetzt in der Zielgeraden.

In das vorherige Stück in der Juni-Ausgabe (msdn.microsoft.com/magazine/hh205754), die Idee der Bereitstellung Variabilität entlang einer Achse namensbasierte kam unter die Lupe, verwenden Namenskonventionen und dynamischen Programmierung – d. h. Binden von Namen, die in.NET in der Regel bedeutet Reflection auf einer gewissen Ebene — einige interessante Design-Probleme zu lösen. Die meisten.NET-Entwickler, ich mir vorstellen, erwarten, dass die meisten der dynamischen Programmierung, auf die sie stoßen durch das Schlüsselwort "dynamisch", die c# 4 bietet. Wie alt-Hand-Visual Basic-Entwickler wissen c# kam nur durch seine Dynamik vor kurzem, während Visual Basic-Programmierer haben es bekannt — und benutzte es, sehr erfolgreich in vielen Fällen — seit Jahrzehnten.

Aber das ist nicht der letzte der Paradigmen — bleibt eine mehr geprüft werden, und wieder, es ist eine, die versteckt ist in plain Sight seit ein paar Jahren. Und während es sicherlich leicht (wenn ein bisschen frech), um funktionales Design als Design-Achse des algorithmischen Kommunalität-Variabilität zu beschreiben, das gleichzeitig zu simplifiziert und verdunkelt seine Fähigkeiten.

In einem einzigen Satz funktionaler Programmierung ist zu behandelnden Funktionen als Werte, wie bei jedem anderen Wert Datentyp, was bedeutet, dass wir Funktionen rund um übergeben können, wie wir können Datenwerte sowie neue Werte aus der diese Werte abgeleitet werden. Oder, genauer gesagt, sollte als erstklassige Bürger innerhalb der Sprache der Funktionen behandelt: sie können erstellt werden, an Methoden übergeben und von Methoden zurückgegeben, so wie andere Werte sind. Aber, dass die Erklärung, wieder, nicht genau aufzuklären, also lassen Sie uns beginnen mit einer einfachen Fallstudie.

Stellen Sie sich vor der Entwurfsphase ist die Schaffung einen kleinen Befehlszeilen-Rechner: ein Benutzer einen mathematischen Ausdruck in Typen (oder Rohre) und der Rechner analysiert sie und gibt das Ergebnis. Entwerfen dies ist ziemlich einfach, wie in dargestellt Abbildung 1.

Abbildung 1 einen einfachen Rechner

class Program
{
  static void Main(string[] args)
  {
    if (args.Length < 3)
        throw new Exception("Must have at least three command-line arguments");

    int lhs = Int32.Parse(args[0]);
    string op = args[1];
    int rhs = Int32.Parse(args[2]);
    switch (op)
    {
      case "+": Console.WriteLine("{0}", lhs + rhs); break;
      case "-": Console.WriteLine("{0}", lhs - rhs); break;
      case "*": Console.WriteLine("{0}", lhs * rhs); break;
      case "/": Console.WriteLine("{0}", lhs / rhs); break;
      default:
        throw new Exception(String.Format("Unrecognized operator: {0}", op));
    }
  }
}

Wie geschrieben, es funktioniert – bis der Rechner etwas anderes als die Kardinal vier Betreiber erhält. Was ist schlimmer aber ist, dass eine erhebliche Menge an Code (im Vergleich zu der Gesamtgröße des Programms) doppelte Code ist und wird weiterhin doppelte Code sein, wie wir neue mathematische Operationen zum System hinzuzufügen (z. B. der modulo-Operator, % oder der Exponent Operator ^).

Stepping zurück für einen Augenblick, es ist klar, dass des aktuellen Arbeitsganges — was wird getan, um die beiden Zahlen — ist was variiert hier, und es wäre schön, zu können, dies in einem allgemeineren Format umzuschreiben, siehe Abbildung 2.

Abbildung 2 einen allgemeineren Taschenrechner

class Program
  {
    static void Main(string[] args)
    {
      if (args.Length < 3)
          throw new Exception("Must have at least three command-line arguments");

      int lhs = Int32.Parse(args[0]);
      string op = args[1];
      int rhs = Int32.Parse(args[2]);
      Console.WriteLine("{0}", Operate(lhs, op, rhs));
    }
    static int Operate(int lhs, string op, int rhs)
    {
      // ...
}
  }

Natürlich, wir könnten einfach neu erstellen den Switch-Case-Block in Betrieb, aber, die nicht wirklich viel gewinnen. Im Idealfall möchten wir eine Art von Zeichenfolgenoperation-Lookup (, die auf der Oberfläche ist eine Form der dynamischen Programmierung wieder, binden den Namen "+", um eine Additiv-Operation, z. B.).

Innerhalb der Design-Muster-Welt wäre dies ein Fall für das Muster Strategie dem konkrete Unterklassen implementieren, eine Basisklasse oder Schnittstelle, die Bereitstellung der erforderlichen Signatur und Kompilierung Typechecking für Sicherheit, etwas nach dem Vorbild der:

interface ICalcOp
{
  int Execute(int lhs, int rhs);
}
class AddOp : ICalcOp { int Execute(int lhs, int rhs) { return lhs + rhs; } }

Die funktioniert … Art. Es ist ziemlich ausführlich, erfordern eine neue Klasse für jeden Vorgang erstellt werden, die wir wünschen. Es ist auch nicht sehr Objekt-orientierte, weil wir wirklich nur Notwendigkeit eine Instanz der es schon einmal, gehostet innerhalb von einer Suchtabelle für matching und Ausführung:

private static Dictionary<string, ICalcOp> Operations;
static int Operate(int lhs, string op, int rhs)
{
  ICalcOp oper = Operations[op];
  return oper.Execute(lhs, rhs);
}

Irgendwie fühlt es sich wie dies vereinfacht werden könnten; und wie einige Leser wahrscheinlich bereits erkannt haben, das ist ein Problem, das schon einmal vor, gelöst ist außer im Rahmen der Ereignishandler Rückrufe. Dies ist genau das, was der Delegat Konstrukt wurde für in c#:

delegate int CalcOp(int lhs, int rhs);
static Dictionary<string, CalcOp> Operations = 
  new Dictionary<string, CalcOp>();
static int Operate(int lhs, string op, int rhs)
{
  CalcOp oper = Operations[op];
  return oper(lhs, rhs);
}

Und natürlich Operationen mit den Vorgängen, dass der Rechner erkennt, aber neue hinzufügen, ein bisschen einfacher wird ordnungsgemäß initialisiert werden muss:

static Program()
{
  Operations["+"] = delegate(int lhs, int rhs) { return lhs + rhs; }
}

Versierte C# 3-Programmierer werden sofort erkennen, dass dies noch weiter verkürzt werden kann mit Lambda-Ausdrücke, die in dieser Version der Sprache eingeführt wurden. Visual Basic kann, in Visual Studio 2010, ähnlich vorgehen:

static Program()
{
  Operations["+"] = (int lhs, int rhs) => lhs + rhs;
}

Dies ist, wo die meisten c# und Visual Basic Entwickler Ideen über Delegaten und Lambda-Ausdrücke zu stoppen. Aber Lambda-Ausdrücke und Delegaten sind weit mehr interessant, vor allem, wenn wir beginnen, erweitern die Idee noch weiter. Und diese Idee der weiterleiten-Funktionen rund um und deren Verwendung in verschiedene Möglichkeiten geht tiefer.

Reduziert, Landkarten und faltet – Oh meine!

Übergeben von Funktionen rund um ist nicht etwas, was wir im Mainstream gewöhnt sind.NET-Entwicklung, so dass ein konkreteres Beispiel wie das Design profitieren kann erforderlich ist.

Übernehmen Sie für einen Moment, die eine Auflistung von Person-Objekten, wie in dargestellt sind Abbildung 3.

Abbildung 3 eine Sammlung von Person-Objekten

class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int Age { get; set; }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> people = new List<Person>()
    {
      new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
      new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
      new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
      new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
      new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
      new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
    };
  }
}

Nun, es passiert so, dass die Geschäftsführung möchte etwas zu feiern (vielleicht sie alle gemacht Quote). Was sie tun möchten, ist jede dieser Personen ein Bier, geben, die mit die traditionellen Foreach-Schleife ziemlich leicht erreicht wird, wie in dargestellt Abbildung 4.

Abbildung 4 der traditionellen Foreach Schleife

static void Main(string[] args)
{
  List<Person> people = new List<Person>()
  {
    new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
    new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
    new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
    new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
    new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
    new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
  };
  foreach (var p in people)
    Console.WriteLine("Have a beer, {0}!", p.FirstName);
}

Es gibt hier einige kleinere Bugs (vor allem in diesem Code ein Bier um meinen 11 Jahre alten Sohn Übergabe ist), aber das größte Problem mit diesem Code? Es ist an sich un-reusable. Versuche, später jeder ein anderes Bier geben erfordern ein anderes Foreach-Schleife, die das Don't wiederholen sich (trocken) verletzt. Konnte, natürlich, sammeln wir den Bier-Ausstellung-Code in eine Methode (klassischen verfahrenstechnischen Kommunalität Response), etwa so:

static void GiveBeer(List<Person> people)
{
  foreach (var p in people)
    if (p.Age >= 21)
        Console.WriteLine("Have a beer, {0}!", p.FirstName);
}

(Beachten Sie, dass ich über-21 Altersüberprüfung hinzugefügt; meine Frau, Charlotte, beharrte, dass ich es einschließen, bevor zur Veröffentlichung dieses Artikels gehen könnte.) Aber was, wenn der Wunsch ist, finden alle, die über 16 Jahren ist und Ihnen einen kostenlosen R-rated Film-Ticket, stattdessen? Oder, finden alle, die im Alter von 39 ist und Ihnen eine "Heilige Kuh, Sie sind alte!" Ballon? Oder finden alle über dem Alter von 65 und geben jedem von ihnen ein kleines Notizbuch zu Sachen notieren Sie, sie sind wahrscheinlich zu vergessen (wie ihr Name, Alter, Adresse …)? Oder finden alle mit dem Nachnamen als "Ford" und laden Sie sie zu einer Halloweenparty?

Je mehr dieser Beispiele, wir werfen, desto mehr wird es klar, dass jedem dieser Fälle präsentiert zwei Elemente der Variabilität: Filterung der Person-Objekte und die Aktion an, mit jedem dieser Person-Objekte. Angesichts der Macht der Delegaten (und die Aktion <T> und Prädikat <T> Arten in eingeführt.NET 2.0), können wir Kommunalität und noch die erforderliche Variabilität schaffen, siehe Abbildung 5.

Abbildung 5 Filterung Person-Objekte

static List<T> Filter<T>(List<T> src, Predicate<T> criteria)
{
  List<T> results = new List<T>();
  foreach (var it in src)
    if (criteria(it))
      results.Add(it);
  return results;
}
static void Execute<T>(List<T> src, Action<T> action)
{
  foreach (var it in src)
    action(it);
}
static void GiveBeer(List<Person> people)
{
  var drinkers = Filter(people, (Person p) => p.Age >= 21);
  Execute(drinkers, 
      (Person p) => Console.WriteLine("Have a beer, {0}!", p.FirstName));
}

Ein häufiger Vorgang ist "Transformieren" (oder, genauer darüber werden, "Projekt") ein Objekt in einen anderen Typ, z. B., wenn wir die letzten Namen aus der Liste der Person-Objekte in einer Liste von Zeichenfolgen zu extrahieren möchten (siehe Abbildung 6).

Abbildung 6 aus einer Liste von Objekten, die eine Liste von Zeichenfolgen konvertieren

public delegate T2 TransformProc<T1,T2>(T1 obj);
static List<T2> Transform<T1, T2>(List<T1> src, 
  TransformProc<T1, T2> transformer)
{
  List<T2> results = new List<T2>();
  foreach (var it in src)
    results.Add(transformer(it));
  return results;
}
static void Main(string[] args)
{
  List<Person> people = // ...
List<string> lastnames = Transform(people, (Person p) => p.LastName);
  Execute(lastnames, (s) => Console.WriteLine("Hey, we found a {0}!", s);
}

Beachten Sie, dass dank der Generika-Verwendung in den Erklärungen der Filter, Execute und Transform (mehr Gemeinsamkeit/Variabilität!), wir ausführen, um alle gefundenen Nachnamen anzuzeigen wiederverwenden können. Beachten Sie, zu, wie die Verwendung von Lambda-Ausdrücke stellen eine interessante Implikation beginnen, klar zu kommen – eine, die sogar noch deutlicher wird, wenn wir einen weiteren gemeinsamen funktionalen Betrieb schreiben reduzieren, die "eine Auflistung unten in einen einzelnen Wert reduziert" durch die Kombination aller Werte zusammen in einer angegebenen Weise. Beispielsweise könnten wir addieren Sie jedermanns Alter zum Abrufen eines Sum of All Ages-Werts mit einer Foreach-Schleife, etwa so:

int seed = 0;
foreach (var p in people)
  seed = seed + p.Age;
Console.WriteLine("Total sum of everybody's ages is {0}", seed);

Oder wir können es mit einer generischen verringern, schreiben, wie in dargestellt Abbildung 7.

Abbildung 7 mit eine generischen reduzieren

public delegate T2 Reducer<T1,T2>(T2 accumulator, T1 obj);
static T2 Reduce<T1,T2>(T2 seed, List<T1> src, Reducer<T1,T2> reducer)
{
  foreach (var it in src)
    seed = reducer(seed, it);
  return seed;
}
static void Main(string[] args)
{
  List<Person> people = // ...
Console.WriteLine("Total sum of everybody's ages is {0}", 
    Reduce(0, people, (int current, Person p) => current + p.Age));
}

Diese Reduzierung-Operation wird oft als "Falte," übrigens bezeichnet. (Für den anspruchsvollen funktionale-Programmierer, die beiden Begriffe sind etwas anders, aber der Unterschied ist nicht entscheidend für Diskussion.) Und ja, wenn Sie vermuten, dass diese Operationen waren wirklich nichts anderes als was LINQ bietet für Objekte (das LINQ to Objects-Feature, die so wenig Liebe zu haben, als es ursprünglich veröffentlicht wurde), Sie wäre spot-on begonnen hatte (siehe Abbildung 8).

Abbildung 8 Falten Operationen

static void Main(string[] args)
{
  List<Person> people = new List<Person>()
  {
    new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
    new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
    new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
    new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
    new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
    new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
  };
  // Filter and hand out beer:
  foreach (var p in people.Where((Person p) => p.Age >= 21))
    Console.WriteLine("Have a beer, {0}!", p.FirstName);

  // Print out each last name:
  foreach (var s in people.Select((Person p) => p.LastName))
    Console.WriteLine("Hey, we found a {0}!", s);

  // Get the sum of ages:
  Console.WriteLine("Total sum of everybody's ages is {0}", 
    people.Aggregate(0, (int current, Person p) => current + p.Age));
}

Für das Unternehmen arbeiten.NET-Entwickler, dies scheint dumm. Es ist nicht wie real Programmierer verbringen Zeit auf der Suche nach Möglichkeiten, Alter-Summierung Code wiederverwenden. Echte Programmierer schreiben Sie Code, die eine Auflistung von Objekten, durchläuft jeweils Vorname in eine XML-Darstellung in eine Zeichenfolge, die für den Einsatz in einer promoveaza-Anforderung oder etwas zu verketten:

Console.WriteLine("XML: {0}", people.Aggregate("<people>", 
  (string current, Person p) => 
    current + "<person>" + p.FirstName + "</person>") 
  + "</people>");

Hoppla. Vermuten Sie, dass LINQ-zu-Objekt-Zeug schließlich nützlich sein könnten.

Funktioneller?

Wenn Sie ein klassisch ausgebildeter objektorientierte-Entwickler sind, dies scheint lächerlich doch elegante zur gleichen Zeit. Kann es ein mind-blowing Moment, da dieser Ansatz an Softwaredesign in wahrsten Sinne des Wortes stammt ein fast diametral gegenüber Weg von Objekten: anstatt mit Schwerpunkt auf die "Dinge" im System, und so dass Verhalten etwas, der mit jeder dieser Dinge ist, sieht die funktionaler Programmierung identifizieren die "Verben" im System und sehen, wie sie auf verschiedene Arten von Daten arbeiten können. Weder Ansatz ist mehr als die andere Recht — jeder erfasst Kommunalität und bietet Variabilität in sehr unterschiedlichen Achsen und wie vorgestellt werden könnte, es gibt Orte, wo jeder elegant und einfach ist, und wo jeder hässlich und ungeschickt sein kann.

Denken Sie daran, im klassischen Objektorientierung Variabilität auf einer strukturellen Ebene, bietet die Möglichkeit, positive Variabilität erstellen durch Hinzufügen von Feldern und Methoden oder den Austausch der vorhandener Methoden (über Override), aber nichts über das Gefangennehmen der ad-hoc-algorithmische Verhalten kommt. In der Tat, es war nicht bis.NET bekam anonyme Methoden, dass dieser Achse der Kommunalität/Variabilität möglich wurde. War es möglich, etwas tun, c# 1.0, aber jeder Lambda musste eine benannte Methode irgendwo deklariert werden, und jede Methode mußte in System.Object-Bedingungen (die Downcasting innerhalb dieser Methoden gemeint), eingegeben werden, da c# 1.0 nicht parametrisierte Typen.

Langjährige Praktiker der funktionalen Sprachen werden erschaudern bei der Tatsache, dass ich den Artikel hier, am Ende, denn es gibt zahlreiche andere Dinge, die eine funktionale Sprache tun kann, neben nur Pass Funktionen rund um als Werte — partielle Anwendung der Funktionen sind ein riesiger Konzept, das großen funktionalen Programmierung machen unglaublich enge und elegant, in Sprachen, die es direkt unterstützen — aber redaktionelle Anforderungen müssen erfüllt sein, und ich bin meine Längenbeschränkungen schieben, wie es ist. Trotzdem sehen nur so viel von den funktionellen Ansatz (und bewaffnet mit den funktionellen Fähigkeiten bereits in LINQ) bieten einen leistungsstarken neuen Design-Einblick.

Noch wichtiger ist, sollten Entwickler sehen mehr dafür interessiert einen langen, harten Blick auf f#, nehmen, die von allen die.NET-Sprachen, ist der einzige, die diese funktionelle Konzepte (teilweise Anwendung und currying) als erstklassige Bürger innerhalb der Sprache erfasst. C# und Visual Basic-Entwickler können ähnliche Dinge tun, aber einige Bibliothek-Unterstützung (neue Typen und Methoden zu tun, was tut F#-natürlich) bedürfen. Glücklicherweise mehrere solche Bemühungen sind im Gange, einschließlich der "funktionalen c#" Bibliothek, verfügbar auf CodePlex (functionalcsharp.codeplex.com).

Verschiedene Paradigmen

Wie es oder nicht, multiparadigm Sprachen sind gemeinsam verwenden, und sie schauen, wie sie hier zu bleiben. Die Visual Studio 2010-Sprachen jede zeigen ein gewisses Maß an jeder der verschiedenen Paradigmen; C++, zum Beispiel hat einige parametrische Metaprogrammierung Ausstattung, die in verwaltetem Code, aufgrund der Art und Weise möglich, für die der C++-Compiler ausgeführt sind, und erst vor kurzem (in der aktuellen C ++ 0 X standard) gewonnen Lambda-Ausdrücke. Sogar der viel geschmähte ECMAScript/JavaScript/Programmiersprache JScript kann Objekte, Verfahren, Metaprogrammierung, dynamische und funktionalen Paradigmen tun; in der Tat ist viel von JQuery auf diese Ideen gebaut.

Glücklich Codierung!

Ted Neward ist ein Principal mit Neward & Associates, ein unabhängiges Unternehmen, spezialisiert auf Unternehmen.NET Framework und Java-Plattform-Systeme. Er hat mehr als 100 Artikel geschrieben, ist ein c#-MVP und INETA-Sprecher und hat verfasst oder Co-Autor ein Dutzend Bücher, einschließlich "Professionelle F#-2.0" (Wrox, 2010). Er berät und Mentoren regelmäßig — erreichen ihn an ted@tedneward.com Wenn Sie interessiert sind, ihn kommen zusammen mit Ihrem Team, oder lesen Sie seinen Blog unter blogs.tedneward.com.

Dank der folgenden technischen Experten für die Überprüfung dieses Artikels: Matthew Podwysocki