Úvod do konceptů funkčního programování v jazyce F#

Funkční programování je styl programování, který zdůrazňuje použití funkcí a neměnných dat. Funkční programování typu je v kombinaci funkčního programování se statickými typy, jako je F#. Obecně platí, že v funkčním programování jsou zdůrazněny následující koncepty:

  • Funkce jako primární konstrukce, které používáte
  • Výrazy místo příkazů
  • Neměnné hodnoty u proměnných
  • Deklarativní programování nad imperativním programováním

V této sérii prozkoumáte koncepty a vzory v funkčním programování pomocí jazyka F#. Kromě toho se naučíte také F#.

Terminologie

Funkční programování, podobně jako jiné programovací paradigmata, obsahuje slovní zásobu, kterou se nakonec budete muset naučit. Tady jsou některé běžné termíny, které uvidíte po celou dobu:

  • Funkce – Funkce je konstrukce, která při zadání vstupu vytvoří výstup. Formálněji mapuje položku z jedné sady na jinou sadu. Tento formalismus je v mnoha ohledech zvednut do betonu, zejména při použití funkcí, které pracují s kolekcemi dat. Jedná se o nejzásadnější (a důležité) koncepty funkčního programování.
  • Výraz – výraz je konstruktor v kódu, který vytváří hodnotu. V jazyce F# musí být tato hodnota vázána nebo explicitně ignorována. Výraz lze triviálně nahradit voláním funkce.
  • Čistota - čistota je vlastnost funkce tak, aby její návratová hodnota byla vždy stejná pro stejné argumenty a že jeho vyhodnocení nemá žádné vedlejší účinky. Čistá funkce zcela závisí na jejích argumentech.
  • Referenční transparentnost - Referenční transparentnost je vlastnost výrazů, které mohou být nahrazeny jejich výstupem, aniž by to mělo vliv na chování programu.
  • Neměnnost – Neměnnost znamená, že hodnotu nelze změnit na místě. To je na rozdíl od proměnných, které se můžou změnit.

Příklady

Následující příklady ukazují tyto základní koncepty.

Functions

Nejběžnějším a základním konstruktorem funkčního programování je funkce. Tady je jednoduchá funkce, která přidá 1 do celého čísla:

let addOne x = x + 1

Podpis jeho typu je následující:

val addOne: x:int -> int

Podpis lze přečíst jako "addOne přijme pojmenovaný intx a vytvoří " int. Formálněji addOne mapuje hodnotu ze sady celých čísel na množinu celých čísel. Token -> označuje toto mapování. V jazyce F# se obvykle můžete podívat na podpis funkce, abyste získali představu o tom, co dělá.

Proč je podpis důležitý? V typovém funkčním programování je implementace funkce často méně důležitá než podpis skutečného typu! Skutečnost, že addOne sčítá hodnotu 1 do celého čísla, je zajímavé v době běhu, ale při vytváření programu, skutečnost, že přijímá a vrací int , je to, co informuje, jak budete tuto funkci skutečně používat. Kromě toho, jakmile tuto funkci použijete správně (s ohledem na její typ podpisu), může být diagnostika jakýchkoli problémů provedena pouze v těle addOne funkce. Toto je podnět pro typové funkční programování.

Výrazy

Výrazy jsou konstrukce, které se vyhodnotí jako hodnota. Na rozdíl od příkazů, které provádějí akci, lze výrazy považovat za provedení akce, která vrací hodnotu. Výrazy se téměř vždy používají v funkčním programování místo příkazů.

Vezměte v úvahu předchozí funkci, addOne. addOne Text je výraz:

// 'x + 1' is an expression!
let addOne x = x + 1

Je výsledkem tohoto výrazu, který definuje typ výsledku addOne funkce. Například výraz, který tvoří tuto funkci, může být změněn tak, aby byl jiným typem, například string:

let addOne x = x.ToString() + "1"

Podpis funkce je teď:

val addOne: x:'a -> string

Vzhledem k tomu, že jakýkoli typ v jazyce F# jej může ToString() volat, x typ byl proveden obecný (nazývá se automatická generalizace) a výsledný typ je .string

Výrazy nejsou jen těla funkcí. Můžete mít výrazy, které vytvářejí hodnotu, kterou používáte jinde. Běžným je if:

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

Výraz if vytvoří hodnotu s názvem result. Všimněte si, že byste ho mohli úplně vynechat result , což znamená if , že výraz bude tělo addOneIfOdd funkce. Klíčovou věcí, kterou je potřeba pamatovat na výrazy, je, že vytvářejí hodnotu.

Existuje speciální typ , unitkterý se používá, když není nic vrátit. Představte si například tuto jednoduchou funkci:

let printString (str: string) =
    printfn $"String is: {str}"

Podpis vypadá takto:

val printString: str:string -> unit

