6 rzeczy, które ma F#, a brakuje ich w LINQ

Jednym z ważniejszych, o ile nie najważniejszym interfejsem w .NET jest IEnumerable<T>. LINQ i extension methods zdefiniowane właśnie na tym interfejsie pozwalają na tworzenie nawet bardzo skomplikowanych transformacji danych Oddajmy głos jednemu z autorów:

meijer

I nie ma tutaj znaczenia to, co chciał przekazać w dwóch kolejnych tweetach.

Teraz weźmy pod uwagę F#, język funkcyjny, a więc taki, w którym przetwarzanie danych musiało być poważnie brane pod uwagę przy projektowaniu. W F# aliasem na dot-netowe IEnumerable<T> jest Seq<‘a>. Na każdej kolekcji implementującej więc wspomniane generyczne IEnumerable mamy w F# metody z Seq. Porównajmy:

var avgPdf = Directory.GetFiles(path)
                .Select(f => new FileInfo(f))
                .Where(f => f.Extension == ".pdf")
                .Average(f => f.Length);

z

let avgPdf = Directory.GetFiles path
                |> Seq.map(fun f -> new FileInfo(f))
                |> Seq.filter(fun f -> f.Extension = ".pdf")                    
                |> Seq.averageBy(fun f -> float f.Length)

To samo ? No prawie. W F# musimy tylko zrzutować long-a do floata (język nie pozwala na niejawne konwersje typów).

To teraz poszukajmy różnic. Nie po to, by udowadniać, co jest lepsze (osobiście uwielbiam oba zapisy i ciężko by mi było pracować w innych technologiach), ale dlatego, że każdy, kto pracuje już trochę z LINQ zauważy, że takie rzeczy jak poniżej to by się w C# przydały.

MaxBy, MinBy

W LINQ Min i Max mamy, ale działają tylko na elementach implementujących IComparable. Dodatkowo, jeśli kolekcja zawiera elementy nie implementujące tego interfejsu, to możemy lambdą wskazać property, które chcemy poddawać porównaniom. Jest to więc tak na prawdę Select + Max. A co będzie, jeśli chcemy porównywać po property, ale zwracać cały element ? F# oferuje taką funkcję:

let maxPdf =  Directory.GetFiles path
                |> Seq.map(fun f -> new FileInfo(f))                   
                |> Seq.maxBy(fun f -> f.Length)  

Zwrócona wartość będzie typu FileInfo, pomimo tego, że porównujemy po longach.

DistinctBy

Metodzie Distinct w LINQ ewidentnie brakuje przeciążenia przyjmującego lambdę. Innymi słowy chcielibyśmy mieć możliwość otrzymania zbioru unikalnego pod kątem wybranego property. Zrobimy to poprzez GroupBy + First, ale możemy to w F# zrobić tak:

let fileInfos = Directory.GetFiles path
                    |> Seq.map(fun f -> new FileInfo(f))                  
                    |> Seq.distinctBy(fun f -> f.Extension)

Iter

Jak można w C# wypisać elementy kolekcji na konsolę, bez użycia pętel ?

Directory.GetFiles(path)
            .Select(f => new FileInfo(f))
            .Select(f => { Console.WriteLine(f.FullName); return 0; })
            .ToArray();

Oczywiście nie znam nikogo, kto by tak robił i jest to raczej wymyślone na potrzeby “publicystyczne”. Select przyjmuje tylko Func, a więc delegata, który coś zwraca, dlatego musimy sztucznie cokolwiek zwrócić. Dodatkowo w związku z deferred exceution, żeby to się wykonało musimy wywołać jedną dodatkową metodę, np. ToArray.

Dlatego w F# możemy to zrobić prościej.

Directory.GetFiles path
    |> Seq.map(fun f -> new FileInfo(f)) 
    |> Seq.iter(fun f -> printfn "%A" f.FullName)

FindIndex

Dla tablicy mamy metodę Array.FindIndex, dla listy jest to instancyjna metoda o takiej samej nazwie.Obie metody przyjmują predykat, w którym możemy przekazać intersujący nas warunek. Dla samego IEnumerable metody te nie są jednak dostępne. Być może dlatego, że indeksy kłócą się trochę z koncepcją iteratorów (IEnumerable może przecież nigdy się nie skończyć i zwracać wartości yieldem do końca świata). Niemniej jednak efekt możemy uzyskać w C# poprzez kombinację TakeWhile i Count. W F# możemy zrobić tak:

files |> Seq.findIndex(fun f -> f.Extension = ".pdf")

Unfold

Przetworzenie kolekcji do pojedynczej, skalarnej wartości jest dostępne w obu językach. W C# mamy metodę Aggregate, w F# są to fold lub reduce. Wybieramy jedną z nich w zależności od tego, czy chcemy ustalić akumulator czy przyjąć za niego pierwszy element kolekcji. A co by było, gdybyśmy chcieli stworzyć kolekcję ze skalara ? Mamy w F# operację unfold, odwrotną do fold. Przykładowo kolejne potęgi dla liczb naturalnych (plus zera) tworzymy tak:

let pows = Seq.unfold(fun f -> Some(f ** 2.0, f + 1.0)) 0.0

Startujemy od zera i podajemy funkcję, która zwraca krotkę dwuwartościową. Funkcja będąca przepisem przyjmuje w każdym przejściu wejście, zwraca krotkę, której pierwszym elementem jest wyjście, a drugim wejście do kolejnego kroku. Taka mała maszyna stanów.

Oczywiście jest to iterator nieskończony więc podejrzenie wyników w Visual Studio poskutkuje OutOfMemoryException.

Partition

Dodatkowo bardzo ciekawą metodę zawiera FSharpowa lista. Często zdarza się, że chcemy przefiltrować kolekcję jakimś predykatem, ale interesują nas zarówno elementy, które spełniły warunek, jak i te, które go nie spełniły. Metoda partition dla listy zwraca dwie inne listy na podstawie podanego warunku.

let (even, odd) = Seq.unfold(fun f -> Some(f ** 2.0, f + 1.0)) 0.0
                    |> Seq.take 10
                    |> List.ofSeq
                    |> List.partition(fun f -> f % 2.0 = 0.0)

 

Co tu robić i jak żyć ?

Nie ma powodów do paniki. To, że czegoś nie ma w LINQ, nie oznacza, że nie możemy sobie tego dopisać. W końcu extension methods mają taką cechę, że można je dodać do każdego typu. W sieci jest kilka projektów rozszerzających IEnumerable, takich, jak MoreLINQ czy Interactive Extensions (w Rx). Fajnie jest poprzeglądać ten kod i zobaczyć, jak wiele rzeczy można jeszcze załatwić extension methodami.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s