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

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

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

Теперь вы возможно думаете: "Изучение типов с помощью интерпритатора - весело! Но я хочу писать настоящий код!" На что поход 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 = 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!