Вступление
Написать статью меня побудило желание рассказать о замечательном языке, которым я пользуюсь уже около года. Дважды я пытался рассказать о нем на местных конференциях, но мой небольшой опыт выступлений и обилие материала никак не позволяют мне донести информацию о нем в полном объеме.
Фантом - современный статически типизированный объектно-ориентированный язык, с элементами функционального программирования и поддержкой динамической типизации. В первую очередь в языке радует лаконичность - большинство типичных задач на Фантоме пишутся в 2-3 раза быстрее, чем на Java, при этом код получается гораздо более читаемым, что тоже является безусловным преимуществом.
В Фантоме объектом является все, т.е. примитивы и массивы отсутствуют полностью.
В данной статье мне хотелось бы дать краткий обзор языка и сформировать общее представление о нем, при этом не ограничиваться просто переводом языка и стандартной документации.
Сначала я дам небольшой обзор основных отличий синтаксиса языка от Java, чтобы в дальнейшем проще было читать и понимать примеры кода.
Объявления переменных
Главным отличием Фантома от Явы в объявлениях локальных переменных и членов класса является наличие отдельного оператора инициализации, в отличие от оператора присваивания. Причина этого проста - поскольку в языке есть вывод типов, оператор ':=' используется для того чтобы явно выразить объявление новой переменной, что позволяет избежать глупых ошибок.
Несколько примеров:
class Foo
{
Str str := "hello" //строковое поле
Int int := 5 //целочисленное поле
Void method()
{
i := 5 //целочисленная переменная
s := "str" //строка
b := false //булевая переменная
Int[] l := Int[,] //пустой список целых с явным указанием типа
}
}
Вывод типов позволяет избавиться от очевидного недостатка объявления переменных в Java - в 90% случаев имя типа приходится писать дважды:
MyMegaCoolTool tool = new MyMegaCoolTool()
Другое важное отличие Фантома от Явы - это возможность неявного приведения типа к потомку. Рассмотрим пример:
base := Base() derived1 := (Derived) base Derived derived2 := base Derived derived3 := (Derived) base
В точности в соответствии с принципом DRY - do not repeat yourself, объявления derived1 и derived2 эквивалентны объявлению derived3, записанному в Java-стиле.
Классы и методы
Здесь есть несколько отличий от Java:
- В одном файле может быть объявлено несколько классов
- По умолчанию область видимости классов и членов - public
- Для наследования используется ':' вместо 'extends'
- Конструкторы объявляются с ключевым словом 'new' и имеют произвольное имя (однако по конвенции имеют префикс 'make')
- Конструкторы суперклассов вызываются через 'список инициализации', а не как вызов метода 'super'
- Точки с запятой нужна только для разделения нескольких выражений на одной стоке
- В случае если метод состоит только из одного выражения, return не нужен
- Имена методов должны быть уникальны в пределах класса (отсутствует перегрузка методов)
- Методы могут указывать значения по умолчанию для параметров
Пример, иллюстрирующий все вышесказанное:
class Base
{
Int i
new make(Int i := 1) { this.i := 1 }
}
class Derived : Base
{
Str s
new make(Str s, Int i) : super(i)
{
this.s = s
}
Void doSomething() { s + i }
}
Замыкания и функции
С точки зрения Фантома, функция - это объект с набором полей, описывающих сигнатуру, и методом call, который собственно вызывает функцию. Все функции имеют тип, унаследованный от базового класса sys::Func. Существует два способа проинициализировать функцию - взять ее из метода, либо при помощи замыкания. Примеры задания типов функций:
|->| func1 //функция без параметров, возвращающая Void |Int i| func2 //функция с одним параметром типа Int, возвращающая Void |Int| func3 //то же что и выше, но без указания имени параметра |Int i, Str s->Bool| func4//функция с двумя аргументами, возвращающая Bool |Int i, |->| -> |Int->Str| | func5 //функция, принимающая целое, пустую функцию и возвращающая функцию |Int->Str|
Обяъвление замыкания похоже на объявление типа функции и последующего блока кода. Блок кода может содержать выражения, ссылающиеся на параметры функции и на другие переменные окружения, в котором объявляется замыкание.
Ниже представлены примеры замыканий:
//объявляется переменная foo, и инициализируется замыканием без аргументов и возвращаемого значения //тип переменной (а это |->|) выведется из замыкания из левой части foo := |->| { echo("hello, world") } //Bar инициализируется функцией от двух целых, возвращающей true если первый аргумент больше bar := |Int a, Int b -> Bool| { a > b } //Настоящее замыкание, baz будет использовать bar |Int, Int->Int| baz := |Int a, Int b-> Int| { bar(a,b) ? a : b }
Литералы
Фантом имеет очень богатый набор литералов по сравнению с Java. В частности, помимо литералов для строк, чисел и булевых переменных, есть следующие литералы
- списки
[1,2,3] //список целых ["a", 1, 5] //список объектов Str[,] //пустой список строк
- отображения
//Map из Str в Int ["a":1, "b":2] //Map из Int в Int[] [1: [1,2,3], 2: [4,5,6]]
- целочисленные интервалы
//от 0 до 55 включительно 0..55 //от -128 до 127 -128..<128
- временные интервалы
sec := 1sec mins := 5min hrs := 34hr
- URI
`http://fantom.org` `/home/komaz` - Объекты классов и методов (reflection):
Str# //Тип строки Int# //Тип целого Str#toStr //Метод toStr Int#negate //Метод negate класса Int
Коллекции
В настоящий момент в Фантоме имеется только два типа коллекций - списки (List) и отображения (Map). При этом работа с ними в основном осуществляется в функциональном стиле, т.е. здесь отстутствует, например, цикл foreach как языковая конструкция. Вместо этого используется метод each, который принимает функцию и зовет ее для каждого элемента. Рассмотрим это поподробнее.
Описание метода List#each (вы уже заметили что я только что использовал литерал для метода?
) выглядит следующим образом:
** ** Call the specified function for every item in the list starting ** with index 0 and incrementing up to size-1. This method is ** readonly safe. ** ** Example: ** ["a", "b", "c"].each |Str s| { echo(s) } ** Void each(|V item, Int index| c)
Т.е. метод each принимает функцию от двух аргументов - элемента коллекции и ее индекса.
Пример кода, печатающий список чисел на консоль:
[1,2,3,4].each |Int item, Int index| { echo(item) }
Да кстати, echo здесь не какая-то магическая команда - это просто статический метод класса Obj (что автоматически делает его доступным из любой части кода), печатающий переданный аргумент в стандартный поток вывода. Однако вернемся к примеру - в принципе все понятно, но выглядит достаточно неуклюже и громоздко. Будем упрощать.
Во-первых, при передаче замыкания в качестве параметра метода, типы аргументов указывать необязательно (поскольку они уже известны компилятору - он знает функциональный тип соответствующего параметра метода), т.е. можно записать проще:
[1,2,3,4].each |item, index| { echo(item) }
Все еще недостаточно элегантно - мы объявляем аргумент index, который при этом не используем. К счастью, у фантомовских функций есть особенность - они могут принимать больше аргументов чем им требуется. То есть мы можем передать в метод each функцию только от одного аргумента, и, несмотря на то, что реализация метода each будет звать ее с двумя аргументами, все будет работать как ожидается:
[1,2,3,4].each |item| { echo(item) }
Уже выглядит более-менее прилично, однако можно улучшить еще чуть-чуть - для замыканий, принимающих только один аргумент, можно вообще не объявлять заголовок, а просто использовать ключевое слово it, тогда пример сокращается до следующего:
[1,2,3,4].each { echo(it) }
Другая полезная функция - map, позволяющая из одной коллекции сделать другую путем переданной функции отображения. Вот ее описание:
** ** Create a new list which is the result of calling c for ** every item in this list. The new list is typed based on ** the return type of c. This method is readonly safe. ** Obj?[] map(|V item, Int index->Obj?| c)
Теперь - пример использования, но сначала рассмотрим такой код на Java:
public String[] getLastNames(Person[] persons) { List<String> result = new ArrayList<String>(); for(Person person : persons) { result.add(person.getLastName()); } return result.toArray(new String[result.size()]); }
И сравним с кодом на Фантоме:
Str[] lastNames(Person[] persons) { persons.map { it.lastName } }
Напомню, что в Фантоме если функция или метод состоят только из одного выражения, то ключевое слово return не нужно. В примере выше этот факт использовался два раза - один раз в методе и один раз в замыкании.
Другие полезные функции (наверняка знакомые многим):
- findAll(|V, Int -> Bool| f) - найти все элементы, для которых f вернет true
- exclude(|V, Int -> Bool| f) - исключить все элементы, для которых f вернет false
- reduce(R, |R, V, Int -> R| f) - свернуть коллекцию до одного объекта
- sort(|V, V -> Int|) - отсортировать коллекцию по переданной функции сравнения
- max(|V, V -> Int|) - найти максимум по переданной функции сравнения
