Programowanie z wykorzystaniem możliwości dynamicznych języków programowania  

Udostępnij na: Facebook

Autor: Bartosz Kierun

Opublikowano: 2011-06-10

W poprzednich artykułach zajmowaliśmy się wprowadzeniem do języka Python, a zwłaszcza jego implementacji o nazwie IronPython. Omówiliśmy jego składnię i podstawowe możliwości, również w kontekście integracji z platformą .NET Framework. Tym razem przyjrzymy się, na czym polega wsparcie dla dynamicznych języków programowania w samym języku C#.

Po przeczytaniu tego artykułu dowiesz się o:

  • architekturze i historii bibliotek DLR, czyli Dynamic Language Runtime,
  • nowych cechach języka C#,
  • najciekawszych możliwościach, jakie daje DLR.

Wstęp

W kolejnych wersjach platformy .NET można zaobserwować rosnące wsparcie dla coraz większej ilości paradygmatów stojących za wieloma różnymi językami programowania. Dzisiaj nazwanie języka C# po prostu statycznie typowanym byłoby sporym niedomówieniem. Za czasów platformy .NET Framework 2.0 pojawiło się w niej wsparcie dla typów ogólnych (generics), w .NET Framework 3.5 –LINQ oraz elementy programowania funkcyjnego. Największą nowością w .NET Framework 4.0, szczególnie w kontekście rozwoju samych języków programowania, jest bez wątpienia element o nazwie DLR, czyli Dynamic Language Runtime. Wprowadził on zasadniczą innowację, a mianowicie lepsze wsparcie dla dynamicznych języków programowania. Przypomnijmy, że większość flagowych języków programowania na platformie .NET, takich jak C# czy Visual Basic .NET, to języki statycznie typowane, czyli wymagające (mówiąc ogólnie) deklaracji typu zmiennej przed jej użyciem.

**Rys. 1. Rozwój możliwości języka C#.

Warto tutaj wspomnieć, że rozwój platformy DLR związany był z pracami nad implementacją pod platformę .NET typowego przedstawiciela rodziny dynamicznych języków programowania, czyli języka Python. W czasie prac nad jego implementacją okazało się, że większość mechanizmów jest na tyle ogólna, że warto je dołączyć do samego środowiska uruchomieniowego CLR czy bibliotek standardowych, aby mogły z nich skorzystać zarówno istniejące języki programowania, jak i przyszłe implementacje innych języków programowania chcących wykorzystywać ów paradygmat.

Do głównych składników DLR należą:

  • Dynamiczny system typów, współdzielony pomiędzy wszystkie implementacje języków wykorzystujących DLR. To bardzo duża zaleta, eliminująca konieczność stosowania warstw pośredniczących, konwersji typów i problemów z zarządzaniem pamięcią.
  • ExpressionTrees – dynamiczne drzewa wyrażeń reprezentujące semantykę danego języka.
  • Call Site Caching – mechanizm pamięci podręcznej przyspieszający operacje wykonywane na obiektach o dynamicznych typach danych, zazwyczaj poprzez zapamiętywanie cech tych obiektów (najczęściej ich typów danych).
  • Dynamic Object Interoperability – to zestaw klas i interfejsów reprezentujących dynamiczny obiekt i operacje na nim, które mogą być użyte przez programistów implementujących kolejne dynamiczne języki programowania.
  • API – pozwalające na osadzanie (hosting) dynamicznych języków we własnych aplikacjach.

**Rys. 2 Architektura Dynamic Language Runtime.

Istotną rolę pełnią tutaj również komponenty typu Binders, które enkapsulują semantykę języka dla danych operacji. Mechanizm ten wykorzystywany jest przez środowisko uruchomieniowe, dzięki czemu zyskuje ono wszelkie informacje, aby wykonać taką operację w czasie wykonania programu.

Wraz z powstaniem bibliotek DLR daje się również zauważyć coraz większe wsparcie firmy Microsoft dla inicjatyw i projektów dystrybuowanych na licencjach typu open source. Tak jest też z językiem IronPython i IronRuby i tak jest z samym projektem Dynamic Language Runtime. Choć jest on częścią platformy .NET Framework 4.0 i z pewnością będzie rozwijany przez zespół pracujący nad jej kolejnymi wersjami, to same biblioteki są również udostępniane na licencji Apache License 2.0 i dostępne w postaci źródeł na witrynie http://dlr.codeplex.com/. Jest to zdecydowany ukłon w stronę społeczności skupionych wokół wielu dynamicznych języków programowania.

