Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Модули и синтаксис функций

WelcomeВновь, backдобро toпожаловать theна Mondayсерию MorningОтрыв Понедельнечного Хаскельного Утра! Это вторая часть серии. Если вы пропустили первую часть, то вам стоит вернуться к ней, где вы сможете скачать, устаноить все необходимое. Мы так же пройдем через базовые идеи выражений, типов и функций.

Теперь вы возможно думаете: "Изучение типов с помощью интерпритатора - весело! Но я хочу писать настоящий код!" На что поход Haskell Liftoffсинтакс? series!К Thisсчастью, isна partэтом 2мы ofи the series. In case you missed it, you should check out part 1, where we downloaded and installed all our tools. We also went through the fundamental concepts of expressions, types, and functions.сосредоточимся.

AtМы thisначенм pointписать youнаш mightмодуль beи thinkingфункции. toПосмотрим yourself,на "learningто, aboutкак typesчитать withнаш theкод interpreterв isинтерпретаторе fine.и Butкак Iзапустить wantего toчерез actuallyиспольнительный writeфайл. code!"Еще Whatизучим doesподробнее Haskell'sсинтакс syntaxфункйий reallyдля lookописания like?более Luckily,сложных that'sидей. exactlyВ theчасти focusтретьей ofэтой thisсерии, article!мы узнаем, как создать свой тип данных!

We'llЕсли startвы writingхотите ourпроследовать ownвместе modulesс andпримерами functions.кода We'llв seeэтойй howчасти, toвы readможете ourпройти codeв intoрепозиторий the interpreter and how to run it as an executable. We'll also learn more advanced function syntax to describe more complicated ideas. In part 3 of this series, we'll check out how we create our own data types!

If you want to follow along with the code examples in this part, you can head to the companionна Github repository!и We'llскачать. provideСсылки linksбудут toуказаны theдальше codeв in a couple different spots throughout the article!

You should also check out our Beginner's Checklist! It will help your review this series and point you towards some more resources that can aid you in your quest to learn Haskell!статье.

WritingНаписание Sourceфайлов Filesс исходным кодом

NowТеперь, thatвы we'reзнакомы familiarс withбазовыми the basic concepts ofидеями Haskell, weмы shouldдолжны startначать writingписать ourнаш ownкод. code.Для Forэтой thisпервой firstчасти partстатьи, ofвы theскачать article,исходинк you can refer to our reference file here onс Github. OrИли youвы canможете writeнаписать itсамостоятельно. allДавайте onначенм yourс own,открытия eitherфайла way!под Let'sназванием startMyFirstModule.hs, byи openingобъявим upв a file called MyFirstModule.hs, and declare it as aнем Haskell модуль используя ключевое слово module byв usingсамом theверху "module" keyword at the top.файла.

module MyFirstModule where

TheВыражение where statementследует followsза theименем nameмодуля ofи theотражает moduleначальную andточку representsнашего theкода. "startingДавайте point"напишем ofочень ourпростое code.выржаение, Let'sкоторое writeнаш aмодуль veryбудет basicэкспортировать. expressionНа thatназначим ourвыражению moduleимя willиспользуя export.знак Weравно. assignВ theотличии expressionот toинтерпретатора, aнам nameне usingнужно theиспользовать equalsслово sign. Unlike the interpreter, we do not need to use the keyword "let" here.let.

myFirstExpression = "Hello World!"

WhenКогда definingопределяется expressionsвыражение withinвнутри aмодуля, module,распространенная itпрактика isэто commonуказать practiceего toсигнатуту putв aсамом typeверхнем signatureуровне onвыражения allи yourфункции. topЭто levelважно expressionsпонять andдля functions.любого Thisкто isсобирается vitalчитать documentationваш forкод. anyoneЭто tryingтак toже readпомогает yourкомпилятору code.выводит Itтипы alsoвнутри helpsвашего theподвыражений. compilerДавайте inferпойдем typesдальше, withinи yourпометим code'sвыражение sub-expressions.используя Soв let'sкачестве goString aheadиспользуя andоператор label this expression as a string using the :: operator..

myFirstExpression :: String
myFirstExpression = "Hello World!"

Let'sТак alsoже defineопределим ourнашу firstпервую function.функцию. ThisОна willбудет takeпринимать aString stringв asкачестве anввода, input,и andскладывать appendвходную theстроку inputсо to the stringстрокой "Hello". NoticeОтметим, howкак weмы defineопределим theтип functionфункции typeиспользуя usingстрелку theот arrowвходного fromтипа theв input type to the output type.выходной.

myFirstFunction :: String -> String
myFirstFunction input = "Hello " ++ input

