F# pierwsze kroki (po raz drugi)

Na ostatnim (tj. 90) spotkaniu KGD.NET miały miejsce dwie prezentacje. Prezentacja Procenta o DI to było to na co czekałem (tzn. potwierdzenie, że idę słuszną drogą oraz rozwianie pewnych wątpliwości). Druga prezentacja (a może lepiej powiedzieć pierwsza) była poświęcona F#. Nie spodziewałem się nic specjalnego po niej. Co tu dużo mówić pewien czas temu stworzyłem nawet coś w tym języku, ale mówiąc szczerze jakoś specjalnie mi to nie podchodziło. Jednak Michał Łusiak pokazał, że F# to język, któremu warto dać jeszcze jedną szansę. Tylko, że tym razem podchodzę do tego języka jako do języka dodatkowego a ponadto mam już pewne doświadczenie w C# więc powinno być mi łatwiej.

Początki

Na początek musiałem przypomnieć sobie składnie.

let x=5
let y=x+1

Czyli utwórz “zmienną” x i przypisz do niej wartość 5 a następnie utwórz “zmienną” y i przypisz do niej wartość x powiększoną o 1. Czemu napisałem “zmienną” zamiast zmienną? Otóż nie da się przypisać nowej wartości do raz utworzonej “zmiennej”. Próba napisania czegoś takiego jak poniżej będzie skutkowała błędem podczas kompilacji (przy okazji widać sposób przypisania nowej wartości)

let x=5
x<-x+1

Jednak komunikat błędu wskaże nam rozwiązanie:

This value is not mutable

Wystarczy więc poprzedzić zmienną słówkiem mutable i wszystko już działa

let mutable x=5
x<-x+1

Teraz może czas napisać jakąś funkcje np. wyliczającą wartość bezwzględną

let AbsoluteValue x y=
    if(x>y) then        
        x-y
    elif (x=y) then
        0
    else
        y-x

oczywiście jest to tylko przykład i to na dodatek przykład, który można napisać dużo lepiej, ale chodziło mi o jedynie o składnie.

Chwila zwątpienia

