Last active
March 15, 2018 11:09
-
-
Save piotrMocz/69d80fe5ef44ddf648c475fe4dd48881 to your computer and use it in GitHub Desktop.
Zdrowe API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Zdrowe API\n", | |
"\n", | |
"Kiedy pisze się programy/biblioteki, których ma używać ktoś poza nami, warto zadbać, żeby API, które wystawiamy, było przejrzyste i nie myliło użytkowników. Pozwoli to ograniczyć błędy i znacznie ułatwi współpracę.\n", | |
"\n", | |
"Sprowadza się to do paru prostych zasad." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Sygnatury funkcji\n", | |
"Przez \"sygnaturę funkcji\" rozumiemy po prostu jej nazwę i typ (czyli typ argumentów i typ wartości zwracanej). Uwaga: nawet w dynamicznie typowanych językach funkcja ma jakiś typ, mniej lub bardziej generalny. Przykładowo:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def identity(a):\n", | |
" return a" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Funkcja `identity` powyżej ma typ `a -> a`. Funkcja zwróci dokładnie to, co dostała (mimo, że Python o tym w zasadzie nie wie i myśli, że taka funkcja bierze \"cokolwiek 1\" i zwraca \"cokolwiek 2\", przy czym cokolwiek 1 i 2 niekoniecznie musiałyby być tym samym -- my wiemy lepiej, bo wiemy, że będzie to dokładnie to samo cokolwiek).\n", | |
"\n", | |
"Oczywiście możemy pisać inne funkcje i typ będzie bardziej szczegółowy:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def add(a, b):\n", | |
" return a + b" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Taka funkcja ma już bardziej skomplikowany typ, bo w grę wchodzi tzw. \"duck typing\". Jest to jednak dość intuicyjne: `(a: {+}, a: {+}) -> a`, co z grubsza mówi: \"funkcja bierze dwa argumenty będące czymkolwiek, co da się dodawać, a zwraca wynik tego dodawania\".\n", | |
"\n", | |
"Rozważmy jeszcze jedną funkcję:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def elemAt(someArray, i):\n", | |
" return someArray[i]" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Funkcja `elemAt` musi dostać coś, co da się indeksować (nazwijmy to `Indexable[Any]` -- kolekcja elementów dowolnego typu, które da się indeksować) oraz liczbę całkowitą (wiemy, że tablic nie da się indeksować niczym innym). W takim razie jej typ to `(Indexable[Any], int) -> Any`." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Sugestia 1\n", | |
"\n", | |
"**Funkcja powinna zwracać zawsze wartości tego samego typu.**\n", | |
"\n", | |
"Mimo, że napisanie takiej funkcji:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 6, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def f(x):\n", | |
" if x < 5:\n", | |
" return [1, 2, 3]\n", | |
" \n", | |
" return 123" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Jest możliwe (i nawet da się ją otypować: `f :: Numeric -> Union[List[Numeric], Numeric]`), to coś takiego jest **bardzo** złe i takich funkcji bardzo niewygodnie się używa (za każdym razem trzeba sprawdzać, co funkcja faktycznie zwróciła, a w większości języków nie ma niezawodnego mechanizmu, by to zrobić). " | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Sugestia 2\n", | |
"\n", | |
"**Listy powinny zawierać zawsze elementy tego samego typu**\n", | |
"\n", | |
"Z powodów takich, jak powyżej." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Sugestia 3\n", | |
"\n", | |
"**To, co funkcja robi, powinno się dać wywnioskować tylko z jej sygnatury.**\n", | |
"\n", | |
"* NIE MOŻNA zakładać, że użytkownicy będą mieli dostęp do źródeł funkcji,\n", | |
"* NIE WARTO zakładać, że użytkownicy będą czytali dokumentację (co nie oznacza, że nie należy jej pisać, należy).\n", | |
"\n", | |
"Przykładowo, funkcja `elemAt` biorąca coś tablicopodobnego i liczbę, powinna tylko zwrócić element w tej tablicy pod danym indeksem. Jeśli będzie robić coś dodatkowo (np. łączyć się z bazą danych, żeby sprawdzić pogodę), to coś jest nie tak." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Sugestia 4\n", | |
"\n", | |
"**Należy rozgraniczyć funkcje \"pure\" od \"impure\"**\n", | |
"\n", | |
"* Pure -- funkcja bierze argumenty i zwraca nowe wartości, wyliczone na podstawie argumentów. Nie zmienia ani argumentów, ani środowiska.\n", | |
"* Impure -- funkcja w jakiś sposób zmienia swoje otoczenie lub z niego korzysta.\n", | |
"\n", | |
"Prosty przykład sortowania:\n", | |
"* (!) jeśli piszemy funkcję `sort`, która bierze tablicę i zwraca tablicę, to użytkownik najprawdopodobniej założy, że funkcja nie zmienia swojego argumentu i zwraca nową, posortowaną tablicę.\n", | |
"* jeśli chcemy, by funkcja sortowała \"w miejscu\", nie powinna nic zwracać! Wtedy od razu wiadomo, że funkcja działa w miejscu. Vide: `sort` vs `sorted` w Pythonie." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 8, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Sorting result: None\n", | |
"The original array: [1, 2, 3, 4, 5]\n", | |
"Sorting result: [1, 2, 3, 4, 5]\n", | |
"The original array: [1, 4, 2, 3, 5]\n" | |
] | |
} | |
], | |
"source": [ | |
"xs = [1,4,3,2,5]\n", | |
"res = xs.sort()\n", | |
"print(\"Sorting result: \", res)\n", | |
"print(\"The original array: \", xs)\n", | |
"\n", | |
"ys = [1,4,2,3,5]\n", | |
"res2 = sorted(ys)\n", | |
"print(\"Sorting result: \", res2)\n", | |
"print(\"The original array: \", ys)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Przykłady funkcji \"pure\":\n", | |
"* funkcja, która bierze liczbę i zwraca wynik szeregu operacji arytmetycznych na tej liczbie\n", | |
"* funkcje typu `map`, `filter`\n", | |
"* funkcje, które biorą argument i sprawdzają, czy zachodzi dla niego szereg predykatów\n", | |
"\n", | |
"Przykłady funkcji \"impure\":\n", | |
"* operacje wejścia/wyjścia\n", | |
"* łączenie się z bazą danych\n", | |
"* requesty HTTP\n", | |
"* funkcje, które biorą tablicę jako argument i modyfikują ją w miejscu" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Sugestia 5\n", | |
"\n", | |
"**Należy preferować funkcje pure od impure.**\n", | |
"\n", | |
"Jeśli tylko jest możliwe ich zastosowanie, należy używać funkcji pure, z szeregu powodów:\n", | |
"* łatwiej pisać poprawne programy (rezultat funkcji zależy jedynie od ich argumentów, dodatkowo, dwukrotne wywołanie funkcji z tym samym argumentem da te same rezultaty)\n", | |
"* łatwiej testować programy (łatwo generować testowe dane, dużo trudniej jest mockować rezultaty zapytań HTTP, obiektów z bazy czy inputu użytkownika)\n", | |
"* kompilator może zastosować więcej optymalizacji (więcej wie o kodzie i może \"zauważyć\" ciekawe rzeczy)\n", | |
"* trywialnie je zrównoleglać\n", | |
"\n", | |
"Przykład, jak niezastosowanie się do Porad 4 i 5 utrudnia życie:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"# library code we don't have access to:\n", | |
"def bad_sort(arr):\n", | |
" arr.sort() # impure\n", | |
" for i in range(len(arr)):\n", | |
" arr[i] += 1 # wat?\n", | |
" return arr # but seems pure to the user\n", | |
"\n", | |
"\n", | |
"# our program code\n", | |
"from badlib import bad_sort\n", | |
"\n", | |
"def main():\n", | |
" xs = [1,3,4,2,5]\n", | |
" # sort, for faster lookups:\n", | |
" ys = bad_sort(xs)\n", | |
" \n", | |
" user_inp = int(input(\"Enter a number: \"))\n", | |
" ys.insert(0, user_inp) # add users number to the list\n", | |
" zs = bad_sort(ys) # sort again, just in case\n", | |
" \n", | |
" print(\"The user has entered: \", sum(zs) - sum(xs)) # not with the sort we're using he didn't...\n", | |
" print(\"Oh and originally we had: \", xs)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Przykład z sortowaniem jest mocno przerysowany, ale wcale nie odległy od rzeczywistości. Wbrew pozorom, nietrudno użyć powyższego mechanizmu, żeby w zupełnie realnych aplikacjach zaszyć błędy, które zostaną wykryte dopiero po wściekłych mailach użytkowników (typu: \"Dlaczego z mojej karty wzięto o 20 złotych za dużo??!!\")." | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Sugestia 6\n", | |
"\n", | |
"Uwaga: to wydaje się być wbrew intuicji, a jest bardzo przydatna. Co więcej, nie jest wymyślona przeze mnie: mówił o tym sam Uncle Bob Martin.\n", | |
"\n", | |
"Programiści mają tendencję do \"ukrywania\" niebezpiecznych operacji za warstwami abstrakcji, co wydaje się zupełnie zrozumiałe: chcemy jak najbardziej \"owinąć\" naszym kodem niebezpieczną logikę. Jest jednak wielki problem: cały nasz program jest zbudowany dookoła tej niebezpiecznej operacji i bardzo silnie z nią związany. Powoduje to również, że w zasadzie każda nasza funkcja jest impure, bo prędzej czy później woła (być może pośrednio) tą niebezpieczną funkcję. Co w takim razie robić?\n", | |
"\n", | |
"**Cała niebezpieczna logika powinna być tak wysoko, jak to możliwe**.\n", | |
"**Powinniśmy wykonać niebezpieczne operacje, a potem na nich robić transformacje, które są \"pure\"**\n", | |
"\n", | |
"Dzięki temu dostajemy kod, który łatwo testować w izolacji (testujemy, czy transformacje są dobre, podając im zmyślone dane) i mamy przewagę funkcji pure, które łatwo zrównoleglać i debugować.\n", | |
"\n", | |
"Przykład (pseudokod):" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def process_input(inp): ## pure function for processing input\n", | |
" # do something (process data)\n", | |
" # possibly calling other (pure) functions\n", | |
" return some_data\n", | |
"\n", | |
"def process_objects(objs, other_objs):\n", | |
" # a pure function, that gets all its data as arguments\n", | |
" # as opposed to reading them from the db on-the-fly\n", | |
" return some_data\n", | |
"\n", | |
"def main_func():\n", | |
" inp = risky_io_operation()\n", | |
" processed = process_input(inp)\n", | |
" objs = load_from_db()\n", | |
" processed2 = process_objects(objs, processed)\n", | |
" save_to_db(processed2)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"W ten sposób możemy w bardzo łatwy sposób testować logikę, podając funkcjom `process_input` i `process_objects` wygenerowane sztucznie dane. Testować operacje takie, jak `risky_io_operation` też oczywiście da się i należy, ale takie testy są dużo mniej deterministyczne (\"flaky\").\n", | |
"\n", | |
"Zły kod w powyższym przypadku \"ukryłby\" `risky_io_operation()` gdzieś w którejś z funkcji, np. `process_input`. Jeśli ta funkcja sama z siebie wczytywała dane, do testowania jej trzeba by użyć całej maszynerii związanej z mockowaniem, a tak naprawdę testowalibyśmy operacje IO, zamiast faktycznej logiki przetwarzającej dane." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.6.1" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment