Функции в Haskell. Първи въпроси

Мисля, че всички редовни читатели на моя блог знаят, че съм умерено пламенен последовател. Не че не искам да чувам нищо за всичко останало - не, просто винаги се застъпвам за възможности за избор, дискусии и постоянно търсенепо-добре и следователно, в крайна сметка, за определена еволюция на методологиите за развитие. Култът към личността ми е толкова неприятен, колкото и култът към отделна програмна методика, защото в дългосрочен план това е чист път към застой и лудост.

Продължавайки тази образователна работа, бих искал да се спра днес на прекрасното функционален езикпрограмиране. Вече три пъти ми изпратиха един и същ въпрос: откъде да започна (продължа) да уча Haskell?

Сигурно е време да дадем кратък отговор и съвет. Тук общ алгоритъмвлизане в тази тема от мен.

Етап 0 - Въведение. Haskell? Какво по дяволите?

Добре известен парадокс сред програмистите, които набират персонал, често наричан „“, и се формулира по следния начин:

Ако една компания избере някакъв малко използван езотеричен език за програмиране като свой основен език за програмиране, тогава такава компания има най-голям шанс да получи най-много най-добрите програмистиНа пазара. Защо? Факт е, че онези програмисти, за които научаването на нови неща не е проблем, ще искат да наемат първо такава „странна компания“; тези, за които малко познатото и трудно достъпното не е пречка; и накрая тези, които имат достатъчно високо самочувствие, за да се предложат при такива очевидно тежки условия.

И най-важното: има два вида програмисти: тези, които учат, за да получат Добра работаи винаги избират мейнстрийм, защото това значително увеличава шансовете им за намиране на работа; и тези, които просто обичат да научават нещо ново, да се развиват и винаги избират най-доброто, което често е далеч от най-доходоносните, както правят колегите им кариеристи. Така, Парадокс на Pythonтвърди, че започвайки разработка на авангардната екзотика, вие като прахосмукачка ще привлечете втора категория програмисти (обратното е и за фирмите, предлагащи работа).

Мога да дам като абстрактен пример напълно подобно тайно таргетиране на фокус групи с дадени свойства, история от близката му младост. Когато все още учех, имахме „странна“ капсула, която беше демонстративна, когато се представяше математически анализникога не е обръщал внимание правилната странапублика. Тоест в аудиторията имаше два реда - отляво и отдясно - и тук той изнася лекция, обяснява нещо, но в същото време НИКОГА не гледа десния ред - цялото му внимание е само върху студентите от левия ред . Също и с отговори на въпроси - дясната лента за него не съществуваше. Не чува нищо оттам.

В резултат на това с времето забелязах интересно нещо: в нашата група имаше автоматична самоидентификация и фрагментация на тези, които математически анализнеобходими и интересни (и те веднага заеха местата на левия ред, след като приеха правилата на тази странна игра), а тези, които нямаха нужда, и те побързаха да заемат местата вдясно, защото тук бяха оставени на техните собствени устройства. Това сортиране стана от само себе си, без допълнителни извънредни усилия отвън. За учителя, доколкото разбрах мотивацията му, това беше много удобно - той не хабеше енергията си за по същество непознати, а веднага създаваше условия да концентрира енергията и вниманието си върху тези, които се нуждаеха от неговите знания. За C студентите това също беше печеливша ситуация.

3. Етап - търсене на дълбочината и усещането за нов език

Третият етап вече се копае по-дълбоко. Тук предлагам малко да разчупим традиционните модели (и аз, о, колко обичам да правя това) и да премина към коренно различен формат за представяне на информация: това не само ще разнообрази задачата за изучаване на език, но и ще включва нови, неактивирани до момента зони на мозъка ви (ще натоварим специално вашия изтощен програмист ляв мозък). Имам предвид отлична видео лекция за Haskell от много умен човек с английски корени.

Ето неговия резултат:

Курсът по функционално програмиране с помощта на Haskell
(Английски език)
35 часа | 1280x720 | XviD - 1326Kbps
25,00 кадъра в секунда | Mp3 - 96Kbps | 20,06 GB

Можете да изтеглите тази голяма видео лекция. Съветвам ви да побързате, преди възмутените притежатели на авторските права да са дотичали и да унищожат този рядък архив на безценица ( актуализация: да, заковаха го : амин. Каня всички заинтересовани да използват тази мега-връзка).

4. Последният етап е практика

Намирането на най-подходящата и вдъхновяваща практическа задача е тема на отделна публикация, в която бих искал да дам някои фрапиращи примери от живота на известни загинали програмисти, така че нека засега оставим настрана моите обяснения по този въпрос финален етапнастрана за известно време.

И така, нещо като този, отчасти очевиден за мнозина алгоритъм, исках да дам на всеки, който иска да научи Haskell, и не само. Направи го!

И накрая, за феновете на други езици за програмиране:

