dom - Konfiguracja Internetu
Jaka jest nazwa funkcji w programowaniu. Funkcje programowania

Cel pracy: 1) przestudiować zasady opisu funkcji; 2) nabyć umiejętności wykorzystania funkcji podczas pisania programów w języku C++.

Informacje teoretyczne

Głównym modułem programów w C++ jest funkcja.

Funkcjonować- logicznie wypełniony, zdecydowanie zaprojektowany fragment programu, który ma nazwę. Funkcje pozwalają dzielić duże zadania obliczeniowe na mniejsze.

Każdy program w C++ koniecznie zawiera funkcję zwaną main, która jest treścią programu. W przypadku wszystkich innych funkcji, jeśli są obecne w programie, należy zadeklarować prototypy - schematyczne zapisy, które informują kompilator o nazwie i formie każdej funkcji w programie.

Składnia prototypu funkcji z parametrami jest następująca:

typ_wartości zwracanej nazwa_funkcji (lista_parametrów ze wskazaniem_typu);

Funkcje w C++ są standardowe (bibliotekowe) i programowalne przez użytkownika.

Funkcje standardowe

Opisy standardowych funkcji znajdują się w plikach zawartych w programie za pomocą dyrektywy #include. Takie pliki nazywane są plikami nagłówkowymi; mają rozszerzenie h.

Odwołanie się do nazwy funkcji w programie głównym nazywa się wywołaniem funkcji.

Wywołanie funkcji skutkuje wykonaniem jakiejś akcji lub obliczeniem jakiejś wartości, która następnie zostaje wykorzystana w programie.

y = grzech(x); //funkcja obliczania sinusa

Definicja funkcji

Ogólnie funkcje definiuje się w następujący sposób:

typ_wartości zwracanej nazwa_funkcji (wpisz nazwa_parametru,..., wpisz nazwa_parametru)

funkcja_ciała

Programowalne funkcje

Funkcje, które programista tworzy samodzielnie, upraszczają proces pisania programów, ponieważ:

    pomagają uniknąć wielokrotnego programowania, ponieważ tej samej funkcji można używać w różnych programach;

    zwiększyć poziom modułowości programu, ułatwiając tym samym jego czytanie, wprowadzanie zmian i poprawianie błędów.

Przykład9 .1. Stwórzmy funkcję, która wypisuje 65 znaków „*” z rzędu. Aby ta funkcja działała w pewnym kontekście, została ona dołączona do programu drukowania na papierze firmowym. Program składa się z funkcji: main() i stars().

// Papier firmowy

#włączać

stała int Limit=65;

puste gwiazdy(pustka); // prototyp funkcji stars()

cout<<"Moscow Institute of Electronic Engineering"<

// Definicja funkcji stars().

dla (liczba=1; liczba<=Limit; count++)

Przyjrzeliśmy się przykładowi prostej funkcji, która nie ma argumentów i nie zwraca żadnych wartości.

Parametry funkcji

Spójrzmy na przykład użycia parametrów funkcji.

Przykład9. 2. Napiszmy funkcję space(), którego argumentem będzie liczba spacji, które powinna wyświetlić ta funkcja.

#zdefiniuj adres „Zelenograd”

#define name „Moskiewski Instytut Inżynierii Elektronicznej”

#define dział „Informatyka i Programowanie”

stała int LIMIT=65;

#włączać

pusta spacja (liczba int);

cout<

spacje=(LIMIT - strlen(nazwa))/2; // Oblicz, ile

// potrzebne są spacje

cout<

space((LIMIT - strlen(dział))/2); // argument - wyrażenie

cout<

//Definicja funkcji stars().

dla (liczba=1; liczba<=LIMIT; count++)

//Definicja funkcji space().

pusta spacja (liczba całkowita)

dla (liczba=1; liczba<=number; count++)

Zmienna liczbowa nazywana jest argumentem formalnym. Zmienna ta przyjmuje wartość aktualnego argumentu w momencie wywołania funkcji. Innymi słowy, argument formalny jest zmienną w definicji wywoływanego podprogramu, oraz faktyczny argument jest konkretną wartością przypisaną tej zmiennej przez program wywołujący.

Jeśli funkcja wymaga do komunikacji więcej niż jednego argumentu, możesz podać listę argumentów rozdzielonych przecinkami wraz z nazwą funkcji:

void printnum(int i, int j)

