Fantom — это объектно-ориентированный, статически-типизированный язык общего назначения, разработанный братьями Фрэнками (Brian Frank, Andy Frank). Одна из ключевых особенностей Fantom — это мощная стандартная библиотека, абстрагированная от конкретной среды, в которой она будет исполняться. В данный момент код, написанный на Fantom, можно запустить в Java Runtime Environment (JRE), .NET Common Language Runtime (CLR) или же скомпилировать в код на JavaScript.
class HelloWorld
{
static Void main() { echo("Hello, World!") }
}
Переносимость
Основной причиной создания Fantom было написание программного обеспечения, которое может запускаться на двух платформах Java VM и .NET CLR. Реальность такова, что большинство компаний разрабатывают свое программное обеспечение для одной из этих платформ. Даже такие динамические языки, как Python и Ruby работают на одной из этих виртуальных машин. Fantom был создан решить проблему переносимости с одной виртуальной машины на другую. Исходный код Fantom компилируется в fcode — байткод, который легко может быть транлирован в Java байткод или IL. Транслирование происходит во время выполнения, что позволяет развертывать Fantom модуль, как отдельный файл, и запускать на любой VM.
Портативность означает значительно больше, чем просто Java или .NET. Как было сказано выше, Fantom может компилироваться в JavaScript для работы в браузерах. При этом Fantom не собирается останавливаться на достигнутом, следующие цели — Objective-C для iPhone, LLVM, Parrot.
Элегантное API
Хотя о вкусах и не спорят(«Beauty is in the eye of the beholder»), создатели Fantom по-настоящему одержимы красивым и удобным API. Один из основных принципов Fantom. Java и .NET имеют одну общую тенденцию максимального деления функционала на маленькие независимые и абстрагированные единицы (классы). Fantom имеет противоположную философию — они верят, что можно обойтись малым, но мощным количеством единиц.
Хорошим примером является пакет java.io, который содержит больше 60 классов и интерфейсов, в Fantom все необходимое лежит в четырех классах: File, Buf, InStream и OutStream. И вот так выглядит его использование:
Void textIO()
{
f := File(`text-io.txt`)
// write text file (overwrites existing)
f.out.printLine("hello").close
// append to existing text file
f.out(true).printLine("world").close
// read text file as big string
echo(f.readAllStr)
// read text file into list of lines
echo(f.readAllLines)
// read text file, line by line
f.eachLine |line| { echo(line) }
}
Типизация
Весь мир раскололся на сторонников статической и динамической типизации. Создатели Fantom считают, что обе стороны крайне критично относятся друг к другу и выбирают середину между ними — умеренный подход к системе типизации.
Со стороны статической типизации, Fantom требует описание полей и сигнатур методов с указанием типов. Это хорошая практика, зафиксировать формат общения между компонентами. К примеру, если я хочу написать метод, который работает со строкой и числом, то это должно быть зафиксировано прямо в коде. В отличие от статической типизации сигнатур методов и полей, в коде она часто только мешает, заставляя писать ненужный код. Вывод типов в Fantom позволяет избежать этой проблемы. Например, громоздкое создание словаря в java:
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "one");
map.put(2, "two");
Эквивалентно в Fantom одной строчке:
map := [1: "one", 2: "two"]
Но иногда вам действительно нужна динамическая типизация, поэтому одной из ключевых особенностей Fantom является возможность вызвать метод, используя статическую или динамическую типизацию. Если вы вызываете метод с помощью оператора ".", вызов будет проверен компилятором и скомпилирован в эффективный машинный код.
С другой стороны, вы можете использовать оператор "->", для указания динамического вызова. На самом деле, "->" будет перенаправлен на вызов Obj.trap. По умолчанию trap работает как ".", но только во время выполнения. Вы можете изменить это поведение, определив свой динамический дизайн.
if (a is Str) { return a->toInt }
obj->foo // obj.trap("foo", [,])
obj->foo(2, 3) // obj.trap("foo", [2, 3])
obj->foo = 7 // obj.trap("foo", [7])
Дженерики
Интересно, что пока Fantom пытается сделать код менее типизированным, Java и C# идут в сторону более строгой типизации, дженерики иллюстрируют этот тренд. Полностью параметризированная системы напрямую связана со сложностью системы, поэтому в Fantom сейчас пытаются найти баланс между пользой и сложностью.
В настоящий момент у Fantom ограниченная поддержка дженериков — пользователь не может использовать свои собственные. Однако, три встроенных класса могут List, Map и Func. К примеру, список целых чисел в Fantom объявляется как Int[]. Создатели Fantom считают, что попали в середину: дженерики есть, но без усложнения системы.
Примеси
Вопрос сопоставления модели из предметной области в код — один из самых часто решаемых вопросов в разработке программного обеспечения. Обычно в объектно-ориентированном программирование данная фраза означает моделирование классов и интерфейсов. Java и C# используют одинаковый подход: классы поддерживают одиночное наследование, интерфейсы множественное наследование, но не поддерживают наследование реализаций.
Каждый, кто работал с Java или C#, знает, что выбор между созданием класса или интерфейса очень важен. Потому что, если вы выберете класс, то вы используете свой единственный шанс на наследование реализации. Если у вас большая и сложная модель, то интерфейсы становятся дополнительной нагрузкой. К примеру, если есть два объекта, имеющих разных наследников, и одинаково реализующие один и тот же интерфейс, функционал придется продублировать. Кроме дублирования есть проблема изменение версии интерфейса, которая затрагивает все реализации.
Есть множество хороших причин почему Java и C# используют модель классов/интерфейсов. Множественное наследование открывает многие двери, но происходит это за счет увеличения сложности и довольно неприятных нюансов. Fantom снова занимает середину, называемую примеси (mixins). Примеси — это интерфейсы, которую могут хранить в себе реализацию. Чтобы избежать ошибок множественного наследования, у примеси ограничены некоторые функции, такие как поля, хранящие состояние. Пример примеси:
mixin Audio
{
abstract Int volume
Void incrementVolume() { volume += 1 }
Void decrementVolume() { volume -= 1 }
}
class Television : Audio
{
override Int volume := 0
}
Если интересно, java эквивалент можно посмотреть здесь.
Модульность
Модульность — важный аспект программного обеспечения, который необходим современному языку программирования.
К сожалению, последнюю декаду в java мы испытываем настоящий ад с classpath. Кроме проблем с classpath, java приняла неправильное решение и перешла к монолиту J2SE размером 44Mb, что значительно замедлило наши с вами приложения.
В .NET подошли к этому вопросу крайне серьезно, поэтому благодаря механизму версий, GAC и другим средствам часть проблем из Java была решена. Но они утеряли простоту zip модуля и начали паковать модули в DLL, которые содержат еще много разных запчастей, что не позволяет легко работать с модулем.
В Fantom все строится на модулях, называемых подами (pods). Как и в java, под — это просто zip файл, который можно легко посмотреть. Мета данные пода хранится в специальном файле /meta.props, представляющее собой записи вида ключ-значение, такие как pod.name, pod.version, pod.depends и так далее. Зависимости пода хранятся в его мета данных и описаны в явном и понятном виде.
Для организации кода в пространстве имен в java используются пакеты, а jar файл рассматривается как модуль. Несоответствие между этими понятиями вызывает большую проблему. У вас есть имя java класса, но оно вам не подскажет в каком jar файле живет этот класс и откуда его загружать.
В Fantom было принято простое решение для управления имен: три уровня иерархии в имени «pod::type.slot», т.е. на первом уровне всегда имя пода, затем имя типа (аналог Class в java) и затем имя слота (метод или поле). Такое согласованное поведение в пространстве имен позволяет легко управлять процессом сборки больших систем.
Функциональное программирование
Java и C# движутся в направлении полноценной поддержки замыканий, но после себя они оставляют большой след истории, в качестве старого API. Fantom создавался с поддержкой замыканий на начальной стадии. Замыкания — ключевая особенность языка, которая используется везде и всегда.
// print a list of strings
list := ["red", "yellow", "orange"]
list.each |Str color| { echo(color) }
// print 0 to 9
10.times |i| { echo(i) }
// create a function that adds two integers
add := |Int a, Int b->Int| { return a + b }
nine := add(4, 5)
// map Int to Str
map := [0:"zero", 1:"one", 2:"two"]
// empty Int:Str map
Int:Str[:]
// map
[1, 2, 3].map { "f" + it * 2 } // ["f2", "f4", "f6"]
//reduce
["2":2, "3":3, "4":4].reduce(0) |Int sum, Int v->Int| { sum + v } // 9
Декларативное описание
Наилучшим описанием будет пара примеров:
Window
{
title = "Example"
size = Size(300, 200)
content = EdgePane
{
center = Label { text = "Hello world"; halign = Halign.center }
bottom = Button { text = "Close"; onAction.add { it.window.close } }
}
}.open
homer := Person
{
name = "Homer Simpson"
age = 39
children =
[
Person { name = "Bart"; age = 7 },
Person { name = "Lisa"; age = 5 },
Person { name = "Maggie"; age = 1 }
]
}
Параллелизм
Большинство языков в настоящий момент имеют одну общую модель между потоками. Это означает, что разработчик сам должен позаботиться о блокировке памяти. Некорректная блокировка может привести к неприятных ошибкам, таким как взаимные блокировки, состояние гонки и т.д. Все это довольно низкий уровень к управлению параллелизма.
Fantom поддерживает параллелизм, используя следующие техники:
- Неизменяемые объекты (потоковая безопасность)
const class Point { new make(Int x, Int y) { this.x = x; this.y = y } const Int x const Int y } p := Point(0, 0) // ok p.x = 10 // throws ConstErr vowels := ['a','e','i','o','u'].toImmutable
- Статические поля могут хранить только неизменяемые объекты, поэтому разные потоки не могут получить доступ к общим изменяемым данным.
- Модель сообщений (actors) для общения между потоками (Erlang-style)
echo("n--- echoActor ---") // this actor just echos messages sent to it a := Actor(ActorPool()) |msg| { echo(msg); return msg } // send some messages and have them printed to console f1 := a.send("message 1") f2 := a.send("message 2") f3 := a.send("message 3") // now block for the result of each message echo("Result 1 = " + f1.get) // message 1 echo("Result 2 = " + f2.get) // message 2 echo("Result 3 = " + f3.get) // message 3
Синтаксический сахар
- Значения по умолчанию для параметров
class Person { Int yearsToRetirement(Int retire := 65) { return retire - age } Int age }
- Вывод типов — типы локальных переменных могут быть выводимыми
- Неявный доступ к полям
class Thing { Int id := 0 { get { echo("get id"); return &id } set { echo("set id"); &id = it } } }
- Нулевые типы — разделение типов на те, которые не могут принимать null, как значение, и которые могут.
Str // never stores null Str? // might store null x := str.size => x is typed as Int x := str?.size => x is typed as Int?
- Убраны проверяемые исключения, в C# Anders Hejlsberg также не включил их (и правильно сделал)
- Числовая точность — существует только 64 разрядная поддержка Int и Float
Ресурсы
- Fantom eclipse-based IDE F4
- Web applications framework Tales
//Hello world written in tales using tales class HelloWorld : Page{ @Route{uri="/hello-world"} Void main(){ response.writeStr("Hello World") } }
- Logic-free template engine Mustache
using mustache Mustache template := Mustache("Hello, {{ name }}!".in) template.render(["name":"world"])
- Fantom Pod repository
- fantom.org
Заключение
Распространённой ошибкой является мнение, что Fantom — это очередная улучшенная версия Java. Но стоит только взглянуть на него, как на язык со своей философией и концепциями, как начинаешь понимать всю его прелесть. Помимо стабильности, к Fantom можно отнести ряд таких особенностей, как дружелюбное сообщество, полностью открытый исходный код, приятную IDE и ещё множество приятных мелочей.
Автор: Zapletnev