**Rys. 3. Logo projektu DLR.

Krótki rys architektoniczny

Choć platforma .NET posiada dość dobre wsparcie dla dynamicznych języków programowania od czasów platformy .NET Framework 2.0 (zwłaszcza za sprawą tzw. fast delegates, czyli szybkich delegatów), to prace nad implementacją języka Python pod tę platformę, czyli IronPythona, przyczyniły się do stworzenia mechanizmów na tyle ogólnych, że stanowią one obecnie bardzo dobrą bazę pod implementację również innych dynamicznych języków programowania. Obecnie istnieją dwie dopracowane i stabilne implementacje takich języków, są to IronPython oraz IronRuby.

Obie wspomniane implementacje są od jakiegoś czasu rozwijane przez społeczności i dostępne na licencjach typu open source.

Projekt IronPython dostępny jest pod linkiem https://ironpython.codeplex.com/, zaś projekt IronRuby pod linkiem http://ironruby.codeplex.com/.

Głównymi elementami architektury DLR są:

  • Wspólny model wykonania, dający możliwość parsowania i wykonania dynamicznych języków we własnych aplikacjach, co może być przydatne, gdy chcemy zapewnić możliwość automatyzacji API naszych aplikacji i systemów np. za pomocą języka IronPython.
  • Abstrakcyjny drzewiasty model reprezentujący semantykę języka (Expression Trees), który jest rozszerzeniem tego modelu znanego z technologii LINQ.
  • Funkcjonalności pozwalające na bardzo szybką generacje kodu (DynamicSites, SiteBinders, Rules).
  • Współdzielony dynamiczny system typów (IDynamicMetaObjectProvider).
  • Dodatki takie jak krotki (Tuple), duże liczby całkowite (BigInteger) czy zespolone (Complex) adaptery (Binders) pozwalające na współdzielenie różnych typów danych pomiędzy statycznymi i dynamicznymi językami programowania. Warto tutaj zwrócić uwagę, że część całkiem ciekawych nowości w .NET Framework 4.0 (takich właśnie jak krotki czy liczby zespolone)  powstała właśnie w celu zapewnienia maksymalnej kompatybilności z popularnymi dynamicznymi językami programowania.

Dzięki wymienionym wyżej elementom implementacja każdego innego języka programowania, który posiada elementy np. dynamicznego systemu typów, jest dużo łatwiejsza dzięki możliwościom oparcia się na już istniejących i solidnych fundamentach, czyli DLR.

Wybrane nowości w języku C# 4.0

W tej sekcji zajmiemy się wybranymi nowościami w języku C# 4.0 w kontekście jego wsparcia dla dynamicznych języków programowania.

Opcjonalne i nazwane parametry metod

Dosyć częstym przypadkiem w czasie projektowania i implementacji klas oraz ich metod jest problem zapewnienia możliwości wywołania tej samej metody z różnymi parametrami wywołania. Dotychczas problem ten rozwiązywany był poprzez mechanizm zwany przeciążaniem metod, co pozwalało zadeklarować w jednej klasie metody o tej samej nazwie, ale z różnymi parametrami wywołania. Problemem w tym przypadku było nie tylko zwiększanie ilości kodu, ale też konieczność obejścia problemu powielania tego samego lub podobnego kodu w każdej takiej metodzie.

Spójrzmy na przykład, w jaki sposób realizowało się takie wymaganie dotychczas:

public void DoJob( string data )
{
DoJob ( data, false, null );
}

public void DoJob ( string data, bool ignoreEmptyLines )
{
DoJob ( data, ignoreEmptyLines, null );
}

public void DoJob ( string data, bool ignoreEmptyLines, ArrayList chars )
{
    // właściwa implementacja metody
}

Dzięki wykorzystaniu parametrów opcjonalnych i nazwanych, powyższa realizacja może wyglądać w sposób następujący (warto tu zwrócić uwagę, że w przypadku deklarowania parametru opcjonalnego musimy podać jego wartość domyślną):

