Skip to content

Instantly share code, notes, and snippets.

@piotrMocz
Last active March 15, 2018 11:09
Show Gist options
  • Save piotrMocz/69d80fe5ef44ddf648c475fe4dd48881 to your computer and use it in GitHub Desktop.
Save piotrMocz/69d80fe5ef44ddf648c475fe4dd48881 to your computer and use it in GitHub Desktop.
Zdrowe API
Display the source blob
Display the rendered blob
Raw
{
"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