Значения и переменные
Как сказано ранее, исходный текст на 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