Właśnie takie przykłady (oraz bardziej skomplikowane ma się rozumieć) spowodowały, że zarzuciłem kiedyś naukę tego języka. Ja jestem jak niewierny Tomasz nie zobaczę nie uwierzę. I właśnie w prezentacji Michała (swoją drogą świetny tytuł “WTF# – czym jest F# i dlaczego powinno Cie to obchodzic”) to mi się spodobało – spięcie F# z C#. Czyli robię to co znam (umiem?) wraz z czymś nowym. Moim celem było napisanie klasy, której zadaniem będzie hashowanie tekstu (nie chciałem się zbytnio męczyć). Zaczynam więc od stworzenia nowego projektu w F#.
newFSharpProject"
Wybieram F# Library i zaczynam rzeźbić. Najpierw zastanowiłem się jakbym napisał takie coś w C#.

using System;
using System.Security.Cryptography;
using System.Text;

namespace MyCSharpLibrary
{
    public class StandardHash
    {
        public byte[] HashInput(string input)
        {
            SHA512 sha512 = SHA512.Create();
            byte[] data = Encoding.UTF8.GetBytes(input);
            byte[] result = sha512.ComputeHash(data);
            return result;
        }
    }
}

To teraz spróbujmy napisać to samo w F#

namespace MyFSharpLibrary

type public StandardHash() = 
    
    member this.HashInput(input:string)=
        let sha512=System.Security.Cryptography.SHA512.Create()
        let data =System.Text.Encoding.Unicode.GetBytes(input)
        let result=sha512.ComputeHash(data)
        result

Jak widać linijka po linijce mamy to samo. Nie byłbym jednak sobą gdybym nie spróbował napisać tego ciut inaczej

namespace MyFSharpLibrary

type public StandardHash() = 
    
    member this.HashInput(input:string)=
        use sha512=System.Security.Cryptography.SHA512.Create()
        input
        |>System.Text.Encoding.Unicode.GetBytes
        |>sha512.ComputeHash

Efekt ten sam a o niebo czytelniej. Oczywiście można tak napisać i w C# ale byłoby to “ciut” nieczytelne

return SHA512.Create().ComputeHash(Encoding.UTF8.GetBytes(input));

Jeśli ktoś jest spostrzegawczy to z pewnością zauważył, że raz użyłem słówka let a innym drugim razem use. Czy to znaczy, że można ich używać zamiennie? Najlepiej sprawdzić. Zamiast let data napisze use data i … otrzymam taki komunikat

Type constraint mismatch. The type 
    byte []    
is not compatible with type
    System.IDisposable    
The type 'byte []' is not compatible with the type 'System.IDisposable'

Wszystko jasne use tylko do IDisposable (czyli przykład w C# jest zły brakuje using). Korzystając z ILSpy podglądając kod a tam mam blok try-finally (oczywiście jeśli korzystałem z use).
Użycie tak napisanej biblioteki w innym projekcie jest banalnie proste wystarczy standardowe Add Reference…. Jedyne co odróżnia biblioteki napisane w C# od tych napisanych w F# to konieczność zbuildowania tych drugich, żeby z innego projektu IntelliSense było w stanie podpowiadać (ta wiedza przydała mi się nieco później).

Problemy

jestem fanem podejścia z wykorzystaniem IoC więc nie odmówiłem sobie tej przyjemności i postanowiłem założyć interfejs nad moją klasą StandardHash.

type public IHash=interface
    abstract HashInput:input:string->byte[]
    end

Można to zapisać jako

type public IHash=
    abstract HashInput:input:string->byte[]

tylko, że jest to dla mnie mniej czytelne.
Mamy więc interfejs teraz trzeba jeszcze go zaimplementować.

W kodzie klasy StandardHash piszemy:

    interface MyFSharpLibrary.IHash with
        member this.HashInput(input)=this.HashInput(input)

Na pozór masło maślane z lewej strony to samo co z prawej. Tylko na pozór otóż jest to informacja, że pod metodą HashInput (lewa strona) ma być odpalona metoda HashInput(prawa strona) więc nic nie stoi na przeszkodzie aby po prawej stronie znalazła się metoda o innej nazwie, ale robiąca to na czym nam zależy. Oczywiście musimy zaimplementować (a może lepiej powiedzieć zmapować) wszystkie metody z interfejsu inaczej nie uda się zbuildować projektu. Ponieważ zarówno C# jak i F# należą do platformy .net to bez większych problemu można używać części napisanych w jednym języku w drugim. Interfejs IHash może implementować klasa napisana w C#. Jeśli ktoś będzie chciał podglądnąć kod tego interfejsu to ujrzy ni mniej ni więcej

[Serializable]
public interface IHash
{
     byte[] HashInput(string input);
}

Właśnie dlatego konieczne jest wcześniejsze zbuildowanie projektu.
Zwykle w C# interfejs umieszczam w pliku zawierającym jego implementacje chyba że kilka klas implementuje ten sam interfejs wtedy przenoszę go do osobnego pliku. Tak też było tym razem. Dodałem nowy plik, w którym umieściłem definicje interfejsu (a w zasadzie to skopiowałem istniejącą). W tym momencie projekt przestał się buildować. WTF pomyślałem. Błąd wskazywał na to, że nie istnieje interfejs, który dodałem przed chwilą. Głupio myśląc postanowiłem sprawdzić zawartość pliku fsproj. A tam jak najbardziej był umieszczony mój plik. Jako drugi z kolei. Zamieniłem więc kolejność odpalam i wszystko gra. Jak się później okazało nie musiałem wcale grzebać w tym pliku. W Solution Exploer jest możliwość ustalenia kolejności w jakiej te pliki są kompilowane.
fSharpMenuItem"

Podsumowanie

Ktoś kto zna F# pewnie powie, że pominąłem wiele ważnych rzeczy np. listy oraz ich łączenie, rekurencje. Może on powiedzieć, że to co pokazałem to nie jest prawdziwe zastosowanie F#, że jest on przeznaczony do całkiem innych celów. I ktoś taki będzie miał rację, ale nie oto mi chodziło. Moim celem było pokazanie, że F# nie gryzie oraz, że z powodzeniem można go stosować w standardowych projektach, że może czasem warto wyjść poza utarte schematy. Bo chyba każdy dev ma świadomość tego, że język w którym pisze to nie jest raczej język, w którym będzie pisał w momencie przejścia na emeryturę (no ale kwestia emerytury to już inna bajka).
—–
Jeśli ktoś chce się zagłębić w temetykę to ze swojej strony mogę polecić F# for C# Developers

5 thoughts on “F# pierwsze kroki (po raz drugi)

  1. Pingback: dotnetomaniak.pl
  2. Moim zdaniem podstawowym popełnianym przez ciebie błędem są próby aplikowania technika z OOP do języka funkcyjnego. Jedyny raz kiedy musiałem użyć interface’ów w F# był wtedy, kiedy napisana przeze mnie biblioteka miała być używana z poziomu C# i potrzeba było wystawić do tego odpowiednie API. Dependency Injection w języku funkcyjnym? Po co? Mamy przecież currying.

    Zamiast skupiać się na podejściu wiem jak to zrobić w C#, jak przepisać ten kod na F# skup się na tym jaki jest problem i jak rozwiązać go w paradygmacie funkcyjnym. Podstawową kwestią jest zrozumienie, że tak jak główną jednostką kompozycji w językach obiektowych są klasy/obiekty, tak w językach funkcyjnych są nimi funkcje.

    1. Zgadza się próbowałem zastosować techniki z OOP do języka funkcyjnego. Interfejs był zastosowany z premedytacją. Mam teraz interfejs i jego implementację w F# a z drugiej strony ktoś inny może zrobić implementację w C# i to razem będzie działać. Tak jak pisałem wiem, że to co pokazałem to nie jest najwłaściwsze zastosowanie F#, ale od czegoś trzeba zacząć. Jest to szczególnie istotne gdy zespół nie jest do końca przekonany. Nikt przy zdrowych zmysłach nie zacznie tworzyć przecież tworzyć całego kodu w nowym dla siebie języku. Trzeba zacząć od niewielkich wstawek i zaprezentowanie takiej właśnie możliwości było moim celem.

      1. Jeżeli chcesz przekonać osoby niezdecydowane do przeskoku C# -> F#, nie wiem czy pokazywanie czegoś, w czym F# radzi sobie gorzej niż C# jest najlepszym pomysłem. Można wprowadzać F# małymi krokami, korzystając z niego w miejscach, gdzie nie musi on współpracować bezpośrednio z C# np. testy (FsCheck pod względem generowania danych testowych odstawia w tyle wszystko, co C# oferuje do tej pory, w dodatku można go zintegrować np. z XUnit) lub buildy (FAKE jest o wiele wygodniejszy i bardziej zaawansowany niż buildery oparte o XML oraz bardziej dojżały niż PVC lub psake).
        Nikt przy zdrowych zmysłach nie zacznie tworzyć przecież tworzyć całego kodu w nowym dla siebie języku.
        To jest kwestia wyłącznie ryzyka projektowego oraz nastawienia zespołu. Są teamy, dla których przejście z .NET/C# na JVM/Scalę nie jest niczym niemożliwym, w innych z kolei podniesienie wersji Entity Frameworka z 4 na 5 w nowych projektach uznawane jest za zbyt wielkie ryzyko. Jedyne problemy to a) poznanie zasad rozwiązywania problemu w nowym paradygmacie b) poznanie frameworka. Składnia języka to kwestia dni/tygodni – w przypadku języków bardzo rozbudowanych (np. F#, Scala, C++) nie potrzeba ich poznawać w całości, można śmiało tworzyć pełnowartościowe programy znając tylko pewien podzbiór ich funkcjonalności.

  3. FAKE faktycznie wygląda ciekawie i może w sumie warto od czegoś takiego zacząć.

Comments are closed.