(czyt<<"Координаты точек”<< i << j <

Wartość wejściowa funkcji może zostać przetworzona dzięki obecności argument; Wartość wyjściowa jest zwracana za pomocą słowa kluczowego return.

Użytkownicy, którym w zasadzie daleko do programowania, rzadko spotykają się z pojęciami funkcji i procedur, a kojarzą się one z czymś matematycznym i biurokratyczno-medycznym. W programowaniu wiele języków operuje tymi pojęciami, jednak nawet eksperci czasami nie są w stanie jasno zrozumieć różnicy między funkcją a procedurą. Podobnie jak w przypadku tego susła: jest tam, ale nikt go nie widzi. Zobaczmy, czy różnice są aż tak niewidoczne.

Co oznaczają terminy funkcja i procedura?

  • Funkcjonować w programowaniu podprogram jest wywoływany z innych podprogramów wymaganą liczbę razy.
  • Procedura- nazwana część programu (podprogram), wielokrotnie wywoływana z kolejnych części programu wymaganą ilość razy.

Porównanie funkcji i procedury

Główną różnicą między funkcją a procedurą jest wynik, który zwraca. W rzeczywistości zarówno funkcje, jak i procedury są logicznie niepodzielnymi blokami tworzącymi kod programu. Funkcja zwraca wartość, procedura w większości języków programowania nie lub (na przykład w C) zwraca pustą wartość. W tym drugim przypadku (w C) procedurę uważa się za wersję podrzędną funkcji.

Nagłówek funkcji zawiera słowo „funkcja”, identyfikator (nazwę właściwą funkcji), opcjonalnie listę parametrów i koniecznie typ wyniku. Treść funkcji musi zawierać operator, który przypisuje nazwę funkcji wartość, którą w rezultacie zwróci. Nagłówek procedury zawiera słowo „procedura”, identyfikator (nazwę procedury) i opcjonalnie listę parametrów.

Wywołanie funkcji jest wykonywane jako część wyrażeń, gdzie te wyrażenia są używane; wywołanie procedury wymaga osobnej instrukcji.

Procedurę wywołuje się wyłącznie z nazwy, natomiast nazwa funkcji jest powiązana z jej wartością. Na diagramach algorytmów wywołanie funkcji jest przedstawione w bloku wyjściowym lub w bloku procesu, wywołanie procedury jest przedstawione w specjalnym bloku „predefiniowanego procesu”.

Różnica między funkcją a procedurą w programowaniu jest następująca:

  • Funkcja zwraca wartość, procedura nie.
  • Nagłówek funkcji musi zawierać typ wyniku.
  • Treść funkcji musi zawierać instrukcję przypisującą wartość do nazwy funkcji.
  • Wywołanie procedury wymaga osobnej instrukcji; wywołanie funkcji jest możliwe jako część wyrażenia.
  • Do wywołania potrzebna jest nazwa procedury, do przypisania wartości potrzebna jest nazwa funkcji.
  • Na diagramach algorytmów wywołanie procedury jest przedstawione w oddzielnym bloku, wywołanie funkcji w bloku procesu lub bloku wyjściowego.

Podstawą każdego programu komputerowego są algorytmy wyrażone w postaci poleceń. Osoba pisząca kod mówi: weź to, zrób z tym to, to i tamto, a następnie wypisz wynik tam i idź odpocząć. Aby więc polecenia w programach nie łączyły się w jeden bałagan i mogły ze sobą współdziałać, grupuje się je w tzw. funkcje i procedury. Zapoznamy się z tymi pojęciami.

Co to jest funkcja

Nazwy funkcji służą: 1) do tworzenia dokumentacji; 2) dla API, czyli interfejsu umożliwiającego połączenie z programem lub całym systemem operacyjnym dowolnej aplikacji. Warto zatem jeszcze raz przypomnieć, że nazwy te należy nadawać w sposób zrozumiały i w miarę możliwości adekwatny do wykonywanych czynności.

Podsumujmy

Funkcje są więc swego rodzaju pojemnikami do grupowania algorytmów. Oni:

  1. odpowiedzialny za określone zadania;
  2. wchodzić w interakcję z innymi obiektami;
  3. stanowią koncepcyjną podstawę współczesnego programowania, niezależnie od tego, jak żałośnie to brzmi.

Procedury to właściwie te same funkcje, choć „puste”, które nic nie zwracają (to jest ich główna różnica). Są to narzędzia pomocnicze przeznaczone do wykonywania rutynowych czynności, a także oszczędzające miejsce, wysiłek i czas.

Poprzednie publikacje:

Nie bez powodu nazwałem ten artykuł „Funkcjami jako integralna część programowania”, ponieważ bez nich, moim zdaniem, żaden język nie ma prawa istnieć. Co to jest? Funkcja jest głównym składnikiem dobrze napisanego programu. Nie tylko ułatwia czytanie kodu, ale także radykalnie zmienia ideę programowania strukturalnego. Za pomocą funkcji można ponownie wykorzystać poszczególne części programu przekazując im dowolne parametry. Nie można sobie wyobrazić żadnego poważnego programu bez tego cudu elementu programistycznego.

Opowiem pokrótce jak to działa. Funkcja to blok instrukcji, który program może wywołać. Kiedy uzyskany zostanie dostęp do nagłówka tego bloku (nazwy funkcji), zostanie on wykonany i wykona pewne akcje określone przez programistę. Następnie blok ten zwraca otrzymaną wartość i przekazuje ją do programu głównego. Wyjaśnię to w praktyce.

Z grubsza rzecz biorąc, wygląda to tak. Wyjaśnię krótko. Tworzymy jakąś zmienną i przypisujemy do niej wynik wykonania funkcji myfunc, która z kolei oblicza wartość podniesienia jakiejś liczby do kwadratu. Funkcje nie są wykonywane natychmiast po uruchomieniu programu, lecz dopiero po ich wywołaniu. Może to być trochę mylące, ale tak właśnie jest.

Jak wywołać funkcję?

Aby wywołać funkcję, należy ją utworzyć. Chociaż istnieją również wbudowane funkcje. Na przykład to: cos, grzech, md5, liczba, abs i tak dalej. Aby je wywołać, wystarczy przypisać żądaną wartość do zmiennej.

