Obsah
1. Algebraické datové typy v Pythonu
4. Datový typ disjunktní sjednocení (discriminated union)
5. Od jazyků z rodiny ML k Pythonu
9. Výpočet plochy geometrických tvarů: základní varianta
10. Detekce hodnoty nekorektního typu
11. Využití strukturálního pattern matchingu
12. Detekce hodnoty nekorektního typu v konstrukci se strukturálním pattern matchingem
13. Zachycení atributů do proměnných se stejným názvem, jaký mají původní atributy
16. Chování při odstranění jedné větve z konstrukce match-case
17. Chování v případě, že je součtový datový typ neúplný
18. Chování při použití zcela nepodporovaného typu při volání funkce area, využití assert_never
19. Repositář s demonstračními příklady
1. Algebraické datové typy v Pythonu
V seriálu o programovacích jazycích ML, CAML, OCaml a F#, který na Rootu vycházel zhruba před dvěma roky, jsme zabývali mj. i jednou z nejlepších technologií, kterou jsou tyto programovací jazyky vybaveny. Jedná se o podporu takzvaného algebraického typového systému (algebraic type system). Jedná se o označení takového typového systému, který umožňuje z jednodušších datových typů skládat složitější (kompozitní) datové typy s (velmi zjednodušeně řečeno) využitím operátorů and a or – výsledný typ tedy buď obsahuje položky všech uvedených typů nebo jednu z těchto položek. Výsledné datové typy se často označují termíny product types a sum types, ovšem v praxi se spíše setkáme s konkrétními realizacemi těchto typů: n-ticemi, záznamy a disjunktním sjednocením.
Algebraické datové typy je možné do jisté míry využít i v Pythonu. Typicky se v tomto případě používá řešení založené na typových informacích (type hints), datových třídách (data classes) a strukturálním pattern matchingu. Jak uvidíme v praktické části dnešního článku, nejedná se vlastně ze syntaktického pohledu o žádné nové konstrukce, ovšem jejich sémantický význam je poměrně velký a posouvá Python k jazykům s vyspělým typovým systémem (zde skutečně urazil dlouhou cestu).
2. Datový typ záznam (record)
Nejdříve se zaměříme na datové typy, které jsou složeny z jiných (jednodušších) datových typů takovým způsobem, že v novém typu musí být obsaženy všechny specifikované složky. V angličtině se tyto typy označují termínem product type a pravděpodobně nejznámějším takovým typem v běžných programovacích jazycích jsou záznamy (record). Příkladem je definice záznamu v jazyku F#:
type car = { Color: string; Model: string; Manufacturer: string; Year: int; }
Následuje příklad definice záznamu v programovacím jazyku Go, který záznamy taktéž přímo podporuje (nazývají se zde struct):
type Car struct { Color string Model string Manufacturer string Year int }
Povšimněte si, že se skutečně jedná o product type, tj. o datový typ, jehož hodnoty mohou obsahovat všechny kombinace hodnot jeho složek. To je v případě složek typu string a int obrovské množství kombinací, proto si ukažme jednodušší strukturu/záznam:
type AccessRights struct { read bool write bool execute bool }
Každá ze složek záznamu může obsahovat jednu z hodnot true nebo false, což jsou dvě hodnoty. Celkem tedy může nový typ AccessRights nabývat jedné z 2×2×2=8 hodnot (zde je již pravděpodobně patrné, proč se používá termín product type).
3. Datový typ n-tice (tuple)
Ve skutečnosti nejsou záznamy tím nejjednodušším součinovým datovým typem. Poněkud jednodušší jsou n-tice, které jsou taktéž některými jazyky podporovány. Příkladem bude opět programovací jazyk F#. V tomto jazyku mohou n-tice obsahovat prvky libovolných typů. Typ n-tice jako celku je pak odvozen od typů jednotlivých prvků. Speciálním případem je n-tice bez prvků, neboli datový typ unit.
Ukažme si příklad použití n-tice se dvěma prvky typu int v jazyku F#:
Printf.printf "%A" (1,2)
Typ této n-tice se zapisuje takto:
int * int = (1, 2)
Typ složitější n-tice, která obsahuje prvky různých typů:
(1, 1.5, "foo", (1,2)) ;; - : int * float * string * (int * int) = (1, 1.5, "foo", (1, 2))
N-tici je samozřejmě možné přiřadit do proměnné a poté si nechat vytisknout její obsah:
let x = (1,2,3) Printf.printf "%A" x
4. Datový typ disjunktní sjednocení (discriminated union)
Velmi důležitým datovým typem je v programovacím jazyku F# (ale nikoli pouze zde) typ nazývaný disjunktní sjednocení neboli discriminated union. V té nejjednodušší podobě může být tento typ definován pouhým výčtem možností:
type Day = Po | Ut | St | Ct | Pa | So | Ne let x = St printf "%A\n" x
Alternativní způsob zápisu:
type Day = | Po | Ut | St | Ct | Pa | So | Ne let x = St printf "%A\n" x
Až doposud by se mohlo zdát, že se vlastně jedná o typ výčet, ovšem možnosti disjunktního sjednocení jsou mnohem větší. Pro některé jazyky, mj. i pro Rust, je typické a idiomatické použití typů Option a Result:
type Option<'a> = | Some of 'a | None
a:
type Result<'T,'TError> = | Ok of ResultValue:'T | Error of ErrorValue:'TError
Nejedná se o nic jiného, než právě o disjunktní sjednocení.
5. Od jazyků z rodiny ML k Pythonu
„Objects are an open universe, where clients can implement new subclasses that were not known at definition time; ADTs are a closed universe, where the definition of an ADT specifies precisely all the cases that are possible.“
Algebraické datové typy se používají v několika desítkách programovacích jazyků, ovšem pravděpodobně nejznámější je jejich využití v jazycích z rodiny ML. Do této skupiny spadají především jazyky ML, Standard ML, CAML, OCaml a F# (možná by se do této skupiny mohl zařadit i jazyk Elm). Nás ovšem bude primárně zajímat způsob využití algebraických datových typů v Pythonu. A vzhledem k tomu, že interpret programovacího jazyka Python neprovádí prakticky žádné statické typové kontroly, doplníme tento nástroj o další dvojici užitečných nástrojů. Bude se jednat o nástroje Mypy a Pyright. V následujících kapitolách se nejdříve ve stručnosti seznámíme s problematikou statických typových kontrol, řekneme si, jak lze v Pythonu implementovat součinové a součtové typy a nakonec si v několika krocích ukážeme postupnou modifikaci příkladu, který je na těchto dvou datových typech založen (i když to zpočátku nemusí být zřejmé).
6. Statické typové kontroly
Python samotný již od verze 3.5 podporuje zápis takzvaných typových anotací resp. nápověd (type annotations, type hints). Jedná se o nepovinnou (zcela dobrovolnou) specifikaci typů parametrů funkcí a metod, návratových hodnot funkcí a metod, typů proměnných atd. A právě zápis typových anotací do značné míry umožňuje provedení statických typových kontrol.
Myšlenka, na níž stojí statická typová kontrola, je snadno pochopitelná, protože se do značné míry podobá dalším analýzám kódu (které provádí překladač, lintery atd.). Celá myšlenka je založena na tom, že u každé proměnné deklarované v programu, u každého parametru funkce a taktéž u každého návratového parametru funkce se přímo či nepřímo uvede datový typ (nepřímo v případě, že jazyk umí typ odvodit z použité hodnoty – jedná se o takzvanou typovou inferenci). Díky tomu, že je specifikace typu proměnné/parametru/návratové hodnoty dostupná přímo ve formě zdrojového kódu, může být typová kontrola skutečně statická – nevyžaduje tedy, aby se program spustil. To má své nesporné výhody, protože takto specifikované informace o typech dokáží zpracovat i moderní (a nejenom moderní) integrovaná vývojová prostředí, která ji mohou použít v kontextové nápovědě atd.
Ovšem současně zde narážíme na značnou nevýhodu: je velmi složité vytvořit snadno použitelný a současně i staticky typovaný programovací jazyk. A další nevýhodou je, že zápis datových typů je vyžadován i v případě, že se tvoří jednoduché skripty nebo prototypy. Proto není divu, že mnoho jazyků (a nutno říci, že mnohdy velmi úspěšných jazyků) striktní zápis datových typů nevyžaduje a tím pádem nebude (zcela) dostupná statická typová kontrola.
7. Product types v Pythonu
Součinové typy lze v Pythonu realizovat několika možnými způsoby. Typicky se používají n-tice nebo datové třídy (dataclass).
Začneme n-ticemi. U nich je možné explicitně určit typ všech jejich prvků nebo typ jednotlivých prvků (potom se vlastně jedná o zjednodušené záznamy). Ostatně si to vyzkoušejme na několika příkladech.
from typing import Tuple p: Tuple[int] = (1, 2, 3)
Tento zápis není korektní, protože specifikuje, že n-tice může obsahovat jediný prvek typu int a nikoli trojici prvků:
tuple_type1.py:3: error: Incompatible types in assignment (expression has type "Tuple[int, int, int]", variable has type "Tuple[int]") [assignment] Found 1 error in 1 file (checked 1 source file)
Pokud skutečně budeme chtít vytvořit n-tici se třemi prvky typu int (například pro reprezentaci barvy atd.), můžeme použít tento zápis:
from typing import Tuple p: Tuple[int, int, int] = (1, 2, 3)
Nic nám však nebrání v tom určit, že každý prvek n-tice má být odlišného datového typu, což je ukázáno na dalším příkladu:
from typing import Tuple p: Tuple[int, float, bool, str] = (1, 3.14, True, "Hello")
Výsledek statické typové kontroly:
Success: no issues found in 1 source file
A naopak si můžeme ukázat, kdy statická typová kontrola nalezne problematický kód:
from typing import Tuple p: Tuple[int, float, bool, str] = (2.0, 3.14, 1, "Hello")
main.py:3: error: Incompatible types in assignment (expression has type "Tuple[float, float, int, str]", variable has type "Tuple[int, float, bool, str]") [assignment] Found 1 error in 1 file (checked 1 source file)
Alternativně lze použít datové třídy, u nichž se aplikací dekorátoru dataclass automaticky vygeneruje jak konstruktor, tak i metoda __repr__. A i v tomto případě můžeme provádět statické typové kontroly:
from dataclasses import dataclass @dataclass class Rectangle: width: float height: float rectangle_shape = Rectangle(height=2, width=3)
Nekorektní volání konstruktoru s hodnotou špatného typu:
from dataclasses import dataclass @dataclass class Rectangle: width: float height: float rectangle_shape = Rectangle(height="foo", width=3)
Výsledek statické typové kontroly:
/home/ptisnovs/src/most-popular-python-libs/algebraic_types/dataclass_2.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/dataclass_2.py:10:36 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "height" of type "float" in function "__init__" "Literal['foo']" is not assignable to "float" (reportArgumentType) 1 error, 0 warnings, 0 informations
Nekorektní volání konstruktoru s neuvedením hodnot atributů:
from dataclasses import dataclass @dataclass class Rectangle: width: float height: float rectangle_shape = Rectangle()
Výsledek typové kontroly:
/home/ptisnovs/src/most-popular-python-libs/algebraic_types/dataclass_3.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/dataclass_3.py:10:19 - error: Arguments missing for parameters "width", "height" (reportCallIssue) 1 error, 0 warnings, 0 informations
8. Sum types v Pythonu
Deklarace součtového typu může být snadná. Předpokládejme, že máme definovány tři typy nazvané Square, Rectangle a Circle (typicky se bude jednat o datové třídy – dataclass). Z nich můžeme odvodit nový součtový typ následujícím způsobem:
Shape = Square | Rectangle | Circle
popř. ve starších verzích Pythonu:
from typing import Union Shape = Union[Square, Rectangle, Circle]
I proměnná Shape je určitého typu, který můžeme zapsat explicitně:
from typing import TypeAlias Shape: TypeAlias = Square | Rectangle | Circle
Můžeme si taktéž nadeklarovat známý typ, Result založený na typových proměnných TOK a TERR:
TOK = TypeVar("TOK") TERR = TypeVar("TERR") Result = Ok[TOK] | Err[TERR]
9. Výpočet plochy geometrických tvarů: základní varianta
Ve druhé části dnešního článku si ukážeme postupnou transformaci zdrojového kódu založeného na běžných třídách do podoby, v níž se využijí algebraické datové typy.
V první verzi zdrojového kódu je realizována funkce nazvaná area, která dokáže vypočítat plochu zadaného geometrického tvaru. Podporovány jsou čtverce, obdélníky a kruhy, přičemž každý z těchto tvarů je implementován formou běžné třídy (bez společného předka). Povšimněte si, že se vlastně nejedná o klasické objektově orientované programování, protože to by vyžadovalo, aby každá z těchto tříd implementovala metodu area (ovšem dále uvidíme, proč problém realizujeme jiným způsobem; v každém případě je však „OOP-přístup“ stále legální a někdy i opodstatněný):
from math import pi class Square: def __init__(self, size): self.size = size class Rectangle: def __init__(self, width, height): self.width = width self.height = height class Circle: def __init__(self, radius): self.radius = radius def area(shape): """Výpočet plochy geometrického tvaru.""" if isinstance(shape, Square): return shape.size**2 elif isinstance(shape, Rectangle): return shape.width * shape.height elif isinstance(shape, Circle): return pi * shape.radius**2 square_shape = Square(10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(2, 3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units") area("xyzzy")
V tomto zdrojovém kódu nalezneme minimálně tři problémy: neřeší se problematika předání nepodporované hodnoty do funkce area, zcela chybí typové anotace (type hints) a navíc je detekce typu předané hodnoty vyloženě škaredá – takové „špagety“ většinou značí špatný návrh.
První problém otestujeme spuštěním skriptu:
Area of square: 100 units Area of rectangle: 6 units Area of circle: 314.1592653589793 units *** zde chybí jakákoli zmínka o chybném předání řetězce "xyzzy" ***
Druhý problém odhalí nástroj Mypy:
$ mypy --strict iteration_01.py
Vypíše se informace o všech místech v kódu, ve kterých chybí typové anotace, které by vylepšily kontrolu chyb:
iteration_01.py:5: error: Function is missing a type annotation [no-untyped-def] iteration_01.py:10: error: Function is missing a type annotation [no-untyped-def] iteration_01.py:16: error: Function is missing a type annotation [no-untyped-def] iteration_01.py:20: error: Function is missing a type annotation [no-untyped-def] iteration_01.py:30: error: Call to untyped function "Square" in typed context [no-untyped-call] iteration_01.py:31: error: Call to untyped function "area" in typed context [no-untyped-call] iteration_01.py:34: error: Call to untyped function "Rectangle" in typed context [no-untyped-call] iteration_01.py:35: error: Call to untyped function "area" in typed context [no-untyped-call] iteration_01.py:38: error: Call to untyped function "Circle" in typed context [no-untyped-call] iteration_01.py:39: error: Call to untyped function "area" in typed context [no-untyped-call] iteration_01.py:42: error: Call to untyped function "area" in typed context [no-untyped-call] Found 11 errors in 1 file (checked 1 source file)
10. Detekce hodnoty nekorektního typu
Detekci předání hodnoty nekorektního typu do funkce area pochopitelně můžeme do programového kódu snadno přidat, i když je nutné upozornit na to, že je zdrojový kód stále špatně čitelný i špatně rozšiřitelný:
from math import pi class Square: def __init__(self, size): self.size = size class Rectangle: def __init__(self, width, height): self.width = width self.height = height class Circle: def __init__(self, radius): self.radius = radius def area(shape): """Výpočet plochy geometrického tvaru.""" if isinstance(shape, Square): return shape.size**2 elif isinstance(shape, Rectangle): return shape.width * shape.height elif isinstance(shape, Circle): return pi * shape.radius**2 else: raise TypeError(f"Unknown type '{type(shape).__name__}'") square_shape = Square(10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(2, 3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units") area("xyzzy")
Nyní po spuštění takto upraveného příkladu dojde ke korektnímu rozpoznání, že se funkci area snažíme předávat hodnotu typu, který není podporován. Vypíše se přesná informace o nepodporovaném typu:
Area of square: 100 units Area of rectangle: 6 units Area of circle: 314.1592653589793 units Traceback (most recent call last): File "/home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_02.py", line 44, in <module> area("xyzzy") File "/home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_02.py", line 29, in area raise TypeError(f"Unknown type '{type(shape).__name__}'") TypeError: Unknown type 'str'
11. Využití strukturálního pattern matchingu
V relativně nedávno vydaném Pythonu verze 3.10 se objevila dlouho očekávaná novinka – takzvaný strukturální pattern matching. Jedná se o v jazyku Python zcela novou programovou konstrukci, která velmi vzdáleně připomíná konstrukci switch-case z programovacího jazyka C (odkud byla převzata do dalších programovacích jazyků, včetně C++ či Javy). Ovšem strukturální pattern matching ve skutečnosti programátorům nabízí mnohem více možností než původní značně primitivní konstrukce switch-case. Některé z těchto možností si ukážeme právě v našem demonstračním příkladu.
Možnosti této nové programové konstrukce zahrnují i možnost testu či testů, zda je hodnotou nějaký objekt (tedy atribut určité třídy), popř. jaké jsou hodnoty atributů tohoto objektu. A právě tyto vzorky použijeme v našem příkladu:
match shape: case Square(size=s): return s**2 case Rectangle(width=w, height=h): return w * h case Circle(radius=r): return pi * r**2
Povšimněte si, že s, w, h a r jsou proměnné, do kterých je zachycena konkrétní hodnota atributů size, width, height či radius.
Úplný zdrojový kód takto upraveného příkladu vypadá následovně:
from math import pi class Square: def __init__(self, size): self.size = size class Rectangle: def __init__(self, width, height): self.width = width self.height = height class Circle: def __init__(self, radius): self.radius = radius def area(shape): """Výpočet plochy geometrického tvaru.""" match shape: case Square(size=s): return s**2 case Rectangle(width=w, height=h): return w * h case Circle(radius=r): return pi * r**2 square_shape = Square(10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(2, 3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units") area("xyzzy")
Výsledky:
Area of square: 100 units Area of rectangle: 6 units Area of circle: 314.1592653589793 units
12. Detekce hodnoty nekorektního typu v konstrukci se strukturálním pattern matchingem
Programová konstrukce match-case umožňuje zapsat v jedné větvi (typicky ve větvi poslední) vzorek, který se zapisuje pouze formou podtržítka. Tento vzorek odpovídá jakémukoli vstupu, což vlastně znamená, že příslušná větev odpovídá (i když ne zcela přesně) větvi default známé z céčkovských jazyků nebo z Javy. To nám mj. umožňuje naprogramovat reakci na situaci, ve které se předává hodnota nekorektního typu, tj. taková hodnota, která není zachycena předchozími větvemi:
match shape: case Square(size=s): return s**2 case Rectangle(width=w, height=h): return w * h case Circle(radius=r): return pi * r**2 case _: raise TypeError(f"Unknown type '{type(shape).__name__}'")
Upravený zdrojový kód demonstračního příkladu nyní vypadá následovně:
from math import pi class Square: def __init__(self, size): self.size = size class Rectangle: def __init__(self, width, height): self.width = width self.height = height class Circle: def __init__(self, radius): self.radius = radius def area(shape): """Výpočet plochy geometrického tvaru.""" match shape: case Square(size=s): return s**2 case Rectangle(width=w, height=h): return w * h case Circle(radius=r): return pi * r**2 case _: raise TypeError(f"Unknown type '{type(shape).__name__}'") square_shape = Square(10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(2, 3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units") area("xyzzy")
Po spuštění skriptu je patrné, že při předání řetězce dojde k (očekávanému) vyhození výjimky:
Area of square: 100 units Area of rectangle: 6 units Area of circle: 314.1592653589793 units Traceback (most recent call last): File "/home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_04.py", line 45, in <module> area("xyzzy") File "/home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_04.py", line 30, in area raise TypeError(f"Unknown type '{type(shape).__name__}'") TypeError: Unknown type 'str'
13. Zachycení atributů do proměnných se stejným názvem, jaký mají původní atributy
V předchozí dvojici demonstračních příkladů jsme zachytávali atributy objektů typu Square, Rectangle a Circle do nových proměnných, které se jmenovaly odlišně, než samotné atributy. Například atribut size objektu typu Square byl zachycen do proměnné s atd.:
match shape: case Square(size=s): return s**2 case Rectangle(width=w, height=h): return w * h case Circle(radius=r): return pi * r**2 case _: raise TypeError(f"Unknown type '{type(shape).__name__}'")
To však ve skutečnosti není nutné, protože jména atributů leží v odlišném jmenném prostoru, než jména proměnných, do kterých je zachytávání prováděno. Tudíž nemusíme trávit čas vymýšlením nových jmen (což nedává smysl – sémanticky stejná hodnota má mít stejné jméno) a můžeme přímo psát:
match shape: case Square(size=size): return size**2 case Rectangle(width=width, height=height): return width * height case Circle(radius=radius): return pi * radius**2 case _: raise TypeError(f"Unknown type '{type(shape).__name__}'")
Upravený zdrojový kód skriptu bude vypadat následovně a bude se chovat stejně, jako skript původní:
from math import pi class Square: def __init__(self, size): self.size = size class Rectangle: def __init__(self, width, height): self.width = width self.height = height class Circle: def __init__(self, radius): self.radius = radius def area(shape): """Výpočet plochy geometrického tvaru.""" match shape: case Square(size=size): return size**2 case Rectangle(width=width, height=height): return width * height case Circle(radius=radius): return pi * radius**2 case _: raise TypeError(f"Unknown type '{type(shape).__name__}'") square_shape = Square(10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(2, 3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
14. Využití datových tříd
V dalším kroku využijeme velmi užitečnou vlastnost, která byla přidána do Pythonu verze 3.7. Jedná se o dekorátor nazvaný dataclass, který je aplikován na celou třídu. S využitím tohoto dekorátoru je možné do třídy automaticky vložit konstruktor __init__, který automaticky inicializuje všechny atributy na základě parametrů předaných konstruktoru. Navíc je vytvořena i metoda __repr__, což zjednodušuje tisk obsahu datových tříd.
Podívejme se na jednoduchou datovou třídu:
@dataclass class Rectangle: width: float height: float
Instance této třídy se vytvoří snadno:
rectangle_shape = Rectangle(height=2, width=3)
Navíc se zpřehlední i samotný zápis pattern matchingu – není zapotřebí explicitně specifikovat proměnné pro zachycení:
match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case Circle(radius): return pi * radius**2
Nová podoba našeho skriptu bude vypadat následovně:
from math import pi from dataclasses import dataclass @dataclass class Square: size: float @dataclass class Rectangle: width: float height: float @dataclass class Circle: radius: float def area(shape): """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case Circle(radius): return pi * radius**2 square_shape = Square(size=10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(height=2, width=3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(radius=10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
15. Přidání typových anotací
Pokusme se nyní ověřit, do jaké míry je skript z předchozí kapitoly korektní z hlediska statické typové kontroly:
$ mypy --strict iteration_06.py iteration_06.py:21: error: Function is missing a type annotation [no-untyped-def] iteration_06.py:33: error: Call to untyped function "area" in typed context [no-untyped-call] iteration_06.py:37: error: Call to untyped function "area" in typed context [no-untyped-call] iteration_06.py:41: error: Call to untyped function "area" in typed context [no-untyped-call] Found 4 errors in 1 file (checked 1 source file)
Z výsledků je patrné, že je nutné doplnit typové anotace, zejména do hlavičky funkce area. V prvním kroku je však nutné definovat nový součtový datový typ reprezentující všechny podporované geometrické tvary. Takový typ se definuje snadno:
Shape = Square | Rectangle | Circle
Následně již můžeme upravit hlavičku funkce area do této podoby:
def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" ... ... ...
Takto vypadá skript po výše popsaných úpravách:
from math import pi from dataclasses import dataclass @dataclass class Square: size: float @dataclass class Rectangle: width: float height: float @dataclass class Circle: radius: float Shape = Square | Rectangle | Circle def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case Circle(radius): return pi * radius**2 square_shape = Square(size=10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(height=2, width=3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(radius=10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
Nyní dopadne statická typová kontrola mnohem lépe:
$ mypy --strict iteration_07.py Success: no issues found in 1 source file
16. Chování při odstranění jedné větve z konstrukce match-case
Připomeňme si, že náš nový datový typ Shape reprezentuje tři podporované geometrické tvary, tedy konkrétně čtverec, obdélník a kruh:
Shape = Square | Rectangle | Circle
Vyzkoušejme si, co se stane v případě, že v konstrukci match-case „zapomeneme“ na větev odpovídající typu Circle:
def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height
V jazycích typu Rust nebo OCaml bychom byli na tento problém ihned upozorněni ještě před spuštěním, ovšem v Pythonu tomu tak není – interpret bez problémů příklad spustí a vypíše namísto obsahu kruhu nekorektní hodnotu:
Area of square: 100 units Area of rectangle: 6 units Area of circle: None units
Musíme tedy použít statickou typovou kontrolu. Nejprve s využitím nástroje Mypy:
$ mypy --strict iteration_08.py iteration_08.py:24: error: Missing return statement [return] Found 1 error in 1 file (checked 1 source file)
V tomto případě je sice problém popsán, ale nevíme, co přesně je špatně. Pokusme se tedy použít nástroj Pyright:
$ pyright iteration_08.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_08.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_08.py:24:27 - error: Function with declared return type "float" must return value on all code paths "None" is not assignable to "float" (reportReturnType) 1 error, 0 warnings, 0 informations
Chybové hlášení je opět neúplné.
Pro úplnost si uveďme celý příklad se „zapomenutou“ větví:
from math import pi from dataclasses import dataclass @dataclass class Square: size: float @dataclass class Rectangle: width: float height: float @dataclass class Circle: radius: float Shape = Square | Rectangle | Circle def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height square_shape = Square(size=10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(height=2, width=3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(radius=10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
17. Chování v případě, že je součtový datový typ neúplný
Chyb ovšem můžeme udělat více. Například můžeme zapomenout na přidání nového podporovaného geometrického tvaru do typu Shape. Příkladem je následující deklarace, ve které chybí typ Circle:
Shape = Square | Rectangle
S tímto typem ovšem budeme počítat ve funkci area (takže se jedná o opak předchozího příkladu):
def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case Circle(c): return c
Nástroj Pyright nás na tento problém upozorní a vypíše přesný druh problému, což je dobře:
$ pyright iteration_09.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_09.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_09.py:44:20 - error: Argument of type "Circle" cannot be assigned to parameter "shape" of type "Shape" in function "area" Type "Circle" is not assignable to type "Shape" "Circle" is not assignable to "Square" "Circle" is not assignable to "Rectangle" (reportArgumentType) 1 error, 0 warnings, 0 informations
Pro úplnost si opět uveďme celý zdrojový kód tohoto příkladu:
from math import pi from dataclasses import dataclass @dataclass class Square: size: float @dataclass class Rectangle: width: float height: float @dataclass class Circle: radius: float Shape = Square | Rectangle def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case Circle(c): return c square_shape = Square(size=10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(height=2, width=3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(radius=10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
18. Chování při použití zcela nepodporovaného typu při volání funkce area, využití assert_never
Předchozí dvojici demonstračních příkladů můžeme zkombinovat. Jak v definici typu Shape, tak i v konstrukci match-case zcela vynecháme typ Circle – program tedy bude připraven na výpočty plochy pro čtverce a obdélníky. Ovšem funkci area posléze zavoláme i s hodnotou typu Circle:
from dataclasses import dataclass @dataclass class Square: size: float @dataclass class Rectangle: width: float height: float @dataclass class Circle: radius: float Shape = Square | Rectangle def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height square_shape = Square(size=10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(height=2, width=3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(radius=10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
Zdrojový kód si necháme zkontrolovat nástrojem pyright, který je v tomto ohledu velmi striktní a přesně vypíše problém, který nastal:
$ pyright iteration_10.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_10.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_10.py:41:20 - error: Argument of type "Circle" cannot be assigned to parameter "shape" of type "Shape" in function "area" Type "Circle" is not assignable to type "Shape" "Circle" is not assignable to "Square" "Circle" is not assignable to "Rectangle" (reportArgumentType) 1 error, 0 warnings, 0 informations
V praxi se taktéž někdy setkáme s využitím funkce assert_never z balíčku typing:
assert_never(arg: Never, /) -> Never Statically assert that a line of code is unreachable. Example:: def int_or_str(arg: int | str) -> None: match arg: case int(): print("It's an int") case str(): print("It's a str") case _: assert_never(arg) If a type checker finds that a call to assert_never() is reachable, it will emit an error. At runtime, this throws an exception when called.
Explicitně tak do zdrojového kódu přidáme kontrolu pro nepodporovaný datový typ. Zápis by mohl vypadat následovně:
match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case _ as unreachable: assert_never(unreachable)
Výsledek statické typové kontroly se změní, ovšem stále je patrné, která část kódu je zapsaná nekorektně:
$ pyright iteration_11.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_11.py /home/ptisnovs/src/most-popular-python-libs/algebraic_types/iteration_11.py:32:26 - error: Argument of type "Circle" cannot be assigned to parameter "arg" of type "Never" in function "assert_never" Type "Circle" is not assignable to type "Never" (reportArgumentType) 1 error, 0 warnings, 0 informations
Opět si ukážeme celý zdrojový kód finální verze demonstračního příkladu:
from dataclasses import dataclass from typing import assert_never @dataclass class Square: size: float @dataclass class Rectangle: width: float height: float @dataclass class Circle: radius: float Shape = Square | Rectangle | Circle def area(shape: Shape) -> float: """Výpočet plochy geometrického tvaru.""" match shape: case Square(size): return size**2 case Rectangle(width, height): return width * height case _ as unreachable: assert_never(unreachable) square_shape = Square(size=10) square_area = area(square_shape) print(f"Area of square: {square_area} units") rectangle_shape = Rectangle(height=2, width=3) rectangle_area = area(rectangle_shape) print(f"Area of rectangle: {rectangle_area} units") circle_shape = Circle(radius=10) circle_area = area(circle_shape) print(f"Area of circle: {circle_area} units")
19. Repositář s demonstračními příklady
Všechny demonstrační příklady, které jsme si dnes ukázali, jsou uloženy v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady:
20. Odkazy na Internetu
- Build a Simple Result type in Python – and why you should use them
https://hamy.xyz/blog/2024–06_python-result-type - Pyright
https://github.com/microsoft/pyright - mypy
https://www.mypy-lang.org/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy/ - Python's Path to Embracing Algebraic Data Types
https://www.turingtaco.com/pythons-path-to-embracing-algebraic-data-types/ - Algebraic Data Types in (typed) Python
https://threeofwands.com/algebraic-data-types-in-python/ - Idiomatic algebraic data types in Python with dataclasses and Union
https://blog.ezyang.com/2020/10/idiomatic-algebraic-data-types-in-python-with-dataclasses-and-union/ - Exhaustiveness checking util
https://github.com/python/typing/issues/735 - OCaml playground
https://ocaml.org/play - Online Ocaml Compiler IDE
https://www.jdoodle.com/compile-ocaml-online/ - Get Started – OCaml
https://www.ocaml.org/docs - Get Up and Running With OCaml
https://www.ocaml.org/docs/up-and-running - Better OCaml (Online prostředí)
https://betterocaml.ml/?version=4.14.0 - OCaml file extensions
https://blog.waleedkhan.name/ocaml-file-extensions/ - First thoughts on Rust vs OCaml
https://blog.darklang.com/first-thoughts-on-rust-vs-ocaml/ - Standard ML of New Jersey
https://www.smlnj.org/ - Programming Languages: Standard ML – 1 (a navazující videa)
https://www.youtube.com/watch?v=2sqjUWGGzTo - 6 Excellent Free Books to Learn Standard ML
https://www.linuxlinks.com/excellent-free-books-learn-standard-ml/ - SOSML: The Online Interpreter for Standard ML
https://sosml.org/ - ML (Computer program language)
https://www.barnesandnoble.com/b/books/other-programming-languages/ml-computer-program-language/_/N-29Z8q8Zvy7 - Strong Typing
https://perl.plover.com/yak/typing/notes.html - What to know before debating type systems
http://blogs.perl.org/users/ovid/2010/08/what-to-know-before-debating-type-systems.html - Types, and Why You Should Care (Youtube)
https://www.youtube.com/watch?v=0arFPIQatCU - Language Workbenches: The Killer-App for Domain Specific Languages?
https://www.martinfowler.com/articles/languageWorkbench.html - Effective ML (Youtube)
https://www.youtube.com/watch?v=-J8YyfrSwTk - Why OCaml (Youtube)
https://www.youtube.com/watch?v=v1CmGbOGb2I - CSE 341: Functions and patterns
https://courses.cs.washington.edu/courses/cse341/04wi/lectures/03-ml-functions.html - Comparing Objective Caml and Standard ML
http://adam.chlipala.net/mlcomp/ - What are the key differences between Standard ML and OCaml?
https://www.quora.com/What-are-the-key-differences-between-Standard-ML-and-OCaml?share=1 - Cheat Sheets (pro OCaml)
https://www.ocaml.org/docs/cheat_sheets.html - Syllabus (FAS CS51)
https://cs51.io/college/syllabus/ - Abstraction and Design In Computation
http://book.cs51.io/ - Learn X in Y minutes Where X=Standard ML
https://learnxinyminutes.com/docs/standard-ml/ - CSE307 Online – Summer 2018: Principles of Programing Languages course
https://www3.cs.stonybrook.edu/~pfodor/courses/summer/cse307.html - CSE307 Principles of Programming Languages course: SML part 1
https://www.youtube.com/watch?v=p1n0_PsM6hw - CSE 307 – Principles of Programming Languages – SML
https://www3.cs.stonybrook.edu/~pfodor/courses/summer/CSE307/L01_SML.pdf - SML, Some Basic Examples
https://cs.fit.edu/~ryan/sml/intro.html - History of programming languages
https://devskiller.com/history-of-programming-languages/ - History of programming languages (Wikipedia)
https://en.wikipedia.org/wiki/History_of_programming_languages - Jemný úvod do rozsáhlého světa jazyků LISP a Scheme
https://www.root.cz/clanky/jemny-uvod-do-rozsahleho-sveta-jazyku-lisp-a-scheme/ - The Evolution Of Programming Languages
https://www.i-programmer.info/news/98-languages/8809-the-evolution-of-programming-languages.html - Evoluce programovacích jazyků
https://ccrma.stanford.edu/courses/250a-fall-2005/docs/ComputerLanguagesChart.png - Poly/ML Homepage
https://polyml.org/ - PolyConf 16: A brief history of F# / Rachel Reese
https://www.youtube.com/watch?v=cbDjpi727aY - F# – .NET Blog
https://devblogs.microsoft.com/dotnet/category/fsharp/ - Playground: OCaml
https://ocaml.org/play - The F# Survival Guide
https://web.archive.org/web/20110715231625/http://www.ctocorner.com/fsharp/book/default.aspx - Python to OCaml: Retrospective
http://roscidus.com/blog/blog/2014/06/06/python-to-ocaml-retrospective/ - Why Programmers Need Limits
https://cscalfani.medium.com/why-programmers-need-limits-3d96e1a0a6db - Signatures
https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/signature-files - F# for Linux People
https://carpenoctem.dev/blog/fsharp-for-linux-people/ - Infographic showing code complexity vs developer experience
https://twitter.com/rossipedia/status/1580639227313676288 - OCaml for the Masses: Why the next language you learn should be functional
https://queue.acm.org/detail.cfm?id=2038036 - Try EIO
https://patricoferris.github.io/try-eio/ - Try OCaml
https://try.ocaml.pro/ - ML – funkcionální jazyk s revolučním typovým systémem
https://www.root.cz/clanky/ml-funkcionalni-jazyk-s-revolucnim-typovym-systemem/ - Funkce a typový systém programovacího jazyka ML
https://www.root.cz/clanky/funkce-a-typovy-system-programovaciho-jazyka-ml/ - Curryfikace (currying), výjimky a vlastní operátory v jazyku ML
https://www.root.cz/clanky/curryfikace-currying-vyjimky-a-vlastni-operatory-v-jazyku-ml/ - Operátor J (Wikipedia)
https://en.wikipedia.org/wiki/J_operator - Standard ML (Wikipedia)
https://en.wikipedia.org/wiki/Standard_ML - Xavier Leroy
https://en.wikipedia.org/wiki/Xavier_Leroy - Unit type
https://en.wikipedia.org/wiki/Unit_type - The Option type
https://fsharpforfunandprofit.com/posts/the-option-type/