\


 Sunday, 05 October 2008
Adaptéry pro funktory v C++ => Adaptéry pro funkce v C#

V C++ je snadné napsat takzvané adaptéry pro funkce, respektive pro funktory - objekty, chovající se jako funkce. K čemu jsou adaptéry dobré? Představme si, že máme napsanou funkci equal_to, která přijímá dva argumenty a vrátí true, jestliže jsou oba argumenty shodné, jinak vrátí false. Jedná se tedy o binární funktor, protože přijímá dva argumenty. Nyní potřebujeme pomocí stl metody find_if vyhledat v naší kolekci všechny prvky, jejichž hodnota je rovna 10. Podmínku v metodě find_if musí představovat unární funktor (funktor přijímající jeden argument - prvek v kolekci - a vracející true jen v případě, že prvek v kolekci podmínku splňuje). Je zřejmé, že binární funktor nemůžeme použít na místě, kde je očekáván unární funktor. V C++ můžeme ale v této situaci namísto psaní dalšího unárního jednoúčelového funktoru využít speciálního adaptéru, jehož účelem je konverze binárního funktoru na unární. Adaptér, který přijde vhod pro naše účely, se jmenuje binder1st (zde by bylo možné použít i adaptér binder2nd). Adaptér binder1st očekává, že mu předáte binární funktor, který má být převeden na unární  a hodnotu, která má být vždy použita jako první argument (proto ...1st) při volání binárního funktoru. Adaptér binder2nd se od adaptéru binder1st liší jen tím, že předaná hodnota bude použita vždy jako druhý argument předaného binárního funktoru. Jinými slovy - při volání funktoru binder1st je kolekce spokojena, že dostala unární funktor, ale náš funktor binder1st interně deleguje volání na binární funktor, kterému předá jako první argument hodnotu, kterou jsme zadali při vytvoření adaptéru binder1st, a jako druhý argument objekt z kolekce, na kterém se má otestovat platnost podmínky.

binder1st<equal_to<int> > equalPredicate = bind1st(equal_to<int>(), 10);
iterator it1 = find_if(v1.begin(), v1.end(), equalPredicate);

V předchozím kódu jsme vytvořili adaptér (unární funktor) nazvaný equalPredicate, který zprostředkovává přístup k binárnímu funktoru equal_to. Skutečnost, že je  funktor equal_to binárním funktorem, poznáme z jeho deklarace.

template<class Type>
   struct equal_to : public binary_function<Type, Type, bool> 
   {
      bool operator()(
         const Type& _Left, 
         const Type& _Right
      ) const;
   };

Na druhém řádku  příkladu adaptér equalPredicate předáme funkci find_if, která porovná každý element v kolekci (const Type&  _Right)  s hodnotou 10. Funkce vrátí první prvek, který vyhoví podmínce _Left==Right  (konkrétně v našem případě jde o podmínku  10 == PrvekVKolekci). Konstantní hodnota 10 byla předána funkci bind1st a bude  představovat při každém volání "adaptovaného" funktoru equal_to  adaptérem equalPredicate  hodnotu argumentu _Left operátoru(). Funkce bind1st je "syntaktickým cukrem", který zjednodušuje vytváření adaptéru, protože nemusíme specifikovat všechny typové parametry adaptéru binder1st, ale spolehneme se na typovou inferenci provedenou kompilátorem.

Konec rychlé exkurze do C++. I v C# nám mohou adaptéry pro delegáty přijít vhod. Představme si, že již máme napsanou třídu, která vrací výsledek porovnání dvou hodnot ("je menší než", "je větší než").

    static class ComparerEx
    {
        public static bool GreaterThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) > 0;
        }

        public static bool LessThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) < 0;
        }
    }

Funkce chceme použít v LINQ podmínkách (např. můžeme chtít z kolekce celých čísel vrátit jen všechna čísla, jež jsou větší než 10). Ale také můžeme chtít sadu podmínek, které můžeme libovolně kombinovat a skládat tak jednoduše výrazy typu "všechny hodnoty z kolekce, jež jsou větší než 20, ale menší než 90". Stejně tak můžeme chtít za chvíli podmínku znegovat a máme zájem o hodnoty nepatřící do intervalu 20-90. Namísto psaní "jednoúčelových" (i anonymních) metod si můžeme jednotlivé podmínky předpřipravit a pomocí adaptérů pro delegáty je skládat do složitějších podmínek. Také můžeme chtít stejnou podmínku použít při restrikci v LINQu (Where extenze pracující s delegátem typu Func<  >) i při práci se staršími metodami (např. FindAll u List<T>), které očekávají odkaz na delegáta typu Predicate. To vše nám speciální adaptéry pro delegáty v C# umožní.

Nejprve se podívejme na použití adaptérů.

 

class Program
    {
        static void Main(string[] args)
        {

            
            Random rand = new Random();

            //Vygenerování náhodných čísel v rozsahu 1..100
            List<int> myList = new List<int>(Enumerable.Range(1, 100).Select((i) => rand.Next(1, 100)));

            //Vytvoření predikátu pro where část LINQ dotazu (všechna čísla, kromě čísel v rozsahu 10 - 90
            var predicate = FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);
            predicate = FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan));
            predicate = FuncExtension.Not(predicate);
            
            //LINQ dotaz  - v selectu je do anonymního typu vyzvednut i index prvku v kolekci
            var result = myList
                                .Where(predicate)
                                .Select((elem, index) => new {elem, index});
                         

            //Výpis LINQ dotazu
            foreach (var res in result)
            {
                
                Console.WriteLine("{0}:{1}", res.index, res.elem);
                
            }

            //Ukázka konverze podmínky (Func<?, bool> na delegáta typu Predicate očekávaného funkcí FindAll
            var vals = myList.FindAll(FuncExtension.ToPredicate(predicate));
            
            //Musíme dostat stejné výsledky jako v předchozím dotazu s využitím LINQu
            foreach (var val in vals)
            {
                Console.WriteLine(val);
            }

            Console.ReadLine();
        }
    }