// deklaracja funkcji z parametrami opcjonalnymi
public static void Foo(string a, int b = 0, bool c = false)
{
            Console.WriteLine("a:{0}, b:{1}, c:{2}", a, b, c);
}
        // i różne sposoby jej wywołania
Foo("a");
Foo("a", 1);
    Foo("c", 2, true);
    Foo("d", c: true);

Nie sposób odmówić parametrom nazwanym pewnego wkładu w zwiększenie czytelności kodu. Spójrzmy na przykład pewnej funkcji i sposób jej wywołania:

// deklaracja funkcji        
public static void SendMessage(string from, string to, string subject, string body) { }
// i przykład jej wywołania
SendMessage
(
from: "me@live.com",
to: "someone@live.com",
subject: "mail subject",
body: "mail body, mail body"
);

Parametry nazwane i opcjonalne są typową cechą większości dynamicznych języków programowania, stąd wsparcie dla tej funkcjonalności jest istotnym mechanizmem zapewniającym integrację statycznych języków programowania, takich jak C#, i dynamicznych, takich jak Python czy Ruby. Przede wszystkim zaś jest to mechanizm bardzo wygodny i wydatnie przyczynia się do zmniejszenia ilości niewiele wnoszącego kodu.

Słowo kluczowe dynamic

Aby zapewnić lepsze wsparcie dla dynamicznych języków programowania w języku C# 4.0, pojawił się statyczny typ danych (J) reprezentowany przez słowo kluczowe dynamic. Dzięki użyciu tego typu danych zmienia się zasadniczo sposób pracy z obiektami (przynajmniej dla programistów C#). Tym razem wszystkie odwołania do właściwości i metod są rozwiązywane podczas wykonania programu, a nie w procesie kompilacji. Choć taki sposób pracy z obiektami może być wygodny w kilku scenariuszach, to ma też parę wad, przede wszystkim tzw. późne wiązanie (late binding) bywa wolniejsze od klasycznego sposobu pracy z obiektami.W dużych aplikacjach również, „formalizm” narzucany przez statyczne języki programowania pozwala na wyłapywanie sporej ilości błędów i potencjalnych błędów już w fazie kompilacji, a nie dopiero podczas wykonania programu. Również funkcjonalności oferowane przez środowisko programistyczne Visual Studio, takie jak zaawansowany Intellisense czy dynamiczna kompilacja pisanego przez nas kodu, są dla większości programistów bardzo wygodne i znacząco przyspieszają tworzenie poprawnego kodu.

Spójrzmy zatem na kilka przykładów:

dynamic d = "test";
            Console.WriteLine(d.GetType());
            // Wypisze: "System.String".
    
            d = 100;
            Console.WriteLine(d.GetType());
            // Wypisze: "System.Int32".
            dynamic d2 = "test";

// poniższa linijka spowoduje pojawienie się wyjątku w czasie wykonania programu.
            d2++;

            // porównanie do typu ‘object’

            object obj = 10;
            Console.WriteLine(obj.GetType());

            obj = obj + 10;   // ta linijka spowoduje błąd w czasie kompilacji
            // wszystko będzie jednak w porządku gdy dokonamy rzutowania (a w tym konkretnym przypadku kosztowniejszego unbox’ingu)
            obj = (int)obj + 10;
            Console.WriteLine(obj);

Kolejny przykład pokazuje, jak wykorzystać typ dynamic jako alternatywę do techniki zwanej reflection (umożliwiającej analizę i modyfikację kodu w czasie wykonywania się programu):

// załóżmy, że wołana metoda zwraca obiekt nieznanego typu…
object calculator = GetCalculatorOfUnknownType();
Type calculatorType = calculator.GetType();
// aby wywołać na tym obiekcie jakąś metodę musielibyśmy użyć refleksji, co jest mało wygodne
object res = calculatorType.InvokeMember
    ("Add", BindingFlags.InvokeMethod, null, calculator, new object[] { 10, 20 });
int sum = Convert.ToInt32(res);

Ten sam przykład przy wykorzystaniu słowa kluczowego dynamic prezentowałby się już następująco:

dynamic calculator = GetCalculatorOfUnknownType();
int sum = calculator.Add(7, 21);

