Значения и переменные

Как сказано ранее, исходный текст на Ficus это последовательность выражений, опделяемых собственно выражениями, объявлениями и директивами. Директивы используются для импорта модулей (описывается в секции Модули) и для настройки компилятора через прагмы. Объявления вводят новые именованные сущности:

  • значения и переменные
  • функции
  • типы, в том числе классы
  • интерфейсы
  • исключения

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

  • Значения присваиваются ровно один раз во время объявления и затем их нельзя изменить.
  • Переменные могут получить новое значение после того, как были инициализированы.

В большинстве языков поддерживаются и значения, и переменные, но в императивных языках используются преимущественно переменные, тогда как в функциональных языках преобладает использование значений. Ficus поддерживает и функциональный, и императивный стили разработки, но все же это больше функциональный язык, так что использование значений поощряется. Например, в компиляторе Ficus, реализованном на Ficus, примерно 4500 объявлений значений и всего около 450 объявлений переменных. Переменные используются в 10 раз реже, чем значения.

Объявления значений и переменных выглядят похоже:

val <имя_значения> [ : <необязательное указание типа>] = <выражение>
var <имя_переменной> [ : <необязательное указание типа>] = <выражение>

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

val a = 5
var b = "abc"
val c = a + 1, d = string(c) // задается два значения

// ошибка: каждое значение или переменная 
// должны явно быть проинициализированы
var x : float;

b += "def"                    // b становится "abcdef"
b = 3.14                      // ошибка: переменная может поменять
                              // значение, но не тип
a = c + 2                     // ошибка: значения нельзя повторно присвоить
var curr_list : int list = [] // явно указываем, что
                              // задается пустой список целых чисел
curr_list = 5 :: curr_list    // добавляем 5 в начало списка

Пользователи C/C++ могу воспринимать значения как const-переменные, концепции очень похоже. Но есть одно важно отличие. Значения нельзя переприсвоить, но если в них содержится мутируемы объект, например массив, то можно изменять части объекта, в случае массива – его элементы:

val m3x3 = array((3, 3), 1.f) // создается матрица 3x3 чисел с плавающей точкой,
                              // заполненная единицами.
m3x3[0, 0] = 0.f              // заменяем.левый верхний элемент нулем
m3x3[:,:] = m3x3 * m3x3       // заменяем всю матрицу 
                              // квадратами значений
m3x3 = m3x3 * m3x3            // ошибка: нельзя переприсвоить значение

Можно спросить, зачем использовать значения, когда переменные объявляются почти также и мощнее?

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

Много ли можно сделать, используя лишь значения? Теория (лямбда-исчисление) говорит, что произвольно сложный алгоритм можно реализовать используя рекурсию, неизменяемые структуры и значения. Например, вот как можно получить первые n чисел Фибоначчи без использования переменных:

fun fibseq(n: int) {
    fun fib_(i: int, a: int, b: int, n: int, result: int list) =
       if i < n {
           fib_(i+1, a+b, a, b::result)
       } else {
           result.rev()
       }
    fib_(0, 1, 1, n, [])
}
println(fibseq(30))

Переиспользование имен значений

Бывает, что есть сложное вычисление, так что вместо большого выражения оно вычисляется по шагам:

val a0 = compute_initial_a()
val a1 = if a0<0 {foo(a0)} else {a0}
val a2 = bar(a1)
val a3 = baz(a2)
...

Такой Код плохо выглядит подвержен ошибкам, поскольку можно случайно ошибиться и написать a1 вместо a2 в определении a3. Чтобы справиться с этим, Ficus позволяет многократно использовать одно имя:

val a = compute_initial_a()
val a = foo(a)
val a = bar(a)
val a = baz(a)
...

Выглядит это так, будто a – переменная, однако компилятор неявно создает новое имя для каждого появления a, то есть после фазы вывода типов код будет выглядеть примерно так:

val a_1234 = compute_initial_a_780()
val a_1235 = foo_542(a_1234)
val a_1236 = bar_601(a_1235)
val a_1237 = baz_927(a_1336)
...

Распаковка структура при определении значений

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

val /* или var */ (x1, x2, ..., xn) = expr

Вместо любого xj можно подставить _, если какое-то из значений не нужно. Распаковка может быть вложенной:

// переопределяем a как min(a, b)
// и одновременно b как  max(a, b)
val (a, b) = (min(a, b), max(a, b))
val person_info = ("Joe", "male", 180, 75, (1980, 5, 1))
val (name, gender, height, weight, (year, _, _)) = person_info