Argument funkcji to wartość przekazywana do niej podczas jej wywoływania. Argumenty funkcji umieszcza się w nawiasach. Tworząc funkcję, określasz warunkowe nazwy argumentów. Następnie nazwy te można wykorzystać w treści funkcji jako zmienne lokalne. Wróćmy do funkcji, które sam tworzy użytkownik. Można to zrobić bardzo łatwo. Najpierw tworzone jest ciało funkcji:

Funkcja hello() ( echo "Witaj, świecie!"; )

Potem do niej zadzwonimy. Co więcej, jeśli nie ma parametrów, po prostu umieszczamy nawiasy. Aby wywołać tę funkcję, używamy tylko linii: Witam();. Dowolna funkcja może również zwracać wartość za pomocą słowa zastrzeżonego powrót. Ta instrukcja zatrzymuje wykonywanie funkcji i wysyła wartość zwracaną do programu wywołującego. funkcja suma($pierwszy, $drugi) ($r=$pierwszy + $sekunda; zwróć $r;) echo sum(2,5); wynik wykonania programu będzie równy 7. Zmienne lokalne i globalne

Podobnie jak w każdym innym języku programowania, istnieją zmienne dostępne tylko w ramach funkcji oraz zmienne dostępne w kodzie samego programu. Takie zmienne nazywane są odpowiednio lokalnymi i globalnymi. Wewnątrz funkcji nie można po prostu uzyskać dostępu do zmiennej utworzonej poza funkcją. Jeśli spróbujesz to zrobić, utworzysz nową zmienną o tej samej nazwie, ale lokalną dla tej funkcji.

$per="Dima"; funkcja primer() // Wykonuje: wyświetlanie zmiennej lokalnej ( echo "Nazywam się ".$per; ) echo primer();

W takim przypadku na ekranie pojawi się fraza „Nazywam się”. Oznacza to, że zmienna $per została utworzona wewnątrz funkcji startera i domyślnie przypisano jej wartość zerową. Aby uniknąć takich ościeży, należy skorzystać z napędu światowy. Poprawmy odpowiednio powyższy kod:

$per="Dima"; funkcja primer() // Wykonuje: wyświetlanie zmiennej globalnej ( global $per; echo "Nazywam się ".$per; ) echo primer();

Wszystko powinno już być w porządku - problem rozwiązany. Tylko nie zapominaj, że jeśli funkcja zmieni wartość zmiennej zewnętrznej, to taka zmiana będzie miała wpływ na cały program, dlatego trzeba ostrożnie używać tego operatora!

Funkcje dwóch lub więcej argumentów

Niektóre argumenty przekazywane do funkcji mogą być opcjonalne, dzięki czemu funkcja będzie mniej wymagająca. Poniższy przykład wyraźnie to ilustruje:

… funkcja Font($text, $size=5) // Wykonaj: wyjściowy rozmiar czcionki ( echo " „.$tekst”."; ) czcionka("Witam
",1); czcionka("Witam
",2); czcionka("Witam
",3); czcionka("Witam
",4); czcionka("Witam
",5); czcionka("Witam
",6); czcionka("Witam
");

Domyślnie rozmiar czcionki wynosi 5. Jeśli pominiemy drugi parametr funkcji, będzie on równy tej wartości.

Wniosek

Zanim się pożegnam, chcę zwrócić uwagę na jedną radę. Polega na umieszczeniu wszystkich zapisanych funkcji w jednym pliku (np.function.php). A potem w pliku, w którym chcesz wywołać funkcję, wystarczy załączyć plikfunction.php i wszystko będzie gotowe do użycia. Dzięki temu znacznie łatwiej będzie zrozumieć logikę programu. Aby się połączyć użyj:

include_once("funkcja.php");

require_once("funkcja.php");

Jeśli rozumiesz istotę zagadnienia omawianego w tym artykule, to jestem pewien, że z łatwością możesz wykorzystać funkcje w swoich programach. Po raz kolejny ma to na celu uczynienie ich bardziej elastycznymi i nadającymi się do ponownego użycia.

To trzeci artykuł z serii „Teoria kategorii dla programistów”.

Kto potrzebuje typów?

W społeczności panuje pewna różnica zdań co do korzyści płynących z pisania statycznego i dynamicznego oraz silnego i słabego pisania. Pozwólcie, że zilustruję wybór sposobu pisania za pomocą eksperymentu myślowego. Wyobraź sobie miliony małp z klawiaturami, radośnie naciskających losowe klawisze, piszących, kompilujących i uruchamiających programy.

W języku maszynowym dowolna kombinacja bajtów wytworzonych przez małpy zostanie zaakceptowana i wykonana. Jednak w językach wysokiego poziomu bardzo ceniona jest zdolność kompilatora do wykrywania błędów leksykalnych i gramatycznych. Wiele programów zostanie po prostu odrzuconych, a małpy zostaną bez bananów, ale reszta będzie miała większe szanse na sensowność. Sprawdzanie typu stanowi kolejną barierę przeciwko bezsensownym programom. Dodatkowo, podczas gdy w językach z typem dynamicznym niezgodności typów będą wykrywane tylko w czasie wykonywania, w językach z silnym typem i sprawdzaniem statycznym niezgodności typów są wykrywane w czasie kompilacji, co eliminuje wiele błędnych programów, zanim będą miały szansę zostać wykonane.

