sobota 17. června 2017

Funkcionální programování

V poslední době jsem začal více využívat ve svém kódu principy funkcionálního programování. Zčásti to začalo použitím vzoru Maybe, ale rozvinul jsem to dále a zkušenost to byla tak dobrá, že se o ní podělím.

Hned na začátek pro ty, co mají přístup ke kurzům na pluralsight, uvádím odkaz na dobrý kurz, který vše pěkně dopodrobna vysvětluje - Applying Functional Principles in C# od Vladimira Khorikova.

Tento pán má i své vlastní stránky a dostupná je i knihovna s níže popsanými třídami

Pár základů

U funkcionálního programování se klade důraz na to, co se má udělat místo toho,jak se to má udělat. S funkcionálním přístupem k programování se už asi setkal každý, kdo kdy napsal něco v Linqu.

employees.Where(e => e.From > 2010).OrderBy(e => e.Firstname);

Tento dotazovací jazyk je typickým příkladem funkcionálního programování, pokud ho používáme, tak říkáme co se má dělat a málokdo se zabývá tím, jak se to vlastně pod pokličkou provádí.

Proč se vlastně funkcionální přístup používá?

K vysvětlení výhod funkcionálního přístupu je nejlepší použít kus nějakého klasického kódu. Třeba tento kód by měl něco uložit:
public void Save(object something)
{
    if (something == null)
        throw new ArgumentNullException();
 
    if (!(something is object))
        throw new ArgumentException();
 
    Storage.Save(something);
}
A v tomhle okamžiku začínají problémy. Kdykoliv budeme podobnou funkci volat, nemůžeme si být jistí výsledkem. Může ale nemusí dojít k výjimce a na tu musíme reagovat - a tak vlastně vědomě či nevědomě větvíme tok programu - navíc ošetření výjimky v bloku catch-try připomíná nechvalně známý příkaz go-to....
Funkcionální přístup se snaží tento problém řešit - metoda by neměla o své funkci lhát a tedy vždy by měla vracet, co slibuje. Výjimka nemá být způsobem, jak upozornit na problém, u funkcionálního přístupu je výjimka něco tak výjimečného, že je nejlepší v tomto případě ukončit raději celou aplikaci.
Ale před tím, než popíši, jak to vlastně funkcionální programování řeší, stojí za to se vrátit na chvíli k příkladu a podívat, jak ten se s výjimkou popere. Někde ve volacím řetězci nalezneme obvykle nějaký takový kód:
public string SaveOrReportBug(object something)
{
    try
    {
        Save(something);
    }
    catch (ArgumentNullException)
    {
        return "Cannot save null";
    }
    catch (ArgumentException)
    {
        return "Wrong type";
    }
    catch
    {
        return "Something happened";
    }
 
    return string.Empty;
}
Výjimka je přeložena na něco lidsky pochopitelného a takto pak  prezentována uživateli.

Funkcionální programování celý proces jen zkrátí a to zavedením typu Result - pokud metoda proběhne, jak bylo očekáváno (tzv. happy path), tak vrátí true v property IsSuccess , a pokud ne, tak je IsFailure nastaveno na true a v Error lze nalézt chybové hlášením.
public class Result
{
    protected Result(bool isSuccess, string error)
    {
        if (isSuccess && !string.IsNullOrWhiteSpace(error))
        {
            throw new InvalidOperationException();
        }
 
        if (!isSuccess && string.IsNullOrWhiteSpace(error))
        {
            throw new InvalidOperationException();
        }
 
        IsSuccess = isSuccess;
        Error = error;
    }
 
    public bool IsSuccess { get; }
 
    public string Error { getprivate set; }
 
    public bool IsFailure => !IsSuccess;
}
K tomu je možné si napsat extensivní metody, které vytváření objektů tohoto typu zkrátí:
public static Result Fail(string format, params object[] args)
{
    return new Result(
        false,
        string.Format(
            CultureInfo.InvariantCulture,
            format,
            args));
}
 
public static Result Ok()
{
    return new Result(truestring.Empty);
}
Původní příklad se ze zavedením třídy Result změní a návratový typ se změní z void (nic se nevrací) na Result:
public Result Save(object something)
{
    if (something == null)
        return Result.Fail("Cannot save null.");
 
    if (!(something is object))
        return Result.Fail("Wrong Type");
 
    try
    {
        Storage.Save(something);
    }
    catch
    {
        return Result.Fail("Cannot save ");
    }
 
    return Result.Ok();
}
Metody občas i něco vrací, to lze ošetřit zavedením generického typu Result<T> s property Value:
public class Result<T> : Result
{
    private readonly T value;
 
    protected internal Result(T value, bool isSuccess, string error) 
        : base(isSuccess, error)
    {
        this.value = value;
    }
 
    public T Value
    {
        get
        {
            if (!IsSuccess)
            {
                throw new InvalidOperationException();
            }
            return value;
        }
    }
}
Výhodou funkcionálního přístupu je fakt, že se můžeme spolehnout na to, jak se metoda chová a není nutné vyhledávat si v dokumentaci, jaké výjimky mohou nastat a následně je ošetřovat - metoda se chová skutečně jako funkce a  ne jako pseudo funkce, jak je ilustrováno na tomto obrázku:


Konflikt s principem CQS?

Zkratka CQS znamená  Command-Query Separation principle a byla takto definována Martinem Fowler (https://martinfowler.com/bliki/CommandQuerySeparation.html) - ve zkratce se metody dělí na dotazy a na příkazy.

  • Dotazy vždy něco vrací, ale nemění pozorovatelný stav systému
  • Příkazy nic nevrací, ale mění pozorovatelný stav systému

Použití funkcionálního přístupu nemusí nutně znamenat popření těchto pravidel, stačí si jen zavést tuto konvenci:

Command (can’t fail)        public void Save(Data data)
Query (can’t fail)               public Data GetById(int id)
Command (can fail)           public Result Save(Data data)
Query (can fail)                  public Result<Data> GetById(int id)

Konkrétní případ implementace funkcionálního programovaní popíšu příště.

Žádné komentáře:

Okomentovat