Ale implementacja dynamicznego typu danych w .NET Framework 4.0 może mieć, dzięki platformie DLR, znacznie szersze możliwości. Dzięki wspomnianym już klasom typu Binders można zapewnić każdemu niestatycznie typowanemu obiektowi współpracę z językiem C# (lub Visual Basic .NET).

Interoperacyjność z technologią COM

Omówione przed chwilą techniki programowania – takie jak opcjonalne i nazwane parametry czy istnienie typu dynamic, w połączeniu z możliwością pisania adapterów (Binders) do różnych innych języków (Python, Ruby, JavaScript) czy technologii programistycznych – daje programistom języka C# zdecydowanie łatwiejszy sposób współpracy z technologią COM (Component Object Model). Jest to o tyle ważne, że nadal olbrzymia część oprogramowania tworzona jest w tej właśnie technologii, o czym świetnie wiedzą programiści tworzący dodatki lub aplikacje z wykorzystaniem np. pakietu Microsoft Office.

Brak wsparcia dla parametrów opcjonalnych doprowadzał do kuriozalnych sytuacji, np. takiej jak na poniższym przykładzie:

var wordApp = new Word.Application();
object useDefaultValue = Type.Missing;
wordApp.Documents.Add(ref useDefaultValue, ref useDefaultValue, 
    ref useDefaultValue, ref useDefaultValue);

Albo takim:

object fileName = "Document.docx";
object missing  = System.Reflection.Missing.Value;
doc.SaveAs(ref fileName,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing);

W powyższym przykładzie widzimy metody posiadające wiele parametrów wywołania, z których wszystkie (lub większość) są opcjonalne. Zanim w języku C# nie pojawiły się różne omawiane przez nas tutaj możliwości, konieczne było podanie ich wszystkich, co – nie oszukujmy się – było mało wygodne.

W języku C# 4.0 wystarczy już następujący sposób wywołania metody:

var wordApp = new Word.Application();
wordApp.Documents.Add();
// i analogicznie:
wordApp.SaveAs();

Ale to nie tylko ta funkcjonalność robi takie wrażenie (zwłaszcza że zdarzały się w modelu obiektowym pakietu Office metody posiadające i po kilkanaście opcjonalnych parametrów wywołania!), ale fakt istnienia specjalnego adaptera (Binder) pozwalającego obejść się bez instalowanych razem z pakietem Office bibliotek typu PIA (PrimaryInteropAssemblies), zawierających definicję typów danych jego modelu obiektowego. Jest to mechanizm zdecydowanie ułatwiający wdrażanie aplikacji.

Duck typing

Duck typing to pojęcie związane z metodą określania typów obiektów. W przeciwieństwie do statycznie typowanych języków programowania, określenie typu obiektu odbywa się tutaj poprzez „określenie” istnienia danej właściwości lub metody już w czasie wykonania programu. Określenie to pochodzi od powiedzenia: „Jeżeli coś chodzi jak kaczka i kwacze jak kaczka, to musi to być kaczka”.

**Rys. 4.Kaczka ;)

Spójrzmy na przykład w języku C#:

public class Kaczka
    {
        public void Quack()
        {
            Console.WriteLine("Kwaaaa!");
        }

        public void Walk()
        {
            Console.WriteLine("Kaczka idzie...");
        }

        public void Swim()
        {
            Console.WriteLine("Kaczka plynie...");
        }
    }

    public class Person
    {
        public void Walk()
        {
            Console.WriteLine("Osoba idzie...");
        }

        public void Swim()
        {
            Console.WriteLine("Osoba plynie...");
}
    }

internal class Program
    {
        private static void Plays(dynamic duck)
{
            // dla obiektu typu Person będzie tutaj wyjątek w run-time!
duck.Quack(); 
duck.Swim();
duck.Walk();
}

        private static void Main()
        {
            Duck duck = new Duck();
            Person person = new Person();
            Plays(duck);
Plays(person);
        }
    }

