Lubię porządne języki programowania a C# z pewnością jest jednym z nich. To moje główne narzędzie pracy. Nie jest to jednak jedyny dobry język programowania na świecie. Kiedy w pracy pojawiła się okazja napisać jeden z naszych serwisów w F#, byłem bardziej niż szczęśliwy. Niektóre projekty były już napisane w C# i przepisywałem je na F#, inne trzeba było pisać od zera. Myślałem, że F# jest tylko kolejnym językiem z dziwną składnią, który robi te same rzeczy w taki sam sposób. Byłem w całkowitym błędzie. Miałem problem z napisaniem metody w interfejsie, potem z zaimpelemtowaniem jej. A potem z użyciem jej w innym miejscu. To wszystko jest tak bardzo inne od C#, że ciężko to wyrazić w słowach. Poza inną składnią obiekty buduje się też i używa w inny sposób (jak choćby to, że w domyśle są niezmienne). Deklaracja metody w C# wszystko mówi o typach już w pierwszej linijce:
Moim pierwszym zadaniem było napisać implementację interfejsu z jedną metodą:
Zaskakujące tu są też zasięgi (scopy).
Jeśli połączymy dwa fakty:
F# przynosi też kilka pułapek. Jeśli stworzysz sobie nowy projekt asp.net mvc, zobaczysz kilka metod wygenerowanych dla Ciebie na dobry start:
Albo powiedzmy, że chcesz zdeserializować wyjście z serwisu do swojego obiektu żeby pobrać wartość właściwości. Tworzysz sobie typ do zdeserializowania
Jest wiele pułapek czyhających na programistę C# robiącego pierwsze kroki w F#. Ale absolutnie warto. Zrób sobie przysługę i naucz się F#. Znalazłem kilka książek, które były bardzo przydatne w najbardziej frustrujących momentach, wśród nich jest jedna,do której sięgałem najczęściej. "Get Programming with F#: A guide for .NET developers" autora: Isaac Abraham. Gdybym miał polecić tylko jedną książkę- poleciłbym tą. Idź coś stwórz.
string method( string param1, int param2)
Podczas gdy w F# może wyglądać bardziej jak to:
let method param1 param2 : string
Widzisz, że parametry nie mają typu? Nie potrzeba go tu wpisywać, Możesz, jeśli chcesz:
let method param1:string param2:string : string
Ale rzecz w tym, że nie musisz. F# jest statycznie typowany tak samo jak C#, tylko, że sprawdzi czy typy są poprawnie używane podczas kompilacji. Trochę upraszczając to tak jakby domyślnie wszystko było generyczne. W praktyce F# podczas kompilacji próbuje dopasować typ i jeśli da się użyć danej funkcji z tym typem, to ją poniekąd sobie dopisuje. Typ zwracany funkcji też może być wyciągnięty z ciała funkcji, dlatego możemy stworzyć funkcję np taką:
let add param1 param2 = param1 + param2
Powyższa linijka zdefiniowała funkcję dodającą dwie rzeczy. Użyj jej z liczbami - doda liczby, użyj ze stringami - doda stringi. Jeśli użyjesz ze stringiem i liczbą - dostaniesz błąd kompilacji. Brzmi uczciwie.Moim pierwszym zadaniem było napisać implementację interfejsu z jedną metodą:
public interface IConfigurationApi
{
Task GetAsync(string path);
}
W F# można używać Task<> ale jest też inny sposób na asynchronicznośc (bardziej naturalny dla F#). Ponieważ to jest nowy projekt (z elementami do przeniesienia z C#), ciągle mogłem zmienić interfejs bez konieczności przerabiania całego projektu.
type IConfigurationApi =
abstract member GetAsync : path: string -> Async
//implementacja poniżej
type ConfigurationService() =
interface IConfigurationApi with
member this.GetAsync path =
let ......
W F# implementowany interfejs jest deklarowany przy okazji implementacji samych metod, inaczej niż w C#, gdzie deklarujemy to zaraz na początku definicji klasy.Zaskakujące tu są też zasięgi (scopy).
Jeśli połączymy dwa fakty:
- Prawie wszystko jest wyrażeniem a wyrażenia mają wartość
- Dodając kilka spacji w kodzie tworzymy zasięg "bardziej lokalny", który też jest wyrażeniem.
let result = //1
let mutable p = 0 //2
for i = 1 to 10 do //3
p <- p + i //4
p //5
Console.WriteLine result // "55"
Szybkie wyjaśnienie: Chcę przypisać zmiennej "result" jakąś obliczoną wartość (1). Obliczenia są w liniach 2-5, które mają mocniejsze wcięcie a więc są "bardziej lokalne". Użyję zmiennej tymczasowej "p" (2). Wykonam moje obliczenia (3,4) i zwrócę obliczoną wartość (5) do zasięgu "mniej lokalnego", który jest linijką przypisania (1). Bardzo fajnie. Po powrocie do oryginalnego zasięgu, zmienne z linijek 2-5 już nie istnieją i nie będą nam zaśmiecać aktualnego zasięgu.F# przynosi też kilka pułapek. Jeśli stworzysz sobie nowy projekt asp.net mvc, zobaczysz kilka metod wygenerowanych dla Ciebie na dobry start:
member this.ConfigureServices(services: IServiceCollection) =
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) |> ignore
member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) =
if (env.IsDevelopment()) then
app.UseDeveloperExceptionPage() |> ignore
else
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts() |> ignore
app.UseHttpsRedirection() |> ignore
app.UseMvc() |> ignore
Możesz popaść w tarapaty, kiedy je nieostrożnie rozszerzysz. Na przykład weźmy tą pozornie niewinną linijkę:
member this.ConfigureServices(services: IServiceCollection) =
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) |> ignore
//patrz poniżej:
services.AddSwaggerGen()
Powinna dodać Swaggera i nic więcej, prawda? Nie, metoda ConfigureServices przestanie być wołana w ogóle. Dlaczego tak się dzieje? Widzisz, ConfigureServices jest szczególną metodą, która musi mieć szczególną sygnaturę. Musi zwracać void (lub "unit" w F#). Ale typ zwracany nie jest zadeklarowany. W tej sytuacji jest wnioskowany z kodu (pisałem o tym wyżej). Po dodaniu linijki ze swaggerem, ostatnie wyrażenie (którego typ jest zwracany jako wynik funkcji), zwraca IServiceCollection, co zmienia sygnaturę metody. To jest podchwytliwe, bo projekt się skompiluje, tylko przestanie się konfigurować. Żeby naprawić sytuację, wystarczy dodać "|> ignore" na końcu ostatniej linijki (jak w linijkach automatycznie generowanych). Albo powiedzmy, że chcesz zdeserializować wyjście z serwisu do swojego obiektu żeby pobrać wartość właściwości. Tworzysz sobie typ do zdeserializowania
type ObjectWithStatus= {StatusName : string}
i wyciągasz sobie wynik serwisu
let data = api.GetResource()
F# w normalnej sytuacji nie zezwala na nulle, typ ObjectWithStatus nie pozwoli na to żeby ustawić mu wartość null. Ale rzecz w tym, że api może zwrócić nulla (choćby nawet z powodu, że jest pisane w C#). Żeby było gorzej, nie możesz sobie porównać wartości zwróconej przez api z nullem, bo skoro typ nie dopuszcza nulli, to kompilator nie pozwoli na porównanie z czymś co jest niemożliwe :). I mamy dziwną sytuację - nulle są nie możliwe, a są zwracane przez serwis, do tego nie możemy prostym ifem sprawdzić wartości bo nie wolno porównywać z nullami (prawdziwa historia z prawdziwego kodu). Wyjściem z tego jest sztuczka z rzutowaniem obiektu na "obj" a dopiero później porównanie do nulla
if data :> obj <> null
then ......
Eleganckie? nie za bardzo, ale działa poprawnie.Jest wiele pułapek czyhających na programistę C# robiącego pierwsze kroki w F#. Ale absolutnie warto. Zrób sobie przysługę i naucz się F#. Znalazłem kilka książek, które były bardzo przydatne w najbardziej frustrujących momentach, wśród nich jest jedna,do której sięgałem najczęściej. "Get Programming with F#: A guide for .NET developers" autora: Isaac Abraham. Gdybym miał polecić tylko jedną książkę- poleciłbym tą. Idź coś stwórz.
Komentarze
Prześlij komentarz