Pytanie więc brzmi: czy chcemy, aby małpy były szczęśliwe, czy też stworzyliśmy odpowiednie programy?
(Nota tłumacza: nie ma się co obrażać, autor po prostu lubi mniej nudne metafory niż RNG i „losowe sekwencje bajtów” i nie nazywa programistów małpami).

Zazwyczaj celem eksperymentu myślowego piszącej na klawiaturze jest stworzenie kompletnych dzieł Szekspira (przypis tłumacza: czyli Wojna i pokój Tołstoja). Sprawdzanie pisowni i gramatyki w pętli znacznie zwiększa Twoje szanse na sukces. Analogia sprawdzania typu idzie jeszcze dalej: kiedy Romeo zostanie uznany za człowieka, sprawdzanie typu zagwarantuje, że nie wyrosną mu liście i nie wyłapie fotonów za pomocą swojego potężnego pola grawitacyjnego.

Typy są potrzebne do komponowania

Teoria kategorii bada składy strzał. Nie można połączyć dowolnych dwóch strzałek: obiekt docelowy jednej strzałki musi odpowiadać obiektowi źródłowemu następnej. W programowaniu przekazujemy wyniki z jednej funkcji do drugiej. Program nie będzie działać, jeśli druga funkcja nie będzie w stanie poprawnie zinterpretować danych uzyskanych przez pierwszą. Obie funkcje muszą do siebie pasować, aby ich kompozycja działała. Im silniejszy system typów języka, tym lepiej można opisać i automatycznie zweryfikować to dopasowanie.

Jedynym poważnym argumentem, jaki słyszę przeciwko silnemu typowaniu statycznemu, jest to, że może ono odrzucić niektóre programy, które są poprawne semantycznie. W praktyce zdarza się to niezwykle rzadko (uwaga tłumacza: żeby nie było nieporozumień, zaznaczam, że autor nie wziął pod uwagę, lub się z tym nie zgadza, że ​​stylów jest wiele, a kaczy typ, znany programistom w językach skryptowych, też ma prawo do życia Z drugiej strony typowanie kaczkowe jest możliwe w systemie ścisłych typów poprzez szablony, cechy, klasy typów, interfejsy, technologii jest wiele, więc opinii autora nie można uznać za całkowicie błędną.) w każdym razie każdy język zawiera swego rodzaju backdoora, który pozwala ominąć system typów, gdy jest to naprawdę konieczne. Nawet Haskell ma unsafeCoerce. Ale takich projektów należy używać mądrze. Gregor Samsa, postać grana przez Franza Kafkę, łamie system typów, zamieniając się w gigantycznego chrząszcza i wszyscy wiemy, jak to się skończy (uwaga tłumacza: źle:).

Innym argumentem, który często słyszę, jest to, że mocne pisanie stanowi zbyt duże obciążenie dla programisty. Mogę współczuć temu problemowi, ponieważ sam napisałem kilka deklaracji iteratorów w C++, z tą różnicą, że istnieje technologia wnioskowania o typie, która pozwala kompilatorowi wnioskować o większości typów z kontekstu, w którym są używane. W C++ możesz zadeklarować zmienną auto, a kompilator wywnioskować jej typ za Ciebie.

W Haskell, z wyjątkiem rzadkich przypadków, adnotacje typu są opcjonalne. Programiści i tak zwykle z nich korzystają, ponieważ typy mogą wiele powiedzieć o semantyce kodu, a deklaracje typów pomagają zrozumieć błędy kompilacji. Powszechną praktyką w Haskell jest rozpoczynanie projektu od opracowania typów. Później adnotacje typów stanowią podstawę implementacji i stają się komentarzami gwarantowanymi przez kompilator.

Silne pisanie statyczne jest często używane jako wymówka, aby nie testować kodu. Czasami usłyszysz, jak programiści Haskella mówią: „Jeśli kod się kompiluje, jest on poprawny”. Oczywiście nie ma gwarancji, że program o poprawnym typie jest poprawny w sensie generowania poprawnych wyników. W wyniku takiego podejścia w wielu badaniach Haskell nie wyprzedził znacząco innych języków pod względem jakości kodu, jak można by się spodziewać. Wydaje się, że w środowisku komercyjnym potrzeba naprawiania błędów istnieje tylko do pewnego poziomu jakości, który jest głównie związany z ekonomiką tworzenia oprogramowania i tolerancją użytkownika końcowego, a ma bardzo niewiele wspólnego z językiem programowania lub rozwojem metodologia. Lepszym miernikiem byłoby zmierzenie liczby projektów opóźnionych w stosunku do harmonogramu lub zrealizowanych ze znacznie ograniczoną funkcjonalnością.

Teraz, jeśli chodzi o twierdzenie, że testy jednostkowe mogą zastąpić silne pisanie. Przyjrzyjmy się powszechnej praktyce refaktoryzacji w językach silnie typowanych: zmianie typu argumentu na funkcję. W językach silnie typowanych wystarczy zmienić deklarację tej funkcji, a następnie poprawić ewentualne błędy kompilacji. W językach słabo typowanych fakt, że funkcja oczekuje teraz innych danych, nie może być przypisany wywołującemu.