Duck typing tym razem w języku Python (za wikipedią - http://en.wikipedia.org/wiki/Duck\_typing ):

class Duck:
    def quack(self):
        print("Quaaaaaack!")
    def feathers(self):
        print("The duck has white and gray feathers.")

class Person:
    def quack(self):
        print("The person imitates a duck.")
    def feathers(self):
        print("The person takes a feather from the ground and shows it.")
    def name(self):
        print("John Smith")

def in_the_forest(duck):
    duck.quack()
    duck.feathers()

def game():
    donald = Duck()
    john = Person()
    in_the_forest(donald)
    in_the_forest(john)

game()

Wybrane możliwości środowiska DLR

W tym podpunkcie zajmiemy się wybranymi możliwościami środowiska DLR, które mogą przydać się programistom języków C# lub Visual Basic .NET w nieco bardziej zaawansowanych zmaganiach z kodem.

 

Expando Object

Klasa ExpandoObject pozwala na tworzenie, dobrze znanych w dynamicznych językach programowania, obiektów typu property bag, pozwalających na elastyczne dodawanie kolejnych właściwości i metod tego obiektu w czasie wykonania programu. Dzięki implementacji interfejsu IDynamicMetaObjectProvider obiekty tego typu są w pełni interoperacyjne pomiędzy językami statycznie typowanymi, takimi jak C#, i dynamicznymi, takimi jak IronPython czy IronRuby. Innymi słowy, obiekt tego typu może być zainstancjonowany w języku C#, natomiast jego metody można wołać np. w języku IronPython czy IronRuby. Choć pozornie interoperacyjność taka nie jest niczym nowym w środowisku .NET, to pamiętajmy, że tutaj mamy do czynienia z obiektem, którego właściwości i/lub metody były dodawane w czasie wykonania programu, a nie zadeklarowane wcześniej.

Wewnętrznie obiekt klasy ExpandoObject wykorzystuje do przechowywania nazw i wartości atrybutów słownik IDictionary<string, object>. Co jest dosyć ciekawe, obiekt tej klasy można zrzutować na wspomniany przed chwilą interfejs, co pozwoli na enumeracje po wszystkich nazwach i wartościach właściwości. Może to być przydatne, jeżeli ilość lub nazwy właściwości nie są znane podczas wykonania programu.

Kolejną ciekawą cechą tejże klasy jest to, że implementuje ona interfejs INotifyPropertyChanging, co pozwala wykorzystać tego typu obiekty również w przypadku oprogramowywania warstw prezentacyjnych lub interfejsów użytkownika np. w technologii Windows Forms lub Windows Presentation Foundation. Warto też wspomnieć, że wiele platform do budowy aplikacji webowych, takich jak Django czy Ruby on Rails, również wykorzystuje tego typu obiekty np. przy komunikacji pomiędzy kontrolerem a widokiem we wzorcu Model-View-Controler (MVC).

Przyjrzyjmy się zatem prostemu przykładowi:

dynamic obj = new ExpandoObject();
// dynamiczne dodawanie właściwości
obj.Name = "Zenek";
obj.Age = 35;
obj.Address = "Dluga 37";

// dynamiczne dodawanie metod
obj.WriteToConsole = (Action)(() => Console.WriteLine("Hello, from dynamic."));
obj.PrintWithParam = (Action<string>)((string s) => Console.WriteLine(s));
Console.WriteLine(obj.Name);
obj.WriteToConsole();

Kolejny przykład to wykorzystanie dynamicznych obiektów do modelowania bardziej złożonych struktur – w tym przypadku struktury reprezentowanej w pliku XML:

XElement contactXML =
    new XElement("Contact",
        new XElement("Name", "Henryk Malinowski"),
      new XElement("Phone", "111-222-333"),
        new XElement("Address",
new XElement("Street1", "ul. Krakowska 123"),
new XElement("City", "Lublin"),
       new XElement("State", "n/a"),
            new XElement("Postal", "11-234")
        )
    );


dynamic contact = new ExpandoObject();
contact.Name = " Henryk Malinowski";
contact.Phone = "111-222-333";
contact.Address = new ExpandoObject();
contact.Address.Street = " ul. Krakowska 123";
contact.Address.City = " Lublin ";
contact.Address.State = "n/a";
contact.Address.Postal = "11-234";

Dynamic Object

Jeżeli chcemy wykorzystać możliwości platformy DLR w bardziej konkretnym celu, właściwsze wydaje się użycie klasy Dynamic Object. Wykorzystujemy ją najczęściej jako klasę bazową dla własnych klas (mimo że nie jest to klasa abstrakcyjna), kiedy chcemy nadać im kilka cech znanych z dynamicznych języków programowania.

Aby w najłatwiejszy sposób dodać ją do naszych obiektów, semantykę znaną z dynamicznych języków programowania należy w klasie je implementującą podziedziczyć po klasie DynamicObject. Klasa ta definiuje kontrakt na kompletny zestaw operacji na obiekcie, zapewniający jego interoperacyjność w obrębie środowiska CLR niezależnie od typu języka. W przeciwieństwie do interfejsu IDynamicMetaObjectProvider, podziedziczenie po tej klasie ma tę zaletę, że nie wymaga od programisty implementacji wszystkich metod.

Spójrzmy na listę metod zdefiniowanych we wspomnianej przed chwilą klasie:

**Rys. 5.Metody klasy DynamicObject.

Przykład użycia tej klasy łatwiej prześledzić na konkretnym przykładzie. Czy nie marzyło się wam kiedykolwiek pracować na obiekcie reprezentującym np. plik XML w łatwiejszy sposób, niż podając jako łańcuchy znaków nazwy kolejnych elementów lub atrybutów? Czy nie prościej byłoby po prostu wykorzystać mechanizm duck typing? Najlepsze jest to, że do zaimplementowania tego mechanizmu nie trzeba dużo pracy.

Spójrzmy na najprostszy przykład klasy, która będzie posiadała cechy znane bardziej z dynamicznych języków programowania. Widzimy, że do przechowywania wartości będzie służył tutaj wewnętrzny słownik typu Dictionary<string, object>:

classSimpleDynamic : DynamicObject
    {
        private Dictionary<string, object> bag = new Dictionary<string,object>();

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = bag[binder.Name];
            return true;
        }

        public override bool TrySetMember(System.Dynamic.SetMemberBinder binder, object value)
        {
            bag[binder.Name] = value;
            return true;
}
    }

