Added by Ivan Inozemtsev, last edited by Ivan Kornienko on Sep 30, 2010  (view change)

Labels

 
(None)

Вступление

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

Фантом - современный статически типизированный объектно-ориентированный язык, с элементами функционального программирования и поддержкой динамической типизации. В первую очередь в языке радует лаконичность - большинство типичных задач на Фантоме пишутся в 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|) - найти максимум по переданной функции сравнения

Mixins

Nullability

Concurrency

Заключение