Testowanie jednostkowe może wychwycić niektóre niespójności, ale testowanie jest prawie zawsze procesem probabilistycznym, a nie deterministycznym (Uwaga tłumacza: być może chodziło im o zestaw testów: nie uwzględnia się wszystkich możliwych danych wejściowych, ale pewną reprezentatywną próbę.) Testowanie jest kiepskim substytutem dowodu poprawności.

Co to są typy?

Najprostszy opis typów jest taki, że są to zbiory wartości. Typ Bool (pamiętaj, że konkretne typy zaczynają się w Haskell wielką literą) odpowiada zestawowi dwóch elementów: True i False. Typ Char to zbiór wszystkich znaków Unicode, takich jak „a” lub „ą”.

Zbiory mogą być skończone lub nieskończone. Typ String, który jest zasadniczo synonimem listy Char, jest przykładem nieskończonego zbioru.

Kiedy deklarujemy x jako liczbę całkowitą:
x::Liczba całkowita
mówimy, że jest to element zbioru liczb całkowitych. Liczba całkowita w Haskell jest nieskończonym zbiorem i może być używana do arytmetyki o dowolnej precyzji. Istnieje również skończony zbiór Int, który odpowiada typowi maszyny, jak int w C++.

Istnieją pewne subtelności, które utrudniają utożsamianie typów z zestawami. Istnieją problemy z funkcjami polimorficznymi, które mają definicje cykliczne, a także z faktem, że nie można mieć zbioru wszystkich zbiorów; ale jak obiecałem, nie będę ścisłym matematykiem. Ważne jest to, że istnieje kategoria zbiorów zwana Zestawem i będziemy z nią pracować.
W zestawie obiekty są zbiorami, a morfizmy (strzałki) są funkcjami.

Zestaw to szczególna kategoria, ponieważ możemy zajrzeć do wnętrza jego obiektów, co pomoże nam wiele zrozumieć intuicyjnie. Wiemy na przykład, że zbiór pusty nie ma elementów. Wiemy, że istnieją specjalne zbiory jednego elementu. Wiemy, że funkcje odwzorowują elementy jednego zbioru na elementy innego. Potrafią zmapować dwa elementy w jeden, ale nie jeden element na dwa. Wiemy, że funkcja tożsamości odwzorowuje każdy element zbioru na siebie i tak dalej. Planuję stopniowo zapomnieć o wszystkich tych informacjach i zamiast tego wyrazić wszystkie te pojęcia w formie czysto kategorycznej, to znaczy w kategoriach obiektów i strzałek.

W idealnym świecie moglibyśmy po prostu powiedzieć, że typy w Haskell to zbiory, a funkcje w Haskell to funkcje matematyczne pomiędzy. Jest tylko jeden mały problem: funkcja matematyczna nie wykonuje żadnego kodu — zna tylko odpowiedź. Funkcja w Haskell musi obliczyć odpowiedź. Nie stanowi to problemu, jeśli odpowiedź można uzyskać w skończonej liczbie kroków, niezależnie od ich wielkości. Istnieją jednak pewne obliczenia wymagające rekurencji, które mogą nigdy się nie zakończyć. Nie możemy po prostu zabronić funkcji niekończących się w Haskell, ponieważ rozróżnienie, czy funkcja kończy się, czy nie – słynny problem zatrzymania – jest nierozwiązywalne. Dlatego informatycy wpadli na genialny pomysł (lub brudny hack, w zależności od punktu widzenia), aby rozszerzyć każdy typ o specjalną wartość zwaną dołem (Nota tłumacza: to określenie (na dole) brzmi po rosyjsku trochę głupio, jeśli ktoś zna dobrą opcję, proszę o sugestię.), co jest oznaczone _|_ lub w Unicode ⊥. Ta „wartość” odpowiada niekompletnemu obliczeniu. Zatem funkcja zadeklarowana jako:
f::Bool -> Bool
może zwrócić True, False lub _|_; to drugie oznacza, że ​​funkcja nigdy się nie kończy.

Co ciekawe, gdy już zaakceptujesz wartość Bottom w systemie typów, wygodnie jest traktować każdy błąd wykonania jako wartość dolną, a nawet pozwolić funkcji na jawne zwrócenie wartości Bottom. To drugie jest zwykle wykonywane przy użyciu niezdefiniowanego wyrażenia:
f::Bool -> Bool f x = niezdefiniowany
Ta definicja przechodzi sprawdzanie typu, ponieważ undefiniowane zwraca wartość dolną, co jest uwzględnione we wszystkich typach, łącznie z Bool. Możesz nawet napisać:
f::Bool -> Bool f = niezdefiniowany
(bez x), ponieważ dół jest również członkiem typu Bool -> Bool.

Funkcje, które mogą zwrócić dół, nazywane są częściowymi, w przeciwieństwie do zwykłych funkcji, które zwracają prawidłowe wyniki dla wszystkich możliwych argumentów.

Ze względu na dół kategoria typów i funkcji Haskell nazywa się Hask, a nie Set. Z teoretycznego punktu widzenia jest to źródło niekończących się komplikacji, więc w tym momencie użyję mojego noża rzeźniczego i skończę z tym. Z pragmatycznego punktu widzenia można zignorować funkcje niekończące się i dno i potraktować Haska jako pełnoprawny zbiór.