NowТеперь thatимея weэтот haveкод, thisмы code,можем weзагрузить canнаш loadмодуль our module intoв GHCi. ToЧтобы doсделать this,это runзапустим GHCi fromиз theтой sameже directoryдиректории containingгде theлежит module.модуль. YouВы canможеет useиспользовать the :load commandкоманду toдля loadзагрузки allвсех theопеределений expressionвыражений, definitionsчтобы inи soметь thatдоступ youк canним. accessДавайте them.посмотрим Let'sна seeэто thisв in action:действии:

>> :load MyFirstModule
(loaded)
>> myFirstExpression
"Hello World!"
>> myFirstFunction "Friend"
"Hello Friend"

IfЕсли weмы modifyизменили ourнаш originalисходный sourceкод, code,мы weможем canвернуться thenобратно goи backперезагрузить andмодуль reload the module inв GHCi usingиспользуя the :r command команду("reload"). Let'sДавайте changeизменим theфункции functionкак to:показано ниже:

myFirstFunction :: String -> String
myFirstFunction input = "Hello " ++ input ++ "!"

AndТеперь nowперезагрузим weи reloadзапустим itеще and run it again!раз!

>> :r
(reloaded)
>> myFirstFunction "Friend"
"Hello Friend!"

InputВвод andи Outputвывод.

SoВ eventuallyконце, weмы wantхотим toиметь beвозможность ableзапускать toнаш runкод ourбез codeнужны withoutиспользовать needingинтерпретатор. toЧтобы useэто theсделать interpreter.мы Toпревратим doнаш this,модуль we'llв turnбинарный ourфайл. moduleДелается intoэто anс executable.помощью Weдобавления doфункции thisпод byназванием addingmain aсо functionспециальной named main with a special type signature:сигнатурой.

main :: IO ()

ThisЭтот typeи signatureсигнатуры mightможет seemказаться aстранным, littleтак odd,как sinceмы weеще haven'tне talkedговорили aboutни о каком IO orили () yet.пока. AllВсё youчто needвам toнужно understandпонять, forто nowчто isэтот thatтип thisсигнатуры typeпозволяет signatureнашей allowsmain ourфункции mainвзаимодействовать functionс toтерминалом. interactМы withможем, theнапример, terminal.запустить Weнекоторые can,выражение forвывода. instance,Для makeэтого itвоспользуемся runспециальным someсинтаксом print statements. We'll use a special syntax calledназываемым "do-syntax". WeБудем useиспользовать theслово worddo, do,и andзатем thenперечислим listвозможные aдействия printingвывода actionна onкаждой eachлинии line below it.под.

main :: IO ()
main = do
  putStrLn "Running Main!"
  putStrLn "How Exciting!"

NowТеперь thatу weнас haveесть thisэта mainглавная function,функция, weнам noне longerнужно needиспользовать toинтерпретатор. useМы theможем interpreter.использовать Weтерминальную canкоманду use the terminal command "runghc"runghc.

> runghc ./MyFirstModule
Running Main!
How Exciting!

OfКонечно, course,вы you'llтак alsoже wantможете toхотет beиметь ableвозможность toчитать readввод inputот fromпользователя, yourи user,вызывать andразличные callфункции. differentДля functionsэтого onнужно it.воспользоваться Youфункцией getgetLine. inputВы usingможете theполучить getLineдоступ function.используя Youспециальный canоператор access the input using the special <-. operator.Затем Thenс within do-syntax, you can now useпомощью "let"do-syntax", likeможно youбудет couldиспользовать inlet theкак interpreterделали toв assignинтерпретаторе anдля expressionназначения toвыражению aимени. name.В Inэтом thisслучае case,мы we'llвызовем callнаше ourпрошлое previous expression.выражение.

main :: IO ()
main = do
  putStrLn "Enter Your Name!"
  name <- getLine
  let message = myFirstFunction name
  putStrLn message

NowПопробуем let's try running it.запустить.

> runghc ./MyFirstModule.hs
Enter Your Name!
Alex
Hello Alex!

AndВот justтак, likeмы that,написали we'veнашу writtenпервую our first smallмаленькую Haskell program!программу.

IF ANDи ELSE SYNTAXсинтакс

