Программирование на языке Scala/Значения и Выражения

Определения терминов править

Компьютерная программа - набор правил и инструкций для компьютера в виде описания (кода), которые он должен применить для обработки и выдачи данных.

Данные - это информация, которую принимает, обрабатывает и выдает компьютерная программа. Данные могут быть представлены в виде чисел, текста, изображений, звуков и так далее.

Всякая программа выдает выходные данные, иначе такая программа не имеет смысла.

Если компьютерная программа предназначена для обработки входящих данных, то её выходные данные, как правило, заранее не определены.

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

Определение (definition) - это привязка данных или программной конструкции к названию.

Примеры:

val num = 1 // определение имени num. Теперь, под именем num подразумевается число 1.
class A // определение имени A. Теперь, под именем A подразумеваем описание кодовой конструкции "класс"
object B // определение имени B. Теперь, под именем B подразумеваем описание кодовой конструкции "объект"
def getNum = 1 // определение имени getNum. Теперь, под именем getNum подразумеваем описание кодовой конструкции "метод"
type MyInt = Int // определение имени MyInt. Теперь, под именем MyInt подразумеваем тип Int

Определения имеют разнообразный синтаксис, но всегда определяют новые имена для данных или конструкции.

Значение править

Термин значение (value) подразумевает или литерал данных (например число 1) или некие именованные данные.

Определить именованое значение можно установ связь между данными и их именем.

Пример:

val numName = 1

val- служебное слово (value - значение), после которого начинается имя значения numName. Далее символ присвоения =. За ним описание данных в виде литерала цифры 1.

Имя значение может быть произвольным, но должно следовать определенным правилам, о которых будет сказано чуть позже. Основные требование к имени значения - оно должно начинаться с маленькой буквы.

Имя за значением закрепляется навсегда, и не меняется при работе программы. Иными словами, значение является константой.

Значение может задаваться не только данными, но и сложным выражением.

Пример:

val numName = 1 + 2 + 3

где 1 + 2 + 3 выражение. При выполнении определения, с именем numName будет связаны данные в виде цифры 6. То есть, перед связыванием с именем будет выполнено вычисление значения выражения.

Отложить связывание имени значения с данными до момента, когда это значение реально будет использоваться, можно используя комбинацию служебных слов lazy val (ленивое значение)

Пример:

lazy val numName = 1 + 2 + 3

В этом случае значение выражения будет вычислено только в момент использования определяемого значения.

Определение значения всегда выполняется только один раз, даже если оно отложено. Соотвественно если значение задается выражением, то выражение вычисляется только однажды.

Значение всегда имеет тип, как и задающие его данные. При определении значения можно указать его тип для наглядности.

val numName: Int = 1

где Int - является именем типа значения.

Выражения править

Выражение (Expression) - это фрагмент кода, который может вычисляться в значение. Соотвественно тип выражения - это тип его вычисляемого значения.

В функциональной парадигме, выражения - это основной строительный блок кода, программа рассматривается как композиция выражений, и результат получаем путем вычисления выражений.

Пример выражения:

1.+(2)

Сначала идет первый аргумент операции. После точки указывается название операции. В скобках указывается второй аргумент операции. Значение выражения будет 3 типа Int.

1.+(2).+(3).max(4).*(17)

Значение выражения будет 102 типа Int

Выражения можно соединять друг с другом с помощью любой доступной операции, при этом второе выражение должно быть вторым аргументом соединяющей операции:

1.+(2).+(3).max(4).*(17) // выражение1 
1.+(2)                   // выражение2
1.+(2).+(3).max(4).*(17).+(1.+(2))  // соеденяются вместе с помощью опреции +

Значение выражения будет 105 типа Int

Теоретически, весь код программы функционального языка можно написать как одно большое выражение. Но это крайне непрактично, так как усложняет понимание и сопровождение программного кода. Необходимо делить выражения на логические части, и давать им имена для идентификации. Основной конструкцией языка позволяющей выделять выражения в отдельный именованный блок, является функция. Но в простых случаях можно просто разбить сложное выражение на части и вычислить значение этих частей, и уже из этих значений составить другое выражение.

val expression1: Int = 1.+(2).+(3).max(4).*(17) 
val expression2: Int = 1.+(2)                   
val result: Int = expression1.+(expression2)

В итоге мы получили значение result равный 105 типа Int

Вычисление выражений с вызовом операций в методической форме править

1.+(2).+(3).max(4).*(17).toChar

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