Dlaczego potrzebujemy modelu matematycznego?

Jako programista znasz składnię i gramatykę języka programowania. Te aspekty języka są zwykle formalnie opisane na samym początku specyfikacji języka. Ale znaczenie i semantykę języka są znacznie trudniejsze do opisania; opis ten zajmuje znacznie więcej stron, rzadko jest wystarczająco formalny i prawie nigdy nie jest kompletny. Stąd niekończące się dyskusje wśród prawników językowych i całego chałupniczego przemysłu książek poświęconych interpretacji zawiłości standardów językowych.

Istnieją formalne sposoby opisu semantyki języka, ale ze względu na ich złożoność stosuje się je głównie w przypadku uproszczonych języków akademickich, a nie prawdziwych gigantów programowania przemysłowego. Jedno z tych narzędzi nazywa się semantyką operacyjną i opisuje mechanikę wykonywania programu. Definiuje sformalizowanego, wyidealizowanego interpretatora. Semantykę języków przemysłowych, takich jak C++, opisuje się zazwyczaj za pomocą nieformalnego rozumowania, często w kategoriach „abstrakcyjnej maszyny”.

Problem polega na tym, że bardzo trudno jest udowodnić cokolwiek w przypadku programów korzystających z semantyki operacyjnej. Aby pokazać właściwość programu, zasadniczo trzeba go „przepuścić” przez wyidealizowany interpreter.

Nie ma znaczenia, że ​​programiści nigdy formalnie nie udowadniają poprawności. Zawsze „myślimy”, że piszemy właściwe programy. Nikt nie siedzi przy klawiaturze i nie mówi: „Och, napiszę tylko kilka linijek kodu i zobaczę, co się stanie”. (nota tłumacza: och, gdyby tylko...) Wierzymy, że napisany przez nas kod wykona określone działania, które przyniosą oczekiwane rezultaty. Zwykle jesteśmy bardzo zaskoczeni, jeśli tak nie jest. Oznacza to, że tak naprawdę myślimy o programach, które piszemy i zazwyczaj robimy to poprzez uruchomienie interpretera w naszych głowach. Po prostu bardzo trudno jest śledzić wszystkie zmienne. Komputery są dobre w wykonywaniu programów, ludzie nie! Gdyby tak było, nie potrzebowalibyśmy komputerów.

Ale istnieje alternatywa. Nazywa się to semantyką denotacyjną i opiera się na matematyce. W semantyce denotacyjnej dla każdej konstrukcji językowej opisana jest interpretacja matematyczna. Zatem jeśli chcesz udowodnić właściwość programu, wystarczy, że udowodnisz twierdzenie matematyczne. Myślisz, że dowodzenie twierdzeń jest trudne, ale tak naprawdę my, ludzie, tworzymy metody matematyczne od tysięcy lat, więc istnieje wiele zgromadzonej wiedzy, którą można wykorzystać. Ponadto w porównaniu z twierdzeniami dowodzonymi przez zawodowych matematyków, problemy, które napotykamy w programowaniu, wydają się być dość proste, jeśli nie trywialne. (Nota tłumacza: dla dowodu autor nie stara się urazić programistów.)

Rozważmy definicję funkcji silni w Haskell, języku, który łatwo nadaje się do semantyki denotacyjnej:
fakt n = produkt
Wyrażenie to lista liczb całkowitych od 1 do n. Funkcja iloczynu mnoży wszystkie elementy listy. Dokładnie tak, jak definicja silni zaczerpnięta z podręcznika. Porównaj to z C:
int fakt(int n) ( int i; int wynik = 1; for (i = 2; i<= n; ++i) result *= i; return result; }
Czy powinienem kontynuować? (notka tłumacza: autor trochę oszukał, biorąc funkcję biblioteczną w Haskell. Tak naprawdę nie było potrzeby oszukiwać; uczciwy opis z definicji nie jest trudniejszy):
fakt 0 = 1 fakt n = n * fakt (n - 1)
No dobrze, od razu przyznam, że to był tani strzał! Silnia ma oczywistą definicję matematyczną. Wnikliwy czytelnik może zapytać: Jaki jest model matematyczny odczytywania znaku z klawiatury lub wysyłania pakietu przez sieć? Przez długi czas byłoby to niewygodne pytanie, prowadzące do dość mylących wyjaśnień. Semantyka denotacyjna wydawała się nieodpowiednia dla znacznej liczby ważnych problemów, które były niezbędne do napisania użytecznych programów i które można było łatwo rozwiązać za pomocą semantyki operacyjnej. Przełom nastąpił w teorii kategorii. Eugenio Moggi odkrył, że efekty obliczeniowe można przekształcić w monady. Okazało się to ważną obserwacją, która nie tylko nadała nowe życie semantyce denotacyjnej i uczyniła programy czysto funkcjonalne wygodniejszymi, ale także dostarczyła nowych informacji na temat tradycyjnego programowania. O monadach porozmawiam później, kiedy opracujemy bardziej kategoryczne narzędzia.