Haskell е свещен език за програмиране, даден на шаманите Диамантена земятяхното върховно божество Комонадакак универсален лекза комуникация и духовно прочистване, подходящ както за божествени същества, така и за (някои) простосмъртни, които са страдали от тежки стадии на интелекта. Поради своя произход езикът винаги е бил функционално чист. Средно изучаването на Haskell започва на 10-12 годишна възраст. Ранното започване на вашето обучение ще гарантира, че ще достигнете третото ниво на Силата до 75-годишна възраст. Не трябва да отлагате за следващия живот това, което можете понезапочнете в този.

    Видове

    Програмите на Haskell са изрази, които, когато бъдат оценени, произвеждат стойности. Всяка стойност има тип. Интуитивно един тип може да се разбира просто като набор приемливи стойностиизрази. За да разберете типа на израз, можете да използвате командата на интерпретатора:type (или:t). Като алтернатива можете да подадете командата :set +t, за да накарате интерпретаторът автоматично да отпечата типа на всеки изчислен резултат.
    Основните видове Haskell са:
    Типовете Integer и Int се използват за представяне на цели числа, а стойностите на Integer не са ограничени по дължина.
    Видове плувкии Double се използва за представяне на реални числа.
    Типът Bool съдържа две стойности: True и False и е предназначен да представя резултата от булеви изрази.
    Типът Char се използва за представяне на знаци. Имената на типове в Haskell винаги започват с главна буква.
    Haskell е строго типизиран език за програмиране. В повечето случаи обаче от програмиста не се изисква да декларира какви типове са променливите, които въвежда. Самият интерпретатор е в състояние да изведе типовете променливи, използвани от потребителя.
    Въпреки това, ако за някаква цел е необходимо да се декларира, че определена стойност принадлежи към определен тип, се използва конструкция от формата: променлива:: Тип. Ако опцията за интерпретатор +t е активирана, тя отпечатва стойностите в същия формат.
    По-долу е даден пример за сесиен протокол за работа с интерпретатора. Приема се, че текстът след подканата Prelude> е въведен от потребителя, а текстът след това представлява отговорът на системата.

    Prelude>:set +t
    Прелюдия>1
    1::Цяло число
    Прелюдия>1.2
    1.2 :: Двойно
    Прелюдия>'а'
    'a' :: Char
    Прелюдия>Истина
    Вярно::Bool

    от от този протоколможем да заключим, че стойностите от тип Integer, Double и Char са посочени според същите правила като в езика C.
    Разширената система от типове и силното въвеждане правят програмите на Haskell безопасни за въвеждане. Гарантирано е, че в правилната програмаВ Haskell всички типове се използват правилно. На практика това означава, че програмата на Haskell не може да причини грешки при достъп до паметта, когато се изпълнява ( Нарушаване на достъпа). Също така е гарантирано, че програмата не може да използва неинициализирани променливи. По този начин много грешки в една програма се проследяват на етапа на нейното компилиране, а не при изпълнение.

    Аритметика

    Интерпретаторът Hugs може да се използва за изчисляване аритметични изрази. Операторите +, -, *, / (събиране, изваждане, умножение и деление) могат да се използват с обичайните правила за приоритет.
    Можете също да използвате оператора ^ (степенно степенуване). Така че една сесия може да изглежда така:

    Прелюдия>2*2
    4::Цяло число
    Прелюдия>4*5 + 1
    21::Цяло число
    Прелюдия>2^3
    8::Цяло число
    Освен това можете да използвате стандарт математически функции sqrt( Корен квадратен), sin, cos, exp и др. За разлика от много други езици за програмиране, Haskell не изисква да поставите аргумент в скоби, когато извиквате функция. Така че можете просто да напишете sqrt 2 вместо sqrt(2). Пример:

    Прелюдия>sqrt 2
    1.4142135623731::Двойно
    Прелюдия>1 + sqrt 2
    2.4142135623731::Двойно
    Прелюдия>sqrt 2 + 1
    2.4142135623731::Двойно
    Прелюдия>sqrt (2 + 1)
    1.73205080756888::Двойно

    от този примерможем да заключим, че извикването на функцията има по-висок приоритет от аритметични операциитака че изразът sqrt 2 + 1 се интерпретира като (sqrt 2) + 1, а не sqrt (2 + 1). За да посочите точния ред на изчисление, използвайте скоби, както в последния пример. (В действителност извикването на функция има по-висок приоритет от всеки двоичен оператор.)
    Трябва също така да се отбележи, че за разлика от повечето други езици за програмиране, Haskell оценява целочислени изрази до неограничен брой битове (Опитайте да оцените израза 2^5000.) За разлика от C, където максималната възможна стойност на тип int е ограничена от битовия капацитет на машината (на съвременните персонални компютрито е равно на 231-1 = 2147483647), типът Integer в Haskell може да съхранява цели числа с произволна дължина.

    Кортежи
    В допълнение към изброените по-горе прости типове, в Haskell можете да дефинирате стойности съставни типове. Например, за да посочите равнинна точка, са необходими две числа, съответстващи на нейните координати. В Haskell една двойка може да бъде определена чрез изброяване на компонентите, разделени със запетаи и поставянето им в скоби: (5,3). Не е задължително компонентите на една двойка да са от един и същи тип: можете да направите двойка, чийто първи елемент е низ, вторият е число и т.н.
    Като цяло, ако a и b са някои потребителски типовеВ Haskell типът двойка, в който първият елемент е от тип a, а вторият е от тип b, се обозначава като (a,b). Например, двойката (5,3) има тип (Integer, Integer); двойката (1, 'a') е от тип (Integer, Char). Могат да се цитират още сложен пример: pair((1,’a’),1.2) принадлежи към тип ((Integer,Char),Double). Проверете това с преводач. Трябва да се отбележи, че въпреки че конструкциите във формата (1,2) и (Integer,Integer) изглеждат сходни, в езика Haskell те обозначават напълно различни обекти. Първият е стойността, докато вторият е типът. За работа с двойки на езика Haskell има стандартни функции fst и snd, които връщат съответно първия и втория елемент на двойката (имената на тези функции идват от английски думи„първи“ (първи) и „втори“ (втори)). Така че те могат да се използват по този начин

    Prelude>fst (5, True)
    5::Цяло число
    Prelude>snd (5, True)
    Вярно::Bool
    Освен двойки, по подобен начин могат да се определят тройки, четворки и т.н. Типовете им се изписват по подобен начин.
    Прелюдия>(1,2,3)
    (1,2,3) :: (Цяло число,Цяло число,Цяло число)
    Прелюдия>(1,2,3,4)
    (1,2,3,4) :: (Цяло число,Цяло число,Цяло число,Цяло число)
    Тази структура от данни се нарича кортеж. Кортежът може да съхранява фиксирано количество разнородни данни. Функциите fst и snd са дефинирани само за двойки и не работят за други кортежи. Когато се опитате да ги използвате, например, за тризнаци, интерпретаторът показва съобщение за грешка. Елемент от кортеж може да бъде стойност от произволен тип, включително друг кортеж. За достъп до елементите на сдвоени кортежи може да се използва комбинация от функциите fst и snd. Следващият пример демонстрира извличане на елемент 'a' от кортеж
    (1, ('a', 23.12)):
    Prelude>fst (snd (1, ('a', 12/23)))
    'a' :: Цяло число

    Списъци
    За разлика от кортежите, списъкът може да съхранява произволен брой елементи. За да дефинирате списък в Haskell, трябва да квадратни скобиизбройте елементите му, разделени със запетаи. Всички тези елементи трябва да са от един и същи тип. Тип списък с елементи, принадлежащи към типа a, означен като [a].

    Прелюдия>
    ::
    Прелюдия>['1','2','3']
    [’1’,’2’,’3’] ::
    Списъкът може да не съдържа никакви елементи. Празен списък се означава като .
    Операторът: (двоеточие) се използва за добавяне на елемент в началото на списък. Левият му аргумент трябва да е елемент, а десният му трябва да е списък:
    Прелюдия>1:
    ::
    Прелюдия>'5':['1','2','3','4','5']
    [’5’,’1’,’2’,’3’,’4’,’5’] ::
    Прелюдия>Невярно:
    ::
    Използвайки оператора (:) и празен списък, можете да конструирате всеки списък:
    Прелюдия>1:(2:(3:))
    ::Цяло число
    Операторът (:) е десен асоциативен, така че можете да пропуснете скобите в горния израз:
    Прелюдия>1:2:3:
    ::Цяло число
    Елементите на списъка могат да бъдат всякакви стойности - числа, символи, кортежи, други списъци и др.
    Прелюдия>[(1,’a’),(2,’b’)]
    [(1,'a'),(2,'b')] :: [(Цяло число,Char)]
    Прелюдия>[,]
    [,] :: []
    За работа със списъци на езика Haskell има голям бройфункции. В това лабораторна работаНека разгледаме само някои от тях.
    Функцията head връща първия елемент от списъка.
    Функцията tail връща списък без първия елемент.
    Функцията за дължина връща дължината на списъка.
    Функциите head и tail са дефинирани за непразни списъци. Когато се опитвате да ги приложите към празен списъкпреводачът съобщава за грешка. Примери за работа с тези функции:
    Прелюдия> глава
    1::Цяло число
    Прелюдия>опашка
    ::
    Прелюдия>опашка
    ::Цяло число
    Прелюдия>дълж
    3::Int
    Обърнете внимание, че резултатът от функцията за дължина принадлежи на тип Int, а не тип Integer.
    За свързване (конкатенация) на списъци, Haskell дефинира оператора ++.
    Прелюдия>++
    ::Цяло число

    струни
    Стойностите на низовете в Haskell, както и в C, са посочени в двойни кавички. Те са от тип String.
    Prelude>"hello" "hello" :: String
    Низовете всъщност са списъци със знаци; Така изразите „здравей“, [’h’,’e’,’l’,’l’,’o’] и

    'h':'e':'l':'l':'o': означават същото нещо, но тип Stringе синоним на. Всички функции за работа със списъци могат да се използват при работа с низове:
    Прелюдия>глава "здравей"
    'h' :: Char
    Прелюдия>опашка "здравей"
    "Здравейте" ::
    Прелюдия>дължина "здравей"
    5::Int
    Прелюдия>"здравей" ++ ", свят"
    "Здравей свят" ::
    Превръщам числови стойностив низове и обратно има функции за четене и показване:
    Прелюдия> шоу 1
    "1" ::
    Прелюдия>"Формула" ++ шоу 1
    "Формула 1" ::
    Прелюдия>1 + прочетете "12"
    13::Цяло число
    Ако функцията show не може да преобразува низ в число, тя ще отчете грешка.

    Функции
    Досега използвахме вградените функции на Haskell. Сега е време да се научите как да определяте нативни функции. За да направим това, трябва да научим още няколко команди за интерпретатор (не забравяйте, че тези команди могат да бъдат съкратени до една буква):
    Командата:load ви позволява да заредите програмата Haskell, съдържаща се в посочения файл, в интерпретатора.
    Командата:edit стартира процеса на редактиране на последния зареден файл.
    Командата:reload препрочита последния зареден файл. Дефиниции потребителски функциитрябва да бъде във файл, който трябва да бъде зареден в интерпретатора на Hugs с помощта на командата:load.
    За да редактирате заредена програма, можете да използвате командата:edit. Той стартира външен редактор (Notepad по подразбиране), за да редактира файла. След приключване на сесията за редактиране редакторът трябва да бъде затворен; в този случай интерпретаторът на Hugs ще прочете отново съдържанието на променения файл. Файлът обаче може да се редактира и директно от Обвивка на Windows. В този случай, за да може интерпретаторът да прочете отново файла, трябва изрично да извикате командата: reload.
    Нека разгледаме един пример. Създайте файл lab1.hs в някоя директория. Позволявам пълен пъткъм този файл - c:\labs\lab1.hs (това е само пример, вашите файлове може да са именувани по различен начин). В интерпретатора на Hugs изпълнете следните команди:

    Prelude>:load "c:\\labs\\lab1.hs"
    Ако изтеглянето е успешно, подканата за интерпретатор се променя на Main>. Въпросът е, че ако името на модула не е посочено, се приема, че е равно на Main.
    Основен>: редактиране
    Тук трябва да се отвори прозорец на редактора, в който можете да въведете програмен текст. Въведете:
    x =
    Запазете файла и затворете редактора. Интерпретаторът на Hugs ще зареди файла
    c:\labs\lab1.hs и сега ще се определи стойността на променливата x:
    Главно>x
    ::
    Имайте предвид, че когато пишете името на файла като аргумент на командата:load, знаците \ се дублират. Освен това, както в езика C, в Haskell знакът \ служи като индикатор за началото на служебния знак ('\n' и т.н.) За да въведете символа \ директно, е необходимо, както в C, за да го екранирате с друг символ \ .
    Сега можем да преминем към дефиниране на функции. Създайте файл в съответствие с процеса, описан по-горе, и напишете следния текст в него:

    square::Integer -> Integer
    квадрат x = x * x

    Първият ред (square::Integer -> Integer) декларира, че дефинираме функция square, която приема параметър от тип Integer и връща резултат от тип Integer. Вторият ред (квадрат x = x * x) е действителната дефиниция на функцията. Квадратната функция приема един аргумент и връща неговия квадрат. Функциите в Haskell са стойности от "първи клас". Това означава, че те са "равни" на стойности като цели числа и реални числа, символи, низове, списъци и др. Функциите могат да се предават като аргументи на други функции, връщани от функции и т.н. Както всички стойности в Haskell, функциите имат тип. Типът функция, която приема стойности от тип a и връща стойности от тип b, се обозначава като a->b.
    Заредете създадения файл в интерпретатора и изпълнете следните команди:

    Основен>: тип квадрат
    square::Integer -> Integer
    Главен>квадрат 2
    4::Цяло число
    Имайте предвид, че по принцип декларирането на функционалния тип square не е необходимо: самият интерпретатор може да направи извод необходимата информацияза типа на функция от нейната дефиниция. Въпреки това, първо, изведеният тип ще бъде по-общ от Integer -> Integer, и второ, изричното уточняване на типа на функция е „добра форма“ при програмиране в Haskell, тъй като декларацията на типа служи като вид документация за функцията и помага за идентифициране на програмни грешки.
    Имената на дефинирани от потребителя функции и променливи трябва да започват с малка буква. Останалите знаци в името могат да бъдат главни или малки букви с латински букви, цифри или символи _ и ’ (долна черта и апостроф). Ето защо, следните са примери за правилни имена на променливи:

    вар
    var1
    variableName
    име_на_променлива
    вар'

    Условни изрази

    Можете да използвате условни изрази в дефиниция на функция в Haskell. Нека напишем функция signum, която изчислява знака на подаден й аргумент:

    signum::Integer -> Integer
    signum x = ако x > 0, тогава 1
    else if x else 0

    Условният израз се записва като:
    ако условие след това израз else израз. Моля, обърнете внимание, че въпреки че този израз прилича на съответния оператор в C или Pascal, условният израз на Haskell трябва да съдържа както then-част, така и else-част. Изрази в частта then и в частта else условен оператортрябва да са от същия тип. Условие в дефиницията на условен израз е всеки израз от тип Bool. Пример за такива изрази са сравненията. Когато сравнявате, можете да използвате следните оператори:
    , = - тези оператори имат същото значение като в езика C (по-малко от, по-голямо от, по-малко от или равно на, по-голямо от или равно на).
    == е операторът за проверка на равенството.
    /= - оператор за проверка на неравенство.
    Bool изразите могат да се комбинират с помощта на общите логически оператори && и || (И и ИЛИ), а отрицанието не функционира.
    Примери за приемливи условия:
    x >= 0 && x x > 3 && x /= 10
    (x > 10 || x Разбира се, можете да дефинирате свои собствени функции, които връщат Bool стойности и да ги използвате като условия. Например, можете да дефинирате функция isPositive, която връща True, ако нейният аргумент е неотрицателен и False в противен случай :
    isPositive::Integer -> Bool
    isPositive x = if x > 0 then True else False

    Сега функцията signum може да се дефинира по следния начин:
    signum::Integer -> Integer
    signum x = ако е положително x, тогава 1
    else if x else 0
    Имайте предвид, че функцията isPositive може да се дефинира по-просто:
    isPositive x = x > 0

    Информацията е взета от: http://sguprog.narod.ru/

Матю Грифин

След като изучавах Haskell дълго време, натрупах достатъчно опит, за да ви дам някои съвети. Освен това бих искал да си набия някои принципи в главата, преди да продължа.

И въпреки че понякога прибягвам до Помощ за Python, сега върша по-голямата част от работата си в мрежата с Haskell.

Първо данните

Мислех да премина от динамичен език към статичен език и в Haskell структурата на данните, с които работите, е ясно описана, когато се декларира. В Python в повечето случаи тази задача се изпълнява от код.

Когато за първи път видях функции в Haskell, се чудех: „Какво са данни? Тази функция приема ли нещо като вход и произвежда ли нещо като изход?“ И когато работех с Python, имах въпрос: „КАКВО КАЗВА КОДЪТ?“

Структурата на данните на Haskell оформи напълно нов начин на мислене за мен, който внесох в работата си в Python. Кодът ми стана значително по-добър. И въпреки че много често ми се струваше, че формата на данните, които описах, се променя без причина, всъщност всичко стана ясно с малко проучване на въпроса. Ограничаването на свободата за промяна на данни също прави кода по-малко сложен и по-разбираем.

Четливост на кода

Python ме привлече със способността действително да пиша четим код. Примерите за код на Haskell изглеждаха просто ужасни, с изключение на някои фрагменти, които изглежда бяха избрани специално, за да не плашат начинаещите. И въпреки че някои части от код изглеждаха много добре, по-голямата част от изходния код беше изпълнен с нещо ужасно. И зад това „ужасно“ се криеше именно цялата мощ на езика.

Определено не исках да пиша "умен" код - такъв, който е в състояние да демонстрира впечатляващите възможности на езика, но е напълно неудобен.

Въпреки това, аз оцених четимостта на Haskell в сравнение с други популярни езици за програмиране. Сякаш оценявах Китайски, тъй като е роден английски език.

Разбрах, че Haskell не е „умен“ език, но с трик. Разбира се, възможно е да се напише „умен“ код на Haskell, но това не е често срещан случай. В същото време силата на интелигентния код е ограничена от силното въвеждане. Ако дадена функция трябва да върне Int, тя ще върне Int. Е, или в краен случай ще изведе грешка при компилация.

И по-мощните абстракции в Haskell всъщност приличат на някаква магия, която се опитах да избегна, когато работех с Python.

Говоря сериозно за четливостта.

Първо, повярвайте и се убедете, че да, хората четат обяви на този нов език и го правят бързо и редовно. След като най-накрая разбрах повечето от известните нюанси, започнах да се чувствам по-спокоен относно кода на Haskell.

Коментари.Те заемат горен редв една от главите на нашата „книга“.

Тази глава описва как Томи отишъл до магазина и си купил патица.

глава:: Томи -> Пазар -> Патица

Функцииот друга, намалена функция, в общата картина кодът е намален максимално.

краткост.Нямате нужда от много код, за да реализирате идеите си.

Вмъкване на знаци

Исках също да спомена функциите за вмъкване, които са често срещани в Haskell. Функциите за вмъкване (или операторите) са тези функции, които използват междувместо това за два аргумента преди. Прост пример е "+".

В Haskell имаме няколко символа за вмъкване, които се използват по подразбиране: $,<$>, <-, ->и т.н. Те могат да ви объркат малко в началото на пътуването.

Не се безпокой! След като се научите как да ги използвате, ще разберете колко полезни и прости са те. Преброих около 5 символа, които използвам редовно.

Не препоръчвам веднага да използвате библиотеката с лещи, тъй като тя съдържа много такива символи. Тази библиотекамного полезно, но в началото ще можете да постигнете големи успехи и без него. Изчакайте, докато можете безопасно да пишете програми средна трудноств Haskell, след което започнете да използвате lens.

Трябва напълно да актуализирате знанията си

Докато изучавате Haskell, ще срещнете нови термини по пътя, като напр функторили монада.

Може да намерите тези думи трудни за запомняне и научаване. Когато започнете да изучавате популярен език за програмиране, много от термините са ви ясни и познати от езиците, които сте научили.

Запомняме информация, като я свързваме с друга информация. Аз например нямам асоциации с думата функтор, така че няма да ми е лесно да науча този термин.

Моята стратегия за научаване на такива думи ми хрумна наскоро и започнах да я използвам всеки път, когато видя термин, който не разбирам. Състои се от подбор на синоними за онези думи, които не разбирате и не са ви познати. След известно време се научих да избирам тези синоними.

Например, функтор.

Haskell показва много неща на вашия екран. Например списъкът е функтор. Това означава, че функция за картографиране, която използва друга функция, е това, което се показва в списъка. След това се създава нов списъкс резултатите.

Карта (+1) -- резултати в

Затова му дадох име - mapa. Думата "mapa" е много лесна за запомняне. Списъкът е функтор. Списъкът е карта.

Моята система за проверка на грешки

Когато написах Python, моят инструмент за отстраняване на грешки бяха изрази за печат.

В Haskell използвах систематични инструменти.

Но! Можете да приложите Debug.Trace. Тази техникаподобно на това как в Python функцияотпечатването е независимо от Haskell IO. И този модулпървоначално може да бъде от полза. Когато за първи път започнахте да работите с Haskell, мислихте ли колко много ще го използвате?

Ако не използвате изрази за проследяване в кода си след проверка за грешки, веднага ще забележите, че кодът ви на Haskell изглежда по-зле от кода на Python.

Най-добрият урок за монадите

Всеки път, когато чуете за успеха на програмист на Haskell, се чудите: „Как този човек е овладял монадите?“ И така, всичко по реда си.

Трябва да направя малко анализиране. Знаех нещо за това, когато написах Python. Но поради моята неопитност в тази област, сега е доста трудно да направя анализ.

Добре, сега ще ти кажа повече. Ще обясня на Haskell.

Намерих видеоклип в YouTube „Разбор на неща в Haskell“, който описва как да правя анализ на JSON в Haskell с помощта на библиотеката Parsec.

Това видео по невнимание ми помогна да разбера как да използвам монади и апликативни функтори, за да създам това, от което се нуждаех. Разбрах как техните функции (хар, хар) се свързват една с друга.

След писане разборС помощта на видеото започнах да разбирам целия код. Също така започнах да разбирам цялата „естество“ на това. Но в началния етап това няма да е полезно.

И така, Parsec върши работата си достатъчно добре, че моята неопитност при писане на анализ е практически незабележима. Както всеки друг начинаещ в Haskell, аз можех лесно да правя добри програми и операции дори в началото на запознанството си с този език.

Възползвайте се от знанията си

Haskell е основният ми език поради няколко причини:

  1. Избор на технологиите, които ще използвам.
  2. Мога да пиша програмите си по-бързо и най-често това са програмите, които продавам.
  3. Не е нужно да се справяте с незначителни грешки.
  4. Дори когато се сблъскам с няколко грешки, бързо ги решавам, тъй като са повече или по-малко ясни.
  5. Python не се фокусира върху скоростта. Haskell прави същото, но изборът все още зависи от мен.
  6. Рефакторингът всъщност е доста „ветровит“. В Python понякога се ритах, когато забравях да коригирам малки грешки в кода, които по-късно причиняваха огромни проблеми.
  7. Прекрасни библиотеки. Основната характеристика на Haskell е високо качествобиблиотеки.
  8. Общност, винаги готова да помогне.
  9. Лесно мащабиране на код до желаното ядро.
  10. Haskell се актуализира често. Миналата година, когато GHC (компилаторът на Haskell) беше актуализиран до версия 7.8, правейки писането на код два пъти по-лесно, много уеб сървъри бяха ускорени.

В заключение бих искал да кажа, че Haskell беше много забавен за мен. Беше най-доброто изживяванеза целия ми живот.

Използва се рекурсивната дефиниция на факториел. Примерът се състои от три части:

  • дефиниция на факторна функция, която приема един аргумент от тип Integer (цяло число с неограничена точност) като вход и има изход от същия тип. Функцията е дефинирана рекурсивно, типът на параметрите е посочен в изричноза да се избегне двусмислие в тяхното определение.
  • дефиниране на функционален ред, който отпечатва число и неговия факториел в необходимия формат. Използването на командата printf е подобно на езика C++.
  • действителното извеждане на числа и техните факториели. За целта командата създава списък с числа от 0 до 16 включително. Функцията за карта с два аргумента прилага първия аргумент (функцията линия) към всеки елемент от втория аргумент (списъка) и в резултат създава списък от така наречените изходни действия (които са обикновени стойности в Haskell) . За да комбинирате тези действия в едно, се използва командата sequence_, която, когато се приложи към списък с действия, изпълнява първото действие в списъка и след това рекурсивно се прилага към опашката на списъка.

Факториал:

Пример за версии на GHC 6.6.1

Функцията fac се дефинира като произведение на числата от 1 до n чрез вградената в езика функция product.

Здравей свят!:

Пример за версии на GHC 6.10.4

Числата на Фибоначи:

Пример за версии на GHC 6.10.4

Този пример използва една от основните характеристики на езика Haskell - възможността за мързелива оценка и използване на безкрайни списъци. Безкраен списък от числа на Фибоначи fibs се дефинира с помощта на функцията zipWith, която прилага първия аргумент (функция на две променливи, в в такъв случай+) към двойки от съответни елементи на втория и третия аргумент (списъци). tail fibs връща опашката на списъка с fibs (т.е. всички елементи с изключение на първия). По този начин първият елемент от списъка, върнат от zipWith, е сумата от първия и втория елемент на списъка fibs и става негов трети елемент.

Числата на Фибоначи:

Пример за версии на GHC 6.10.4

Този пример използва рекурсивната дефиниция на числата на Фибоначи чрез двойки съседни числа в последователност. Отпечатват се само първите елементи на двойките.

module Main where import Text.Printf fibNextPair :: (Int , Int ) -> (Int , Int ) fibNextPair (x , y ) = (y , x + y ) fibPair :: Int -> (Int , Int ) fibPair n | n == 1 = (1, 1) | в противен случай = fibNextPair (fibPair (n - 1 )) ред n = printf "%d, " $ (fst . fibPair) n main = do sequence_ $ map line [ 1 .. 16 ] putStrLn "..."

Квадратно уравнение:

Пример за версии на GHC 6.10.4

Haskell предоставя тип данни за работа с комплексни числа. Квадратната функция приема като аргумент списък от три комплексни числа(коефициенти на уравнението) и връща списък с корените на уравнението. Записвайте вид корен sign ви позволява да предадете знака на операция като аргумент и по този начин да обобщите нотацията на два знака за корен квадратен от дискриминанта.

module Main where import Data.Complex import System.IO (hFlush , stdout ) quadratic :: (Complex Double , Complex Double , Complex Double ) -> [ Complex Double ] quadratic (0 , _ , _ ) = quadratic (a , b , в) | d == 0 = [ корен (+ )] | в противен случай = [ корен (+), корен (- )] където d = b * b - 4 * a * c коренен знак = знак (- b ) (sqrt d ) / (2 * a ) main = do putStr "A = " hFlush stdout a<- readLn :: IO Double putStr "B = " hFlush stdout b <- readLn :: IO Double putStr "C = " hFlush stdout c <- readLn :: IO Double print $ quadratic (realToFrac a , realToFrac b , realToFrac c )

Функционалните езици за програмиране (FPL) са преди всичко нов (за много) начин на мислене за програмите и програмирането. Често добре написаният функционален код е много по-изразителен и чист от еквивалентен императивен или ООП код.

Повечето съвременни функционални езици също предоставят някои гаранции относно програмите. По-специално, Haskell в повечето случаи гарантира, че дадена програма няма да излезе с грешка при достъп до паметта, като, да речем, C++, или грешка при преобразуване на типа, като, да речем, Python или Ruby. Повечето от тези грешки ще бъдат открити по време на компилация на програмата.

Функционалното програмиране се основава на простата идея, че функциите в даден език не се различават от променливите. Обикновено те добавят към това, че функциите като цяло трябва да бъдат функции в математическия смисъл, тоест те не трябва да имат вътрешно състояние и не трябва имплицитно да променят външното състояние (среда). Пряка последица от липсата на състояние е фактът, че функция, извикана със същите аргументи, връща същата стойност. Последицата от непромененото външно състояние е липсата на странични ефекти. Например printf в C/C++ приема някои аргументи и връща число. Но освен това, printf отпечатва знаци на терминала. Последното е страничен ефект. Като цяло косвените промени във външно (по отношение на програмата) състояние се считат за странични ефекти.

Но ако външното състояние е непроменено, програмата неизбежно се превръща в нещо като кантианско „нещо само по себе си“ - не може да чете/пише файлове, терминал и т.н. Това не изглежда много полезно. Всъщност FLP излизат от тази ситуация, като въвеждат някаква псевдопроменлива „свят“, която може изрично да бъде предадена на главната функция на програмата (main), а нейното променено състояние изрично се връща от главната функция на програма. Така една функционална програма може да бъде представена алгебрично като определена функция:

\[ W_(n+1) = P(W_(n)), \]

където \(W\) е състоянието на околната среда ("свят"), а \(P\) е програмата.

Друга особеност на FLP, която пряко следва от факта, че функциите нямат и не променят състоянието си, е, че във FLP много често няма „променливи“. Всички изрази са константи, когато се разглеждат от гледна точка на императивните езици. Това свойство се нарича „референтна прозрачност“: ако видите, че дадена функция приема променлива, винаги знаете точно каква стойност има тази променлива.

Този подход значително опростява „мисленето“ за програми: вече не е необходимо да пазите състоянието в главата си - кодът работи точно както се чете - няма скрити параметри.

Използването на чисти функции, наред с други неща, прави възможно значително опростяване на изпълнението на функции от по-висок ред, т.е. функции, които работят върху функции (които също могат да работят върху функции и т.н.). Следователно повечето FLP имат поддръжка за функции от по-висок ред, „вградени“ в езика по интуитивен начин. Като малък пример ще сравня имплементациите на функцията for_each, работеща със списък в C++ и Haskell.

Функции от по-висок ред

В Haskell подобен код би изглеждал така:

Всъщност извикването на функция с аргументи в Haskell е толкова фундаментално, че няма специален синтаксис, така че подобен код може да бъде написан така:

Всъщност езикът вече има такава функция и тя се нарича map. Ще се върнем към него малко по-късно.

Къри

Много интересен момент тук е използването на (1+) за добавяне на 1. За да направите това, ще трябва да говорите за нотацията на Haskell на Брукс Къри (американски математик, 1900-1982), на когото е кръстен езикът. Тази нотация се нарича частично приложение на функциитеили къри.

Накратко, всяка функция от няколко аргумента \(f(x,y)\) може да бъде разложена на последователни приложения на няколко функции от един аргумент. Например \(f(2,3)\) може да се разложи като:

\[ g(y) = f(2,y) \] \[ f(2,3) = h(g(3)) \]

Можете да забележите, че функцията на два аргумента \(f(x,y)\) се превръща във функция на един аргумент \(g(y)=f(x,y)\) . Това е къри. Ако напишем същото нещо в нотация на Haskell:

Тук е приложима операция, наречена алгебрична ета редукция. Дублиращите се аргументи могат да бъдат пропуснати. В резултат на това получаваме

Какво общо има това с (1+)? Най-директният. (1+) . Добавянето е функция на два аргумента и може да се запише като x+y = (+) x y. Тогава прилагането на (+) 1 към един аргумент ще ни даде функция с един аргумент, която добавя 1 към аргумента (1+) е просто синтактична конвенция („захар“), която се редуцира до (+) 1 .

Как да използвате функцията

За да работи карирането, аргументите на функцията се прилагат отляво надясно (или ляво-асоциативно). По този начин, израз на формата

Означава същото като

Това отговаря на въпроса защо не можете да напишете print for_each (1+) и да очаквате адекватни резултати (всъщност такъв код просто няма да се компилира, тъй като print for_each не може да приема аргументи)

Има оператор $, който е точно-асоциативно използване на функцията. Следователно кодът може да бъде написан така:

Това ви позволява да спестите малко скоби, но приложимостта зависи от конкретния случай.

Ета редукция, функционална композиция и нотация без точки

Друг начин за спестяване е намаляването на eta.

Това намаление ви позволява да пишете доста компактен и в същото време напълно разбираем код, като пропускате „допълнителни“ променливи. Тази нотация се нарича „без точки“ (аргументите понякога се наричат ​​„точки“, оттук и името). Нека напишем програма, която чете редове и ги отпечатва в обратен ред.

Пренебрегвайки последния ред за сега (предполагаме, че там има магия), нека разгледаме функцията revlines. Очевидно х е доста излишно тук. В математиката има нотация за състава на функциите, обозначена с точка:

\[ f(g(x)) = (f\cdot g)(x) \]

Haskell има подобна нотация: f (g x) = (f . g) x . Тогава можем да пренапишем revlines като

Или, като използвате намалението на eta:

Използването на функции от по-висок ред също е възможно в този контекст. Да кажем, че може да искаме не само да изведем низовете в обратен ред, но и да извършим някаква друга операция върху масива от низове. Тогава можем да напишем:

Тип анотации

Може да забележите, че в целия код, написан досега, типовете не са посочени никъде. Haskell е строго типизиран език, но типовете не са задължителни. Това е така, защото Haskell извежда типовете автоматично. Въпреки това, за да се подобри четливостта на кода (и да се намали вероятността от грешки), типовете могат да бъдат посочени. Нека запишем типовете на някои функции и да обсъдим какво означава това:

По конвенция типовете започват с главна буква. Ако даден тип е обозначен с малка буква, Haskell автоматично го извежда въз основа на контекста. Така че, например, използвахме for_each с цели числа. Тогава a=Int, b=Int. Можем също така да го използваме със струни и т.н.

Оператор -> – точно-асоциативен (поради кърене), следователно

тези. „функцията приема два аргумента“, еквивалент

тези. „функция приема един аргумент и връща функция от един аргумент“

карта като функция от по-висок ред

Нека се опитаме да напишем наша собствена функция за работа с низове. Да кажем, че пишем програма, която автоматично форматира код.

Получаваме чудовищно изглеждащо съобщение за грешка

Не може да се съпостави тип „Char“ с „’. Очакван тип: -> Действителен тип: -> В първия аргумент на „withLines“, а именно „indent“ В израза: withLines indent

Защо се случва това? Нека запишем видовете на всички функции.

Грешката става съвсем очевидна: withLines очаква функция от тип ->, но ние й даваме String->String. Като цяло String се дефинира като , т.е. списък с герои. Това обяснява съобщението на компилатора.

Нека си припомним за функцията map, която извършва операция върху всеки елемент от масива. Нейният тип

Замествайки a,b=String, намираме това, което търсим:

Оказва се, че map е функция от по-висок ред, която трансформира функция над елементи във функция над масиви (списъци). Такива операции се наричат ​​„повдигане“, защото те „повдигат“ функция от по-проста категория типове към по-сложна. Като цяло, типовата система на Хаскел е силно базирана на теорията на категориите. Сега обаче няма да говорим за теория на категориите.

Разделяне на изявления

Друг пример за използване на функции от по-висок ред

Тук има някакъв доста странен синтаксис. Като начало, нека имаме предвид, че всеки двоичен оператор (и най-общо казано, всяка двоична функция) може да бъде частично приложен като

Като цяло „op“ е използването на двоичната функция op като двоичен оператор

Със същия успех можете частично да приложите втория аргумент:

Тази синтактична захар се нарича „разделяне на изявления“.

. е двоична функция от по-висок ред:

Съответно, ако пренапишем, да речем, withLines с аргумент, получаваме:

Типове данни

В предишните примери използвахме типа данни списък (масив). Как работи този тип? Нека се опитаме да направим персонализиран тип списък.

Сега можем да напишем нещо подобно:

Haskell, разбира се, има вграден тип списък. Дефинира се, както следва:

Можете да видите паралелите. : е Add или cons, е Empty или нула, а List a е [a]. Можем лесно да дефинираме нашите собствени оператори:

Или ги използвайте директно в дефиницията на типа.

Единственото нещо, вградено в езика, е синтаксисът на формата, който автоматично се преобразува в x:y:...:.

Тук се извикват Add и Empty конструктори от типа List. Това са функции, които „конструират“ стойност от тип List. Те практически нямат нищо общо с обектните конструктори в C++/Java.

Някои примери с нашия персонализиран тип:

С mystery1, mystery2 всичко трябва да е ясно. mystery3 е безкраен списък и Haskell се справя добре с него (тъй като е мързеливезик - стойностите се изчисляват само когато се използват), mystery4 е грешка в типа. Int и String не могат да бъдат в един и същи списък.

Други типове данни се записват по подобен начин.

Модели на аргументи

За простота нека предефинираме нашия тип списък на латиница и веднага да използваме оператора:

Нека напишем няколко функции за нашия персонализиран списък:

Прилича на някаква черна магия. Всъщност е просто, ние дефинираме функция, която работи само за специфичен конструктор. _ просто означава, че игнорираме това поле. Можем също да направим аргументите по-конкретни, например:

Шаблоните се проверяват в реда, в който са декларирани, така че във втория случай можем просто да игнорираме стойността.

Ако не посочим всички възможни опции, компилаторът отчита това, например:

Води до съобщение

Съвпадението(ята) на шаблони не са изчерпателни В уравнение за „isOne“: Модели без съвпадение: Няма

Трябва да имате предвид, че програмата ще се срине, ако подадете стойност към такава частично дефинирана функция, която тя не може да обработи.

Може би тип

Изглежда малко странно, че нашата функция takeOne връща списък, а не стойност. Подобна глава на библиотечна функция връща стойност, но хвърля изключение на празен списък (да, има изключения в Haskell, но те се опитват да ги избегнат по очевидни причини). Може да се напише във формата

undefined е ключова дума, която съответства на произволен тип и срива програмата, когато се опитва да изчисли. Друга опция за undefined е грешка x, където x е низ. грешка ще отпечата низа x преди излизане.

На помощ идва типът Може би. Дефинира се така:

По принцип е като списък, само без списъка. takeOne може да бъде пренаписан като:

Изглежда малко по-смислено от опцията за списък и много по-безопасно от undefined.

Нека се опитаме да напишем търсене за знак в низ, използвайки типа Може би:

Предпазители на модела

Предишният код може да бъде пренаписан малко по-кратък:

Този синтаксис се нарича „пазачи на шаблони“ или ограничители на шаблони, свободно преведени на руски. По същество, ако изразът след | се оценява на True, след което шаблонът се задейства. Ако не, тогава се проверява следващият разделител или следващият шаблон, ако е бил последният. иначе е просто по-четлив синоним на True.

В ограничителите също е възможно да се правят шаблонни сравнения. Синтаксисът за това е:

Тогава кодът по-горе може да бъде пренаписан като:

Тип класове

Всъщност можем да напишем по-обща версия на търсенето елемент V списък, като не прави почти нищо:

Променихме само сигнатурата на типа, останалото остана същото. Подписът изглежда странно: съдържа израза Eq a => . Това означава, че типът a принадлежи към класа Eq - класът от типове, за които е дефинирана еквивалентност, т.е. оператор (==) .

Eq се определя, както следва:

Вижда се, че това е само подпис на типа. За да се дефинира имплементация за конкретен тип, се дефинира екземпляр на класа:

Как точно се осъществява сравнението на символи е доста дълга история. Примитивни типове като Int, Char или, да речем, Double са доста дълбоко вградени в езика. Можем обаче да дефинираме екземпляри за по-сложни типове:

Тук отново въвеждаме класово ограничение. По същество ние казваме, че за всички типове a в класа Eq има тип Maybe a, а също и в класа Eq. Нищо не ви пречи да добавите свои собствени типове.

Типовите класове ви позволяват да пишете общи, но безопасни за тип функции.

Отново за Може би

Типът Може би е доста прост и същевременно изненадващо полезен. Има много функции, които Може бидефинираният може да използва този тип като върната стойност. Например:

Това многопо-добре от връщането на NULL като в C/C++. Първо, като погледнете сигнатурата на типа, веднага става ясно какво да очаквате от функцията. Второ, компилаторът няма да позволи изчисления с несъществуваща стойност. Ако в "класическите" езици типовете служат главно на компилатора, тогава във функционалните типове те служат предимно на програмиста.

Очевидният въпрос с Може би е: да кажем, че имаме функция

Помислете за този код:

Какъв е резултатът от такъв код? Правилният отговор е типова грешка. 1 е от тип Int, докато takeOne връща тип Maybe Int. Проблемът е очевиден. Наивно решение на този проблем е да напишете условие:

Ясно е обаче, че за всякакви сложни изрази това се превръща в много времеемка задача.

Сега нека си припомним, че когато говорих за map, казах, че тази функция „повдига“ първия си аргумент в категорията на списъците. Същата концепция се прилага и за всекипроизводни типове. Езикът има обща функция fmap. Да видим как работи:

Този израз връща само 2. fmap "повдига" своя аргумент в категорията на производните типове. Кои точно - компилаторът автоматично извежда от контекста.

Типът fmap се определя като

Това използва клас Functor, чието единствено изискване е fmap да бъде дефиниран. За Maybe дефиницията на екземпляра е тривиална:

Като цяло клас Functor е клас от типове, които приемат един аргумент тип. Друг пример за функтор е списък. Така че map е само специален случай на fmap.

Монади

Засега само ще кажа, че Може би и списъкът са монади. Концепцията за монада идва от теорията на категориите. Тази концепция е ключова в повечето функционални езици, тъй като Haskell, например, капсулира състоянието на средата (това, което функциите не се променят) в IO (входно-изходна) монада. Накрая можем да запишем подписа за main:

Основната функция връща „празнота“ (всъщност разчетена като единица и подобна по значение на празнотата в C) в IO монадата. IO от своя страна някъде „вътре“ съдържа информация за състоянието на околната среда.

Реплика, която често срещаме

използва следните функции:

return и (>>=) (прочетени като свързване) са дефинирани в класа Monad. return просто „избира“ стойността в монадата и bind взема стойността в монадата и я предава на функцията, която връща монадата. В този случай определено „състояние“, капсулирано от монадата (ако съществува), може да бъде имплицитно прехвърлено от функция към функция чрез оператора за свързване.

За тези, които се интересуват, добро обяснение:

При проявен интерес ще маркираме лекцията за продължение.