V příkladu jsme si naplnili myList náhodnými čísly v intervalu od 1 do 100. Proměnná predicate představuje podmínku.

Použitím adaptéru Bind2nd(FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);) vytvoříme podmínku "všechna čísla větší než  10". Vidíme, že jsme funkci ComparerEx.GreaterThan, která očekává dva argumenty, "adaptovali-převedli" na funkci (přesněji řečeno na delegáta), který očekává jeden argument. Druhým argumentem funkce ComparerEx.GreaterThan je vždy konstantní hodnota 10 předaná  při volání funkce Bind2nd.

V dalším kroku vytvoříme podmínku ("všechna čísla menší než 90" - FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan)); ) a zkombinujeme ji s předchozí podmínkou pomocí speciálního adaptéru, který představuje operátor And (FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan))). Operátor And je pro zbytek aplikace stále jen obyčejným (unární) delegátem na funkci, která přijímá jeden argument a vrací true nebo false. Nyní máme tedy podmínku "všechna čísla větší než 10 a menší než 90".

Naše konečná podmínka ale má mít podobu (""všechna čísla s výjimkou čísel větších než 10 a menších než 90"). Proto použijeme další speciální adaptér Not, který v předchozích krocích sestavenou podmínku zneguje (FuncExtension.Not(predicate);)

Z kolekce myList vybereme přes LINQ všechna čísla splňující podmínku (proměnná predicate s definicí podmínky je argumentem extenzní metody Where) a vypíšeme je do konzole.

Nakonec ještě stejnou podmínku chceme předat metodě FindAll. Metoda FindAll ale očekává delegáta nazvaného Predicate, a proto použijeme další "adaptující" funkci ToPredicate, který stávající definici podmínky konvertuje na Predicate.

Jak adaptéry pracují? Podívejme se na funkci Bind2nd.

 public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

Bind2nd je generická funkce, která jako argument (T1 bindValue) očekává hodnotu, která bude představovat vždy druhý argument adaptovaného delegáta (Func<T0, T1, R> originalFunc - funkce přijímající dva argumenty, první typu T0, druhý typu T1 a vracející R). Funkce vrátí nového delegáta (Func<T0, R>), který ukazuje na funkci očekávající jeden argument  typu  T0 a vracející instanci generického typu R. Delegát při svém spuštění pouze vezme předaný argument (arg) a poskytne jej jako první argument delegátovi originalFunc, kterému současně vždy předá jako druhý argument hodnotu v původním argumentu bindValue.

Podobně fungují i další adaptéry. Pro zajímavost se podívejme na adaptér ToPredicate, který z předaného delegáta vytvoří delegáta typu Predicate, čehož jsme využili v předchozím příkladu.

public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }

Funkce očekává ve svém argumentu originalFunc odkaz na delegáta typu Func, který přijímá jeden argument typu T a vrací bool. My vrátíme delegáta typu Predicate<T>, přičemž vrácený lambda výraz deleguje vykonání funkce na původního delegáta originalFunc. Pro zbytek aplikace je delegát Func<T, bool> skryt za rozhraním adaptéru Predicate<T>, který nám pomohl pro funkci FindAll přeložit podmínku "v neznámém jazyce" do srozumitelné řeči.

Následuje kompletní výpis kódu adaptérů. To, co nám prozatím chybí, je ekvivalent funkce bind1st (bind2nd) z C++, který by nám zjednodušil zápis podmínek bez nutnosti zadávat "ručně" generické argumenty. Ale o tom popřemýšlím zase "někdy jindy". :-)

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FunctionExtensions
{
    static class FuncExtension
    {
        
        public static Func<T1, R> Bind1St<T0, T1, R>(T0 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(bindValue, arg));
        }

        public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

        
        public static Func<T0, bool> Not<T0>(Func<T0, bool> originalFunc)
        {
            return (arg => !originalFunc(arg));
        }

        public static Func<T0, T1, bool> Not<T0, T1>(Func<T0, T1, bool> originalFunc)
        {
            return ((arg1, arg2) => !originalFunc(arg1, arg2));
        }

        public static Func<T0, T1, T2, bool> Not<T0, T1, T2>(Func<T0, T1, T2, bool> originalFunc)
        {
            return ((arg1, arg2, arg3) => !originalFunc(arg1, arg2, arg3));
        }

        public static Func<T0, T1, bool> And<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2) && originalFunc(arg1, arg2));
        }

        public static Func<T0, bool> And<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1  => originalFunc(arg1) && originalFunc2(arg1));
        }

        public static Func<T0, T1, bool> Or<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2)  || originalFunc(arg1, arg2));
        }
        
        public static Func<T0, bool> Or<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1 => originalFunc(arg1) || originalFunc2(arg1));
        }

        public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }
    }
}


Sunday, 05 October 2008 17:34:45 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ | Návrhové vzory