Jedną z istotnych zalet posiadania matematycznego modelu programowania jest możliwość przeprowadzenia formalnego dowodu poprawności oprogramowania. Może to nie wydawać się tak ważne, gdy piszesz oprogramowanie konsumenckie, ale istnieją obszary programowania, w których koszty awarii mogą być ogromne lub w których zagrożone jest życie ludzkie. Ale nawet pisząc aplikacje internetowe dla systemu opieki zdrowotnej można docenić fakt, że funkcje i algorytmy ze standardowej biblioteki Haskell są dostarczane wraz z dowodami poprawności.

Funkcje czyste i brudne

To, co nazywamy funkcjami w C++ lub jakimkolwiek innym języku imperatywnym, nie jest tym samym, co matematycy nazywają funkcjami. Funkcja matematyczna to po prostu odwzorowanie wartości na wartości.

Funkcję matematyczną możemy zaimplementować w języku programowania: taka funkcja po podaniu wartości wejściowej obliczy wartość wyjściową. Funkcja podnosząca liczbę do kwadratu prawdopodobnie pomnoży wartość wejściową przez siebie. Zrobi to za każdym razem, gdy zostanie wywołane i gwarantuje, że zwróci ten sam wynik za każdym razem, gdy zostanie wywołane z tym samym argumentem. Kwadrat liczby nie zmienia się wraz z fazami księżyca.

Co więcej, obliczenie kwadratu liczby nie powinno skutkować podarowaniem psu smacznego przysmaku. „Funkcji”, która to robi, nie można łatwo modelować za pomocą funkcji matematycznej.

W językach programowania funkcje, które zawsze dają ten sam wynik na tych samych argumentach i nie mają skutków ubocznych, nazywane są czystymi. W czystym języku funkcjonalnym, takim jak Haskell, wszystkie funkcje są czyste. Ułatwia to określenie semantyki denotacyjnej tych języków i modelowanie ich za pomocą teorii kategorii. W przypadku innych języków zawsze możesz ograniczyć się do czystego podzbioru lub osobno pomyśleć o skutkach ubocznych. Później zobaczymy, jak monady pozwalają nam modelować wszelkiego rodzaju efekty przy użyciu wyłącznie czystych funkcji. W rezultacie nic nie tracimy ograniczając się do funkcji matematycznych.

Przykłady typów

Kiedy już zdecydujesz, że typy są zbiorami, możesz wymyślić kilka całkiem egzotycznych przykładów. Na przykład, jaki typ odpowiada pustemu zbiorowi? Nie, w C++ nie jest to void, chociaż ten typ nazywa się Void w Haskell. Jest to typ, który nie jest wypełniony żadną wartością. Możesz zdefiniować funkcję, która przyjmuje Void, ale nigdy nie możesz jej wywołać. Aby to wywołać, musisz podać wartość typu Void, a jej po prostu nie ma. Jeśli chodzi o to, co ta funkcja może zwrócić, nie ma żadnych ograniczeń. Może zwrócić dowolny typ (choć nigdy tak się nie stanie, ponieważ nie można go wywołać). Innymi słowy, jest to funkcja polimorficzna pod względem typu zwracanego. Haskellerowie nazwali to:
absurd::Pustka -> a
(Uwaga tłumacza: w C++ nie można zdefiniować takiej funkcji: w C++ każdy typ ma co najmniej jedną wartość.)

(Pamiętaj, że a jest zmienną typu, która może być dowolnego typu.) Ta nazwa nie jest przypadkowa. Istnieje głębsza interpretacja typów i funkcji w kategoriach logiki zwana izomorfizmem Curry'ego-Howarda. Typ Pustki reprezentuje nieprawdziwość, a funkcja absurdalna reprezentuje twierdzenie, że coś wynika z fałszu, jak w łacińskim wyrażeniu „ex falso sequitur quodlibet”. (Nota tłumacza: wszystko wynika z fałszu.)

Następny jest typ odpowiadający zbiorowi singletonów. Jest to typ, który ma tylko jedną możliwą wartość. To znaczenie to po prostu „istnieje”. Być może nie rozpoznasz go od razu, ale w C++ jest on nieważny. Pomyśl o funkcjach od i do tego typu. Zawsze można wywołać funkcję void. Jeśli jest to czysta funkcja, zawsze zwróci ten sam wynik. Oto przykład takiej funkcji:
int f44() (zwróć 44; )
Można by pomyśleć, że ta funkcja akceptuje „nic”, ale jak właśnie widzieliśmy, funkcji akceptującej „nic” nie można wywołać, ponieważ nie ma wartości reprezentującej typ „nic”. Co zatem akceptuje ta funkcja? Koncepcyjnie przyjmuje wartość fikcyjną, która ma tylko jedną instancję, więc nie musimy jawnie określać jej w kodzie. Haskell ma jednak symbol określający to znaczenie: pustą parę nawiasów (). Zatem z powodu zabawnego zbiegu okoliczności (a może nie?) wywołanie funkcji z void wygląda tak samo w C++ i Haskell. Dodatkowo, ze względu na zamiłowanie Haskella do zwięzłości, ten sam symbol () jest używany dla typu, konstruktora i pojedynczej wartości odpowiadającej zbiorowi singletonów. Oto funkcja w Haskell:
f44::() -> Liczba całkowita f44() = 44
Pierwsza linia deklaruje, że f44 skonwertuje typ () zwany „unit” na typ Integer. Druga linia określa, że ​​f44 używa dopasowywania wzorców do konwersji jedynego konstruktora dla jednego, a mianowicie (), na liczbę 44. Funkcję tę wywołuje się podając wartość ():
f44()
Należy pamiętać, że każda funkcja jednego jest równoznaczna z wybraniem jednego elementu z typu docelowego (tutaj wybrana jest liczba całkowita 44). W rzeczywistości f44 można traktować jako kolejną reprezentację liczby 44. To jest przykład tego, jak możemy zastąpić bezpośrednie odniesienie do elementów zbioru funkcją (strzałką). Funkcje od jednego do pewnego typu A są w relacji jeden do jednego z elementami zbioru A.

