Защо се нуждаем от всички тези паундкори и монади
Чисти данни
Ще се опитам да покажа това с доста изкуствен и вероятно безполезен пример, но акцентът ще бъде върху важността на споделянето и повторната употреба на код.
Терминът "чист" е претоварен в програмирането. Например, ние разбираме фразата „Ruby е чисто обектен език“ като „Ruby е език, в който всичко е обекти“. Но фразата „Haskell е чист функционален език“ трябва да се разбира като „Haskell е функционален език без странични ефекти“. В тази статия ще използваме термина „чист“ в още един контекст. "Чисти данни" са данните, които искам да извлека. По принцип примитивните типове са числа, низове, понякога по-сложни, например картина или няколко стойности. Съответно „мръсни данни“ са данни, които съдържат освен това, което искам, допълнителна информация.
Тук пишем програмата:
Програмата е проста за опозоряване - молим потребителя да въведе 2 реда и след това да покаже резултата от изчислението. Виждаме, че нашата функция foo все още не е дефинирана (тя винаги срива програмата), въпреки че Haskell вече може да компилира нашия код.
Сега нека пренапишем нашата функция по-подробно, като използваме само "чисти" данни:
Както можете да видите, тук също е ясно, ние изразяваме функцията foo чрез бар функцията и я приехме като нормално целочислено деление. Повечето функционални езици за програмиране улесняват създаването на функции въз основа на чисти данни.
Изглежда, че всичко е наред - проста и елегантна програма. Но netushki! Резултатът от функцията е много по-сложен, отколкото бихме искали. Както разбираме, е невъзможно да се раздели на 0 и потребителят може да въвежда не числа, а леви линии. Нашият кодсе оказа опасно. Императивният подход за решаване на такива проблеми е разделен на 2 групи: или използвайте разклонения, или използвайте изключения. Често двата подхода се комбинират. Тези подходи са толкова мощни, че се използват най-вече и във функционални езици. Казано направо, в Haskell има изключения, но те са недоразвити, имат нужда от реформиране и не се улавят по най-добрия начин. И най-важното - в повечето случаи те просто не са необходими. Но не по-малко - можете. Така че нека се опитаме да пренапишем нашия код, използвайки разклонения и изключения.
Мръсни данни
Haskell (и много функционални езици) имат приличен отговор на подобни проблеми. Основната сила се крие в алгебричните типове данни.
Ако погледнем горния пример, можем да видим, че нашите функции могат да паднат. Решението е да се използват nullable типове данни. В езиците ML и Scala този тип се нарича Option, в Haskell се нарича Maybe a.
Ние не обръщаме внимание на извличащата част, просто казваме, че искаме от компилатора независимо да може да преведе нашия тип данни в низ. А именно,
Типът данни е Nothing, ако нямаме данни, и Just a, ако имаме. Както можете да видите, типът данни е "мръсен", защото съдържа излишна информация. Нека пренапишем нашите функции по-правилно, по-безопасно и без изключения.
Първо, нека заменим функциите, причинили срива, с безопасни:
Сега, вместо да падат, тези функции дават резултата Nothing , ако всичко е наред, тогава резултатът Just.
Но останалата част от нашия код зависи от тези функции. Ще трябва да променим почти всички функции, включително тези, които са тествани многократно.
Както можете да видите, проста програмасе превърна в доста чудовищен код. Много обвиващи функции, много излишен код, много промени. Но това е мястото, където много функционални езици за програмиране спират. Сега можете да разберете защо в тези езици, въпреки възможността за създаване на много ADT, ADT не се използват много често в кода.
Може ли да се живее с ATD, но без такава оргия? Оказва се, че можете.
Функцорите ни идват на помощ в началото.
Функционерите са типове данни, за които има функция fmap
както и неговия инфикс синоним:
така че винаги да са изпълнени следните условия за всички стойности на типа данни:
Условие за идентичност: fmap > Условие на състава: fmap (f . g) == fmap f . fmap g
Където id е функцията за идентичност
И (.) - функционален състав
Функторът е тип клас, където създадохме специална fmap функция. Нека да разгледаме неговите аргументи - взема една "чиста" функция a -> b , вземаме "мръсната" функториална стойност f a и получаваме функториалната стойност f b на изхода.
Типът данни Maybe е функтор. Нека създадем екземпляр (инстанция) за типа Може би, така че законите на функторите да не се нарушават:
Как да използваме чиста функция с функтора Maybe? Много просто:
Виждаме основното тук - не пренаписахме нашата функция baz, което означава, че не е необходимо да я тестваме отново за грешки и всичко остава универсално и чисто, но лесно създадохме нейната безопасна версия, която приема nullable числа вместо числа.
Въпреки това, ако искаме да използваме функтор, докато се опитваме да пренапишем safeBaz, ще се провалим. Функторите работят само с функции с един „мръсен“ аргумент на функтора. Какво да направите за функции с множествопараметри?
Приложни функтори
Тук на помощ идват апликативните функтори:
Приложните функтори са функтори, за които са дефинирани 2 функции: чиста и ( )
Така че за тях, за всякакви стойности от един и същи тип данни, винаги са изпълнени следните правила:
Условие за идентичност: чист id v == v Условие за състав: чист (.) u v w == u (v w) Условие за хомоморфизъм: чист f чист x == чист (f x) Условие за обмен: u чист y == чист ($ y) u
Основната разлика между функтор и апликативен функтор е, че функторът плъзга чиста функция през стойност на функтор, докато апликативният функтор ни позволява да плъзгаме функториална функция f (a -> b) през стойност на функтор.
Maybe е приложен функтор и се дефинира така:
Време е да пренапишем safeBaz. По принцип функцията е пренаписана, комбинирайки функтора fmap за първия аргумент и апликативното низиране на останалите аргументи:
Но можете да пренапишете функцията, като използвате изключително апликативни функции (монаден стил) - първо правим „чистата“ функция чисто апликативна и апликативно нанизваме аргументите:
невероятно! Може би е възможно да пренапишете функцията a''' едновременно с помощта на апликативни функтори? уви
Нека да разгледаме сигнатурата на бар функцията: bar :: Int -> Int-> Maybe Int Функцията приема "чисти" аргументи като вход и произвежда "мръсен" резултат като изход. Така че в по-голямата си част в реалното програмиране най-често се срещат точно такива функции - те приемат „чисти“ аргументи като вход, а „мръсният“ резултат е изходът. И когато имаме няколко от тези функции, монадите ни помагат да ги обединим.
Монадите сатипове данни, за които съществуват функции return и (>>=).
така че правилата да се изпълняват за всякакви стойности от типа:
Лява идентичност: върне a >>= k == k a Дясна идентичност: m >>= return == m Асоциативност: m >>= (x -> k x >>= h) == (m >>= k) >>= ч
За удобство има допълнителна функция с обратен ред на аргументите:
Разбираме, че типът Maybe е монада, което означава, че можем да дефинираме неговия екземпляр (инстанция):
Между другото, ако разгледаме по-отблизо вътрешното съдържание и надписите, ще видим, че: pure == return fmap f xs == xs >>= return . f
Време е да пренапишем функция a''''
Е, не се оказа много по-добре. Това е така, защото монадите са красиво написани за една променлива. За щастие има много допълнителни функции. Можете да използвате функцията liftM2
Или използвайте функцията liftM2 и се присъединете
В краен случай можете да използвате синтактична захар за монади, като използвате нотацията do:
Разликата в използването на funtors и monads
Ако намалим основните функции до един тип, ще видим:
Всички се използват за предаване на "мръсни" стойности на функции, докато функциите очакват "чисти" стойности като вход. Funtors използват "чиста" функция. Приложните функтори са „чиста“ функция вътре в „замърсяване“. Монадите използват функции, които имат "мръсен" изход.
Програма без рутина
Е, най-накрая можете напълно и точно да пренапишете цялата програма:
Кодът отново стана прост и ясен! Не пожертвахме и сантиметър безопасност в процеса! Въпреки това не сме променили голяма част от кода! В същото време чистите функции остават чисти! В същото времеизбягайте от рутината!
Възможно ли е да живеем във функционален свят без функтори и монади? Мога. Но ако искаме да използваме пълната мощ на алгебричните типове данни, ще трябва да използваме функтори и монади за удобно функционално съставяне на различни функции. Защото това е чудесен изход от рутината и път към кратък, разбираем и често използван код!