NowТеперь we'reмы goingсобираемся toнемного shiftподвинуться, gearsи aпосмотреть littleна bit,то andкак lookмы intoможем howсделать weнашу canфункцию makeболее ourинтересной functionsиспользуя moreHaskell interestingсинтакс byконструктор. usingЕсть Haskell'sдве syntaxвозможности constructs.для Thereэтого. areВы twoможете optionsссылаться forна followingполный alongфайл here.который Youимеет canвесь referконечный toкод, theкоторый completeмы fileпишем whichв hasэтой allчасти. theИли finalвы codeможете we'llиспользовать writeметод for"сделай thisсам", part.где Orпридется youсамостоятельно canзаполнить useопределения theкак do-it-yourselfпоказано fileв where you get to fill in the definitions yourself as you go along!статье.

TheПервая firstсинтаксическая syntaxидея conceptкоторую we'llмы exploreизучем isбудет if-statements.выражение Let'sif. supposeДавайте weпредполжим, wantмы toхотим askпопросить theпользовтаеля userввести toчисло. enterЗатем aвы number.делаем Thenразличные we'llдействия doв differentзависимости thingsот basedтого onнасколько howбольшое large the number is.число.

IfВыражение statementsif areнемного aотличается bit different inв Haskell thanот theyтого, areк inчему otherмы languages.привыкли. ForДля instance,примера, theследующее followingвыражение statementлегко makesпонимается perfect sense inв Java:

if (a <= 2) {
  a = 4;
}

SuchТакие anвыражения if-statementне cannotмогут existсуществовать inв Haskell! AllВсе if-statementsвыражения mustif haveдолжны anиметь else branch!ветвление! ToЧтобы understandпонять why,почему, weнам haveнужно toвернуться thinkк backосновам toиз theпрошлой fundamentalsстатьи. fromПомните, theвсе last article. Remember, everything inв Haskell isэто anвыражение, expression,и andлюбое everyвыражение expressionимеет hasтип. aТак type.как Sinceмы weможемт couldназначить assignвыражению theимя, expressionчто toоно aбудет name,значить whatдля wouldимени itесли meanвыражение forстанет thatfalse? nameДавайте взглянем на пример правильного if the statement turned out to be false? Let's look at an example of a correct if statement:выражения:

myIfStatement a = if a <= 2
  then a + 2
  else a - 2

ThisЭто isзаконченное aвыражение. completeНа expression.первой Onлини, theмы firstнаписали line,выражения weтипа writeBool, anкоторое expressionможет ofвыдать typeTrue Bool,или whichFalse. evaluatesНа toвторой True,строке, orмы False.написали Thenвыражение, onкоторое theбудет secondрезультатом line,если weрезультат writeбудет anTrue. expressionТретья thatстрока willсработает beесли theрезультат resultпроверки будет False.

Помните, любое выражение имеет тип. Так каков же тип этого if theвыражения? booleanПредпололжим isнаш true.ввод Theимеет thirdтип lineInt. isВ aэтом differentслучае, expressionобе thatветви willтоже beбудут usedInt, ifзначит theтип resultнашего isвыражения false.должен быть тоже 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

WhatЧто happensслучиться, ifесли weмы tryпопробуем toсделать, makeтак, itчто soстроки thatбудут theиметь linesразличный have different types?тип?

myIfStatement :: Int -> ???
myIfStatement a = if a <= 2
  then a + 2
  else "Hello"

ThisРезультатом willбудет resultошибка, inне anважно errorкакой noбы matterтип whatмы typeне weпытались tryуказать toв substituteкачестве asрезультат. theЭто result!важный Thisурок isдля anвыражения importantif. lessonУ forвас ifесть statements.две Youветви, haveи twoкаждая branches,ветвь andдолжна theyвыдавать eachтот haveже toрезультат. resultРезультирующий inтип theэто sameтип type.всего The resulting type is the type of the whole statement.выражения.

AsОтступление, aнаш sideпример note,будет ourвести examplesк willтому, tendчтобы toвы useиспользовали aопределенный particularвид styleзаписи. ofОднако, indentation.вы However,можете youсобрать canвсё putв theодной if-statement on one line if you like as well:строке.

myIfStatement :: Int -> Int
myIfStatement a = if a <= 2 then a + 2 else a - 2

Note thatВ Haskell doesнет notelif haveвыражения anкак "elif"в statementPuthon. likeНо Python.подобный Youмеханизм canдостижим. stillВы achieveможете thisиспользовать effectif though!выражение Youкак canцелое useвыражение anдля if-statementветви as the entire expression for an else-branch.else.

myIfStatement :: Int -> Int
myIfStatement a = if a <= 2
  then a + 2
  else if a <= 6
    then a
    else a - 2

GUARDSОхранные выражения(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 = 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. 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 can 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 this 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 declares diff1, diff2, and diff3 as intermediate values. Then we can use them in the base of the function. We can 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 by 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, you can use let as an action without needing in. You actually need 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 use 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!

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!