A co z funkcjami, które zwracają wartość void lub, w Haskell, zwracają ją? W C++ takich funkcji używa się do efektów ubocznych, ale wiemy, że takie funkcje nie są funkcjami rzeczywistymi w matematycznym znaczeniu tego słowa. Czysta funkcja, która zwraca jeden, nic nie robi: odrzuca swój argument.

Matematycznie funkcja ze zbioru A do zbioru singletonowego odwzorowuje każdy element na pojedynczy element tego zbioru. Dla każdego A istnieje dokładnie jedna taka funkcja. Oto dla liczby całkowitej:
fInt::Integer -> () fInt x = ()
Podajesz dowolną liczbę całkowitą, a ona zwraca jeden. W duchu zwięzłości Haskell pozwala na użycie znaku podkreślenia jako argumentu, który jest odrzucany. Dzięki temu nie trzeba wymyślać dla niego nazwy. Powyższy kod można przepisać jako:
fInt::Integer -> () fInt_ = ()
Należy pamiętać, że wykonanie tej funkcji jest nie tylko niezależne od przekazanej jej wartości, ale także niezależne od rodzaju argumentu.

Funkcje, które można zdefiniować za pomocą tego samego wzoru dla dowolnego typu, nazywane są parametrycznie polimorficznymi. Można zaimplementować całą rodzinę takich funkcji za pomocą jednego równania, używając parametru zamiast konkretnego typu. Jak wywołać funkcję polimorficzną z dowolnego typu na jeden? Oczywiście nazwiemy to jednostką:
jednostka::a -> () jednostka _ = ()
W C++ zaimplementowałbyś to w ten sposób:
szablon pusta jednostka (T) ()
(uwaga tłumacza: aby pomóc kompilatorowi zoptymalizować go w noop, lepiej jest tak):
szablon pusta jednostka (T&&) ()
Następny w „typologii typów” jest zestaw dwóch elementów. W C++ nazywa się to bool, a w Haskell, co nie jest zaskakujące, Bool. Różnica polega na tym, że w C++ bool jest typem wbudowanym, natomiast w Haskell można go zdefiniować w następujący sposób:
dane Bool = True | FAŁSZ
(Definicję tę należy czytać w następujący sposób: Bool może mieć wartość True lub False.) W zasadzie możliwe byłoby opisanie tego typu w C++:
wyliczenie bool(prawda, fałsz);
Ale wyliczenie C++ jest w rzeczywistości liczbą całkowitą. Można by użyć „wyliczenia klas” C++ 11, ale wtedy należałoby zakwalifikować wartość nazwą klasy: bool::true lub bool::false, nie wspominając już o konieczności dołączenia odpowiedniego nagłówka do każdego pliku która go używa.

Funkcje Pure Bool po prostu wybierają dwie wartości z typu docelowego, jedną odpowiadającą True i jedną odpowiadającą False.

Funkcje w języku Bool nazywane są predykatami. Na przykład biblioteka Data.Char w Haskell zawiera wiele predykatów, takich jak IsAlpha lub isDigit. Podobna biblioteka istnieje w C++ , która deklaruje między innymi funkcje isalpha i isdigit, ale zwracają one wartość typu int, a nie wartość logiczną. Obecne predykaty są zdefiniowane w i nazywane są ctype::is(alfa, c) i ctype::is(cyfra, c).



 


Czytać:



Konstrukcja i zasada działania

Konstrukcja i zasada działania

Dysk optyczny to zbiorcza nazwa nośników danych wykonanych w postaci dysków, z których odczyt odbywa się za pomocą nośnika optycznego...

Utwórz portret z czcionki za pomocą Photoshopa Portrety z liter

Utwórz portret z czcionki za pomocą Photoshopa Portrety z liter

Możliwości przekształcenia fotografii w wizualne arcydzieła jest naprawdę wiele, a jedna z nich jest bardzo atrakcyjna – portret z tekstu...

Jak ponownie zainstalować program na komputerze. Jak ponownie zainstalować grę bez jej usuwania

Jak ponownie zainstalować program na komputerze. Jak ponownie zainstalować grę bez jej usuwania

Jeśli gra została pobrana z Internetu w postaci obrazu dysku (zazwyczaj pliki w formacie ISO i MDF), to do jej zainstalowania potrzebne będą...

Ormiańskie kanały satelitarne Ormiańskie kanały na Hotbird

Ormiańskie kanały satelitarne Ormiańskie kanały na Hotbird

Dziś abstrahujemy trochę od Tricolor, NTV Plus i telewizji płatnej w ogóle. Istnieje wiele satelitów, które wytwarzają...

obraz kanału RSS