Вычисление производится по модели замещения, т.е. в выражении согласно приоритету последовательно, вместо операции и его аргументов подставляется значение операции. И так происходит до тех пор пока доступных операций не останется.

1.+(2).+(3).max(4).*(17).toChar
    3.+(3).max(4).*(17).toChar
          6.max(4).*(17).toChar
                6.*(17).toChar
                    102.toChar
                            'f'


Для изменении порядка вычисления операций можно использовать формирование сложного аргумента операции в виде подвыражения, тогда для вычисления операции сначала необходимо вычислить подвыражение, чтобы получить значение сложного аргумента:

1.+(2).+(3.max(4)).*(17).toChar
    3.+(3.max(4)).*(17).toChar
              3.+(4).*(17).toChar
                  7.*(17).toChar
                      119.toChar
                              'w'


При использовании в выражении другого выражения в виде имени выражения, вычисление выражения с именем будет также в приоритете:

val superMax = 3.max(4)
1.+(2).+(superMax).*(17).toChar
    3.+(superMax).*(17).toChar
            3.+(4).*(17).toChar
                7.*(17).toChar
                    119.toChar
                            'w'


Выражения в операторной нотации править

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

1 + 2 + 3 max 4

полный аналог этого выражения в методическом стиле:

1.+(2).+(3).max(4)

Однако операторная нотация привносит и традиционные математические приоритеты операций. Эти приоритеты являются неявными, о них нужно всегда помнить.

Пример:

1 + 2 + 3 max 4 * 17

имеет результат 68

1.+(2).+(3).max(4).*(17)

имеет результат 102, при кажущейся зрительной аналогии. Это результат присутствия во первом выражении неявного приоритета операции *.

Возможно имеет смысл, указывать приоритеты явно с помощью круглых скобок. Пример:

1 + 2 + 3 max (4 * 17)

дает ожидаемый результат 68

Компилятор принимает решение о приоритете в операторной нотации на основе первого символа опреции, использованного в форме записи операторов. Если имя операции начинается, к примеру, с *, то он получит более высокий приоритет, чем метод, чье имя начинается на +. Следовательно, выражение 2 + 2 * 7 будет вычислено как 2 + (2 * 7).

Аналогично этому выражение a +++ b *** c, в котором a, b и c — переменные, а +++ и *** — методы, будет вычислено как a +++ (b *** c), поскольку метод *** обладает более высоким уровнем приоритета, чем метод +++

Таблица приоритетов первых символов операторов:


Первый символ операции Приоритет
Унарные операторы: +, -, !, ~ высший приоритет
* / %
+ -
:
= !
< >
&
^
буквы, $, _ нисший приоритет


Если приоритет одинаков, то операторы выполняются слева на право.

Можно использовать обе нотации в одном выражении 1 + 2 + 3.max(4) или более читаемо (1 + 2 + 3).max(4)

Алгебраические выражения править

Выражения которые рассматривались в данной главе можно назвать константными выражениями, что означает отсутствие в них незаданных параметров. Такие выражения используются в коде очень редко, так как значение такого выражения можно вычислить заранее, при написании кода, и не делать это при выполнении программы. Например выражение (1 + 2 + 3).max(4) можно сразу в коде заменить на его значение 6.

Такие выражения обычно используют для демонстрации смысла значения. Например выражение val yearMinutes: Int = 365 * 24 * 60 показывает количество минут в году.

Если выражение содержит хотя бы один параметр (имя другого значения), который заранее не определён в коде, то такое выражение уже можно назвать алгебраическим и его нельзя вычислить заранее. Выражение в таком случае превращается некую формулу, значение которой зависит от параметра.

Пример:

val x: Int = (значение или выражение вводит пользователь)
val result: Int = (1 + 2 + 3).max(4) + x
val result2: Int = 6 + x // в сокращенной форме


Результат выражения нельзя вычислить заранее, так как его значение зависит от пользователя программы.

Использование алгебраического выражения в другом выражении делает это выражение тоже алгебраическим. Алгебраические выражение являются доминирующими в коде реальных проектов, а количество их параметров может быть огромным.

Домашнее задание: править

  1. В учебном проекте создать файл Expression.sc (Scala-Worksheet).
  2. Написать 20 наименованных константных выражений используя операции типа Int и разные нотации. Вычислить вручную их значение и тип. Затем вычислить их значение и тип интерпретатором. Сравнить результаты и найти ошибки.
  3. Соединить несколько выражений в составные выражения, используя имена выражений, вычислить их значение и тип.