Typ unit označuje, že se nevrací žádná skutečná hodnota. To je užitečné v případě, že máte rutinu, která musí "pracovat", i když nemá žádnou hodnotu, která by se v důsledku této práce vrátila.

To je na ostrém kontrastu s imperativním programováním, kde ekvivalentní if konstruktor je příkaz a vytváření hodnot se často provádí s mutačními proměnnými. Například v jazyce C# může být kód napsán takto:

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

Je vhodné poznamenat, že jazyk C# a další jazyky ve stylu jazyka C# podporují ternární výraz, který umožňuje podmíněné programování založené na výrazech.

V funkčním programování je vzácné ztlumit hodnoty pomocí příkazů. I když některé funkční jazyky podporují příkazy a mutaci, není běžné tyto koncepty používat v funkčním programování.

Čisté funkce

Jak jsme už zmínili, čisté funkce jsou funkce, které:

  • Vždy se vyhodnotí jako stejná hodnota pro stejný vstup.
  • Nemá žádné vedlejší účinky.

V tomto kontextu je užitečné uvažovat o matematických funkcích. V matematice jsou funkce závislé pouze na jejich argumentech a nemají žádné vedlejší účinky. V matematické funkci f(x) = x + 1závisí hodnota f(x) pouze na hodnotě x. Čisté funkce v funkčním programování jsou stejné.

Při psaní čisté funkce musí funkce záviset pouze na jejích argumentech a nesmí provádět žádnou akci, která má za následek vedlejší účinek.

Tady je příklad nečisté funkce, protože závisí na globálním, proměnlivém stavu:

let mutable value = 1

let addOneToValue x = x + value

Funkce addOneToValue je jasně nečistá, protože value je možné ji kdykoli změnit tak, aby měla jinou hodnotu než 1. Tento vzor v závislosti na globální hodnotě je vyhýbání se funkčnímu programování.

Tady je další příklad nečisté funkce, protože provádí vedlejší efekt:

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

I když tato funkce nezávisí na globální hodnotě, zapíše hodnotu x do výstupu programu. I když s tím není nic zlého, znamená to, že funkce není čistá. Pokud jiná část programu závisí na něčem externím programu, například na výstupní vyrovnávací paměti, může volání této funkce ovlivnit jinou část programu.

Odebráním printfn příkazu je funkce čistá:

let addOneToValue x = x + 1

I když tato funkce není ze své podstaty lepší než předchozí verze s printfn příkazem, zaručuje, že všechna tato funkce vrací hodnotu. Volání této funkce libovolný početkrát vytvoří stejný výsledek: pouze vytvoří hodnotu. Předvídatelnost daná čistotou je něco, co se mnoho funkčních programátorů snaží.

Neměnitelnost

A konečně, jeden z nejzákladnějších konceptů typového funkčního programování je neměnnost. V jazyce F# jsou všechny hodnoty ve výchozím nastavení neměnné. To znamená, že je nelze ztlumit na místě, pokud je explicitně označit jako proměnlivé.

V praxi práce s neměnnými hodnotami znamená, že změníte svůj přístup k programování z " Potřebuji něco změnit", na "Potřebuji vytvořit novou hodnotu".

Například přidání 1 k hodnotě znamená vytvoření nové hodnoty, nikoli ztlumení existující hodnoty:

let value = 1
let secondValue = value + 1

V jazyce F# následující kód funkci neztlumívalue. Místo toho provede kontrolu rovnosti:

let value = 1
value = value + 1 // Produces a 'bool' value!

Některé funkční programovací jazyky vůbec nepodporují mutaci. V jazyce F# se podporuje, ale nejedná se o výchozí chování pro hodnoty.

Tento koncept se ještě více rozšiřuje o datové struktury. V funkčním programování mají neměnné datové struktury, jako jsou sady (a mnoho dalších), jinou implementaci, než byste mohli původně očekávat. Koncepčně, například přidání položky do sady, nezmění sadu, vytvoří novou sadu s přidanou hodnotou. Pod kryty je to často dosaženo jinou datovou strukturou, která umožňuje efektivní sledování hodnoty, aby bylo možné v důsledku toho dát odpovídající reprezentaci dat.

Tento styl práce s hodnotami a datovými strukturami je kritický, protože vás přinutí zacházet s jakoukoli operací, která něco upraví, jako by vytvořil novou verzi této věci. To umožňuje, aby ve vašich programech byly konzistentní věci, jako je rovnost a porovnatelnost.

Další kroky

V další části se důkladně zaměříme na funkce a prozkoumáte různé způsoby jejich použití v funkčním programování.

Použití funkcí v jazyce F# zkoumá funkce hlouběji a ukazuje, jak je můžete používat v různých kontextech.

Další texty

Série Myšlení funkčně je dalším skvělým zdrojem informací o funkčním programování pomocí jazyka F#. Zabývá se základy funkčního programování praktickým a snadno čitelným způsobem pomocí funkcí jazyka F#, které ilustrují koncepty.