// a poniżej sposób wywołania obiektów tej klasy
dynamic a = new SimpleDynamic();
    a.Name = "Zenek";
    a.Age = 112;
    Write(a);

private static void Write(dynamic a)
{
Console.WriteLine("{0} is {1} years old.", a.Name, a.Age);
}

W metodach o nazwie TryGetMember lub TrySetMember można by oczywiście zaimplementować dynamiczne operacje, np. na węzłach pliku XML lub JSON.

Przedstawiony poniżej sposób implemetacji umożliwi odwoływanie się do poszczególnych węzłów tak jak do właściwości obiektu (część implementacji została pominięta dla zwiększenia czytelności przykładu):

public class DynamicXMLNode : DynamicObject
{
    XElement node;
    public override bool TrySetMember(
        SetMemberBinder binder, object value)
    {
        XElement setNode = node.Element(binder.Name);
node = setNode;
return true;
    }
    public override bool TryGetMember(
        GetMemberBinder binder, out object result)
    {
        XElement getNode = node.Element(binder.Name);
        node = getNode;
        return true;
    }
}

Powyższa implementacja umożliwiałaby taki oto sposób pracy z plikiem XML:

dynamiccontact = newDynamicXMLNode("Kontakt");
contact.Name = "Zenek Malinowski";
contact.Age = "12";

Podsumowanie

W niniejszym artykule poznaliśmy możliwości bibliotek Dynamic Language Runtime z perspektywy programisty C#. Dowiedzieliśmy się, jakie są nowości w składni języka C# i jakie możliwości oferują biblioteki będące częścią DLR. Mimo wielu nowych możliwości musimy pamiętać, że język C# jest jednak językiem statycznie typowanym i ich nadużywanie nie ma większego uzasadnienia, choćby ze względu na mniejszą wydajność i brak silnej kontroli typów, co ma znaczenie szczególnie w większych programach i aplikacjach. Nie oznacza to jednak, że nie da się dla tych nowych możliwości znaleźć ciekawych i użytecznych zastosowań. Jednym z lepszych tego przykładów jest projekt Simple.Data stworzony przez Bobby Johnsona dostępny pod odnośnikiem: https://github.com/NotMyself/Simple.Data. Może tak wyglądałoby ADO.NET, gdyby powstawało 10 lat później?