Модули и синтаксис функций
Вновь, добро пожаловать на серию Отрыв Понедельнечного Хаскельного Утра! Это вторая часть серии. Если вы пропустили первую часть, то вам стоит вернуться к ней, где вы сможете скачать, устаноить все необходимое. Мы так же пройдем через базовые идеи выражений, типов и функций.
Теперь вы возможно думаете: "Изучение типов с помощью интерпритатора - весело! Но я хочу писать настоящий код!" На что поход Haskell синтакс? К счастью, на этом мы и сосредоточимся.
Мы наченм писать наш модуль и функции. Посмотрим на то, как читать наш код в интерпретаторе и как запустить его через испольнительный файл. Еще изучим подробнее синтакс функйий для описания более сложных идей. В части третьей этой серии, мы узнаем, как создать свой тип данных!
Если вы хотите проследовать вместе с примерами кода в этойй части, вы можете пройти в репозиторий на Github и скачать. Ссылки будут указаны дальше в статье.
Написание файлов с исходным кодом
Теперь, вы знакомы с базовыми идеями Haskell, мы должны начать писать наш код. Для этой первой части статьи, вы скачать исходинк с Github. Или вы можете написать самостоятельно. Давайте наченм с открытия файла под названием MyFirstModule.hs
, и объявим в нем Haskell модуль используя ключевое слово module
в самом верху файла.
module MyFirstModule where
Выражение where
следует за именем модуля и отражает начальную точку нашего кода. Давайте напишем очень простое выржаение, которое наш модуль будет экспортировать. На назначим выражению имя используя знак равно. В отличии от интерпретатора, нам не нужно использовать слово let
.
myFirstExpression = "Hello World!"
Когда определяется выражение внутри модуля, распространенная практика это указать его сигнатуту в самом верхнем уровне выражения и функции. Это важно понять для любого кто собирается читать ваш код. Это так же помогает компилятору выводит типы внутри вашего подвыражений. Давайте пойдем дальше, и пометим выражение используя в качестве String
используя оператор ::
.
myFirstExpression :: String
myFirstExpression = "Hello World!"
Так же определим нашу первую функцию. Она будет принимать String
в качестве ввода, и складывать входную строку со строкой "Hello". Отметим, как мы определим тип функции используя стрелку от входного типа в выходной.
myFirstFunction :: String -> String
myFirstFunction input = "Hello " ++ input
Теперь имея этот код, мы можем загрузить наш модуль в GHCi. Чтобы сделать это запустим GHCi из той же директории где лежит модуль. Вы можеет использовать :load
команду для загрузки всех опеределений выражений, чтобы и меть доступ к ним. Давайте посмотрим на это в действии:
>> :load MyFirstModule
(loaded)
>> myFirstExpression
"Hello World!"
>> myFirstFunction "Friend"
"Hello Friend"
Если мы изменили наш исходный код, мы можем вернуться обратно и перезагрузить модуль в GHCi используя :r
команду("reload"). Давайте изменим функции как показано ниже:
myFirstFunction :: String -> String
myFirstFunction input = "Hello " ++ input ++ "!"
Теперь перезагрузим и запустим еще раз!
>> :r
(reloaded)
>> myFirstFunction "Friend"
"Hello Friend!"
Ввод и вывод.
В конце, мы хотим иметь возможность запускать наш код без нужны использовать интерпретатор. Чтобы это сделать мы превратим наш модуль в бинарный файл. Делается это с помощью добавления функции под названием main
со специальной сигнатурой.
main :: IO ()
Этот и сигнатуры может казаться странным, так как мы еще не говорили ни о каком IO
или ()
пока. Всё что вам нужно понять, то что этот тип сигнатуры позволяет нашей main
функции взаимодействовать с терминалом. Мы можем, например, запустить некоторые выражение вывода. Для этого воспользуемся специальным синтаксом называемым "do-syntax". Будем использовать слово do
, и затем перечислим возможные действия вывода на каждой линии под.
main :: IO ()
main = do
putStrLn "Running Main!"
putStrLn "How Exciting!"
Теперь у нас есть эта главная функция, нам не нужно использовать интерпретатор. Мы можем использовать терминальную команду runghc
.
> runghc ./MyFirstModule
Running Main!
How Exciting!
Конечно, вы так же можете хотет иметь возможность читать ввод от пользователя, и вызывать различные функции. Для этого нужно воспользоваться функцией getLine
. Вы можете получить доступ используя специальный оператор <-
. Затем с помощью "do-syntax", можно будет использовать let
как делали в интерпретаторе для назначения выражению имени. В этом случае мы вызовем наше прошлое выражение.
main :: IO ()
main = do
putStrLn "Enter Your Name!"
name <- getLine
let message = myFirstFunction name
putStrLn message
Попробуем запустить.
> runghc ./MyFirstModule.hs
Enter Your Name!
Alex
Hello Alex!
Вот так, мы написали нашу первую маленькую Haskell программу.
IF и ELSE синтакс
Теперь мы собираемся немного подвинуться, и посмотреть на то как мы можем сделать нашу функцию более интересной используя Haskell синтакс конструктор. Есть две возможности для этого. Вы можете ссылаться на полный файл который имеет весь конечный код, который мы пишем в этой части. Или вы можете использовать метод "сделай сам", где придется самостоятельно заполнить определения как показано в статье.
Первая синтаксическая идея которую мы изучем будет выражение if
. Давайте предполжим, мы хотим попросить пользовтаеля ввести число. Затем вы делаем различные действия в зависимости от того насколько большое число.
Выражение if
немного отличается в Haskell от того, к чему мы привыкли. Для примера, следующее выражение легко понимается в Java:
if (a <= 2) {
a = 4;
}
Такие выражения не могут существовать в Haskell! Все выражения if
должны иметь else
ветвление! Чтобы понять почему, нам нужно вернуться к основам из прошлой статьи. Помните, все в Haskell это выражение, и любое выражение имеет тип. Так как мы можемт назначить выражению имя, что оно будет значить для имени если выражение станет false
? Давайте взглянем на пример правильного if
выражения:
myIfStatement a = if a <= 2
then a + 2
else a - 2
Это законченное выражение. На первой лини, мы написали выражения типа Bool
, которое может выдать True
или False
. На второй строке, мы написали выражение, которое будет результатом если результат будет True
. Третья строка сработает если результат проверки будет False
.
Помните, любое выражение имеет тип. Так каков же тип этого if
выражения? Предпололжим наш ввод имеет тип Int
. В этом случае, обе ветви тоже будут Int
, значит тип нашего выражения должен быть тоже Int
.
Remember every expression has a type. So what is the type of this if-expression? Suppose our input is an Int. In this case, both the branches (a+2 and a - 2) are also ints, so the type of our expression must be an Int itself.
myIfStatement :: Int -> Int
myIfStatement a = if a <= 2
then a + 2
else a - 2
Что случиться, если мы попробуем сделать, так, что строки будут иметь различный тип?
myIfStatement :: Int -> ???
myIfStatement a = if a <= 2
then a + 2
else "Hello"
Результатом будет ошибка, не важно какой бы тип мы не пытались указать в качестве результат. Это важный урок для выражения if
. У вас есть две ветви, и каждая ветвь должна выдавать тот же результат. Результирующий тип это тип всего выражения.
Отступление, наш пример будет вести к тому, чтобы вы использовали определенный вид записи. Однако, вы можете собрать всё в одной строке.
myIfStatement :: Int -> Int
myIfStatement a = if a <= 2 then a + 2 else a - 2
В Haskell нет elif
выражения как в Puthon. Но подобный механизм достижим. Вы можете использовать if
выражение как целое выражение для ветви else
.
myIfStatement :: Int -> Int
myIfStatement a = if a <= 2
then a + 2
else if a <= 6
then a
else a - 2
Охранные выражения(GUARDS)
InВ situationsслучаее whereкогда youмы mayхотите haveобработать manyразличные differentситуации, casesдля though,читабельности itкода canв beнем moreможно readableиспользовать toохранные useвыражения. guardsОхранные inвыражения yourпозволяют code.вам Guardsпроверять allowлюбое youчисло toразличных checkусловий. onМы anyможем numberпереписать ofкод differentвыше conditions.используя We can rewrite the code above using guards like so:их.
myGuardStatement :: Int -> Int
myGuardStatement a
| a <= 2 = a + 2
| a <= 6 = a
| otherwise = a - 2
ThereЕсть areпара a couple tricky parts here. First, we don't use the term "else" with guards, we use "otherwise"тонкостей. Second,Первая each- individualнам caseне lineнжно hasиспользовать itsключевое ownслово else
с охранными выражениями, используется otherwise
. Второе - каждый отдельный случай имеет свой собственный =
sign,знак, andи thereэто isне not=
anзнак =для signвсего forвыражения. theВаш wholeкод expression.не Yourсоберется codeесли won'tвы compileпопробуете ifнаписать, youчто-то try to write something like:подобное:
myGuardStatement :: Int -> Int
myGuardStatement a = -- BAD!
| a <= 2 ...
| a <= 6 ...
| otherwise = ...
PATTERNСопоставление MATCHINGс образцом.
UnlikeВ otherотличии languages,от других языков, Haskell hasимеет otherдругой waysспособ ofветвления branchingв yourкоде codeкроме besidesбулевых booleans.типов. YouВы canможете alsoтак performже произвести сопоставление с образом(pattern matching.matching). ThisЭто allowsпозволит youизменить toповедение changeкода theосновываясь behaviorна ofструктуре theобъекта. codeДля basedпримера, onмы theможем structureнаписать ofмножество anверсий object.функции Forкаждя instance,из weкоторых canработает writeна multipleопределенном versionsвиде ofаргументов. aВот functionпример, thatкоторый eachведет workсебя onпо aдругому particularосновывась patternна ofтипе arguments.списка, Here'sкоторый anон example that behaves differently based on the type of list it receives.получает.
myPatternFunction :: [Int] -> Int
myPatternFunction [a] = a + 3
myPatternFunction [a,b] = a + b + 1
myPatternFunction (1 : 2 : _) = 3
myPatternFunction (3 : 4 : _) = 7
myPatternFunction xs = length xs
TheПервый firstпример exampleбудет willсовпадать matchс anyлюбым listсписком thatкоторый consistsсодержит ofотдельный aэлемент. singleВторой element.пример Theбудет secondсовпадать exampleс willлюбыми matchпримером anyу exampleкоторого withдва exactlyэлемента. twoТретий elements.пример Theиспользует thirdнекоторый exampleсинтакс usesобъединения someс concatenationкоторым syntaxмы we'reеще notне familiarзнакомы. withНо yet.он Butсовпадает itс matchesлюбым anyсписком listкоторый thatначинается startsс with the elementsэлемента 1 andили 2. TheСледующая nextстрока, lineлюбой matchesсписок, anyкоторый listначинается that starts withс 3 andи 4. ThenПоследний theпример finalбудет exampleсовпадать willс matchдругими all other lists.списками.
ItВажно isотметить, importantкаким toспособо noteшаблоны theсвязывают waysзначения inс whichименами. theВ patternsпервом bindпримере, valuesодин toэлемент names.списка Inсвязан theс firstименем, example,так, theчто singleмы elementможем ofиспользовать theего listв isвыражении. boundВ toпоследнем theпримере, nameполный aсписок soсвязан weс canxs
, useпоэтому itмы inможем theиспользовать expression.его Inв theвыражении, lastчтобы example,мы theмогли fullвзять listего isдлинну. boundДавайте toпосмотрим theна nameэти xs,примеры soв we can take its length. Let's see each of these examples in action:действии.
>> myPatternFunction [3]
6
>> myPatternFunction [1,2]
4
>> myPatternFunction [1,2,8,9]
3
>> myPatternFunction [3,4,1,2]
7
>> myPatternFunction [2,3,4,5,6]
5
NoteПорядокthatвыраженийtheважен!orderВторойofпримерourимеетdifferentтакиеstatementsжеmatters! The second example could have also matched theшаблоны (1 : 2 : _).pattern.НоButтакsinceкакweмыlistedсначалаtheуказали [1,2]patternшаблон,first,онitбудетusedиспользоватьthatэтуversionверсиюofфункции.theЕслиfunction.мыIfпоставимweуниверсальноеputзначениеaпервым,catchallтоvalueвсегдаfirst,будетourвыполнятьсяfunctionтолькоwillэтотalwaysуниверсальныйuseшаблон.that pattern!
-- BAD! Function will always return 1!
myPatternFunction :: [Int] -> Int
myPatternFunction xs = 1
myPatternFunction [a] = a + 3
myPatternFunction [a,b] = a + b + 1
myPatternFunction (1 : 2 : _) = 3
myPatternFunction (3 : 4 : _) = 7
Luckily,К theсчастью, compilerкомпилятор willпредупредит warnнас usо byтом, defaultчто aboutмы theseне un-usedиспользуем patternкакие matches:шаблоны сопоставления с образцом.
>> :load MyFirstModule
MyFirstModule.hs:31:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction [a] = ...
MyFirstModule.hs:32:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction [a, b] = ...
MyFirstModule.hs:33:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction (1 : 2 : _) = ...
MyFirstModule.hs:34:1: warning: [-Woverlapping-patterns]
Pattern match is redundant
In an equation for ‘myPatternFunction': myPatternFunction (3 : 4 : _) = ...
AsПоследним aхочется finalотметить, note,нижнее anподчеркивание(как underscoreпоказано (likeвыш) weможет seeбыть above)использованно canдля beлюбого usedшаблона, forкоторый anyмы patternне orхотим partиспользовать. ofЭто aуниверсальная patternфункция thatи weработает don'tдля needлюбого to use. It functions as a catchall and works for any value:значения.
myPatternFunction _ = 1
CASEУсловные STATEMENTSвыражения
YouВы canможете alsoиспользовать useсопоставление patternс matchingобразом inв theсередине middleфункции ofи aусловными functionвыражениями. withМожно caseпереписать statements.прошлый Weпример could rewrite the previous example like so:так:
myCaseFunction :: [Int] -> Int
myCaseFunction xs = case xs of
[a] -> a + 3
[a,b] -> a + b + 1
(1 : 2 : _) -> 3
(3 : 4 : _) -> 7
xs -> length xs
NoteОтметим, thatчто weмы useиспользуем anстрелку arrow ->
insteadвместо ofзнака anравно equalsдля signкаждого forслучая. eachУсловные case.выражения Theболее caseобобщены, statementпроще isиспользовать aвнутри bitфункции. moreДля general in that you can use it deeper within a function. For instance:примера:
myCaseFunction :: Bool -> [Int] -> Int
myCaseFunction usePattern xs = if not usePattern
then length xs
else case xs of
[a] -> a + 3
[a,b] -> a + b + 1
(1 : 2 : _) -> 3
(3 : 4 : _) -> 7
_ -> 1
WHERE ANDи LET
SoЕсли ifвы youпришли comeиз fromимперативного aязыка, backgroundвы inдолжно aбыть moreнаблюдаете imperativeсейчас. language,И youотметили, mightчто beпохоже makingмы anникогда observationне rightобъявляем now.промежуточные Youпеременные. mightВсе noticeвыражения, thatчто weиспользуются, neverполучаются seemиз toшаблонов define intermediate variables. All the expressions we use come from the patterns of the arguments. Now,аргументов. Haskell doesn'tне technicallyимет haveтехнически "variables"переменных, becauseтак expressionsкак neverвыражения changeне theirменяют value!их Butзначения!Но weвсе canеще stillможем defineизменить sub-expressionsподвыражение withinвнутри ourнашей functions.функции. ThereЕсть areпара aразличных coupleспособов differentдля waysэтого. toДавайте doпредставим this.один Let'sприме, considerгде oneмы exampleпроизводим whereнесколько weматематических performопераций severalна math operations on some inputs:входе.
mathFunction :: Int -> Int -> Int -> Int
mathFunction a b c = (c - a) + (b - a) + (a * b * c) + a
WhileПока weмы canможем congratulateпоздравить ourselvesдруг onдруга gettingс ourтем, functionчто onфункция oneнаписана line,в thisстроку, codeэтот isn'tкод actuallyне veryсовсем readable.читаем. WeМы canможем makeсделать itего farболее moreчитаемым readableиспользуя byпромежуточные usingвыражения. intermediateДля expressions.начала We'llсделаем firstэто doиспользуя thiswhere
using a where clause.выражение.
mathFunctionWhere :: Int -> Int -> Int -> Int
mathFunctionWhere a b c = diff1 + diff2 + prod + a
where
diff1 = c - a
diff2 = b - a
prod = a * b * c
TheЧасть where
sectionобъявляет declaresdiff1
, diff1,diff2
diff2,и anddiff3
diff3в asкачестве intermediateпромежуточоного values.значения. ThenПотом weмы canможем useиспользовать themих inв theкачестве baseбазы ofфункции. theМы function.можем Weиспользовать canwhere
useрезультаты whereдруг resultsс withinдругом, eachи other,не andважно itв doesn'tкаком matterпорядке whatони order we list them in.объявленны.
mathFunctionWhere :: Int -> Int -> Int -> Int
mathFunctionWhere a b c = diff1 + diff2 + prod + a
where
prod = diff2 * b * c
diff1 = c - a
diff2 = b - diff1
However,Однако, beнужно sureбыть youуверенным don'tв makeтом, aчто loopвы byне makingделаете yourцикл where
, resultsгде dependкаждый onрезультат eachзавит other!от соседнего.
mathFunctionWhere :: Int -> Int -> Int -> Int
mathFunctionWhere a b c = diff1 + diff2 + prod + a
where
diff1 = c - diff2
diff2 = b - diff1 -- BAD! This will cause an infinite loop!
-- diff1 depends on diff2!
prod = a * b * c
WeМы canможем alsoполучить accomplishтот theже sameрезультат resultиспользуя bylet
usingвыражение. aСинтаксически letпохожая statement.формулировка, Thisза isисключением aнового similarвыражения syntacticперед. construct,Нам exceptпотом, weнужно declareиспользовать theключевое newслово expressionsдля beforehand.указания Weвыражеения thenкоторое haveбудет toиспользовать use the keyword in to signal the expression that will use the values.значения.
mathFunctionLet :: Int -> Int -> Int -> Int
mathFunctionLet a b c =
let diff1 = c - a
diff2 = b - a
prod = a * b * c
in diff1 + diff2 + prod + a
InВ ситуации с IO
situationsкак likeмы weписали hadвывод whenи printingчтения, andможно readingиспользовать input,let
youв canкачестве useдействия letбез asтребования. anВам actionпросто withoutнужно needingсделать in.это Youбез actuallyиспользования needwhere
toкогда doваше thisвыражение insteadзависит ofот usingпользовательского where when your expression depends on the user's input:ввода.
main :: IO ()
main = do
input <- getLine
let repeated = replicate 3 input
print repeated
WeМы canможем getобойти aroundэту thisтему. though.Мы Weможем canиспользовать usewhere
whereдля toобъявления declareфункции functionsвнутри withinнашей ourфункции. functions!Пример Theвыше exampleможно aboveпереписать couldпо also be written like so:другому:
main :: IO ()
main = do
input <- getLine
print (repeatFunction input)
where
repeatFunction xs = replicate 3 xs
InВ thisэтом example,примере, weмы declareобъявили repeatFunction
asкак aфункцию, functionкотораяа thatпринимает takes a list список(or aили String inв ourнашем case!случае). ThenЗтаем onна theстроке print
, line,мы weпередаем passвходную ourстроку inputв stringкачестве asаргумента anв argumentфункциюю. to the function. Cool!Класс!
SUMMARYЗаключение
ThisМы concludesизучили partочень 2много ofвсего! ourНачали Haskellс Liftoffнаписания series.нашего Weкода, coveredполучение aввода, lotвыведения ofв groundтерминал, here!и Weзапуска startedнашего writingприлоежния ourв ownкачестве code,исполнительного gettingфайла. userИзучили input,расширенный printingсинтакс toфункции. the terminal, and running our Haskell as an executable. Then learned about some more advanced function syntax. We exploredИзучили if-statements,выражения, patternсопоставление matching,с образом, выражения where
andи let
. clauses.
IfЕсли youвас thinkчто-то someсмутило, ofне thisбойетсь, wasвернитесь aи littleпроверьте confusing,еще don'tраз beпервую afraidстатью, toдля goтого, backчтобы andустаканить checkваши outзнания partв 1типа toвыражений! solidifyЕсли yourвам knowledgeвсё onпонятно types- andдвигайтесь expressions!дальше Ifк you'reследующей goodстатье. onВ thisней material,мы youобсудим shouldразличные nowспособы moveсоздания onнашего toсобственного partтипа 3.данных Thereв we'll discuss the various ways of creating our own data types in Haskell!Haskell.
If you want to take a look at some different beginner resources and neat tools, check out our Beginner's Checklist! It will also provide you with a quick review of this whole series!
If you're starting to feel confident enough to want to start your own Haskell project (even a small one!), you should also take a look at our Stack Mini Course! It will walk you through using the Stack utility to create a project. You'll also learn to add components and incorporate Haskell's awesome set of open source libraries into your code!