Funxy (фанкси, fun x(y)) — гибридный язык программирования со статической типизацией, pattern matching и удобной работой с бинарными данными.
Гибридный означает сочетание императивного и функционального стилей. Можно писать привычные конструкции if/for, а можно — map/filter/match с pipes и композицией. Зависит от задачи и ваших предпочтений — стили спокойно можно смешивать.
Статическая типизация с выводом типов — компилятор проверяет типы до выполнения, но в большинстве случаев их не нужно указывать явно:
import "lib/list" (map)
// Типы выводятся автоматически
numbers = [1, 2, 3]
doubled = map(fun(x) { x * 2 }, numbers)
// Можно указать явно, если нужно
fun add(a: Int, b: Int) -> Int { a + b }
Для чего подходит
Скрипты и автоматизация. Один бинарник без зависимостей — скачал и работает. Встроенная работа с файлами, JSON, HTTP, SQL.
Небольшие п��иложения. CLI-утилиты, API-сервисы, обработка данных.
Работа с бинарными данными. Парсинг на уровне отдельных битов. Сетевые протоколы, форматы файлов, нестандартные структуры.
Обучение программированию. Простой синтаксис, но с важными концепциями: типы, pattern matching, иммутабельные структуры данных, рекурсия с TCO (можно писать рекурсивный код без страха переполнения стека).
Для кого
Для тех, кто интересуется новыми языками, пишет утилиты и прототипы, хочет попробовать функциональный стиль. Язык экспериментальный — фидбек приветствуется.
Возможности
Pattern matching
fun describe(n) {
match n {
0 -> "zero"
n if n < 0 -> "negative"
_ -> "positive"
}
}
// Деструктуризация в match
user = { name: "admin", role: "superuser", age: 25 }
result = match user {
{ name: "admin", role: role } -> "Admin: ${role}"
{ name: name, age: age } if age >= 18 -> "Adult: ${name}"
_ -> "Guest"
}
print(result) // Admin: superuser
String patterns
// Пример роутинга
fun route(method, path) {
match (method, path) {
("GET", "/users/{id}") -> "User ID: ${id}"
("GET", "/files/{file...}") -> "File: ${file}"
_ -> "Not found"
}
}
print(route("GET", "/users/42")) // User ID: 42
print(route("GET", "/files/css/main.css")) // File: css/main.css
print(route("POST", "/other")) // Not found
{id}— захватывает сегмент пути до следующего/. Переменнаяidполучает типString.{file...}— greedy-захват, забирает весь остаток пути. Например, для/files/css/main.cssпеременнаяfileбудет"css/main.css".
Роутинг прямо в pattern matching, без отдельной библиотеки.
Pipes
import "lib/list" (filter, map, foldl)
result = [1, 2, 3, 4, 5]
|> filter(fun(x) { x % 2 == 0 })
|> map(fun(x) { x * x })
|> foldl(fun(acc, x) { acc + x }, 0)
// 20
Работа с битами
import "lib/bits" (bitsExtract, bitsInt)
// TCP-флаги: 6 бит
packet = #b"010010" // SYN + ACK
specs = [
bitsInt("urg", 1),
bitsInt("ack", 1),
bitsInt("psh", 1),
bitsInt("rst", 1),
bitsInt("syn", 1),
bitsInt("fin", 1)
]
match bitsExtract(packet, specs) {
Ok(flags) -> print(flags) // %{"ack" => 1, "syn" => 1, ...}
Fail(e) -> print(e)
}
Алгебраические типы данных
type Shape = Circle Float | Rectangle Float Float
fun area(s: Shape) -> Float {
match s {
Circle r -> 3.14 * r * r
Rectangle w h -> w * h
}
}
// Option и Result встроены
fun safeDivide(a, b) {
if b == 0 { Zero } else { Some(a / b) }
}
В декларации типа скобки опциональны: Circle Float или Circle(Float). При создании значения используются скобки: Circle(2.0), Rectangle(3.0, 4.0).
Tail Call Optimization
fun countdown(n, acc) {
if n == 0 { acc }
else { countdown(n - 1, acc + 1) }
}
print(countdown(1000000, 0)) // работает, стек не переполняется
TCO работает и для взаимной рекурсии — когда функции вызывают друг друга. Это важно для state-машин:
fun isEven(n) {
if n == 0 { true } else { isOdd(n - 1) }
}
fun isOdd(n) {
if n == 0 { false } else { isEven(n - 1) }
}
print(isEven(1000000)) // true, без переполнения стека
Циклические зависимости
Модули могут импортировать друг друга — анализатор корректно разрешает циклы.
Один бинарник
./funxy script.lang # запуск
./funxy -help lib/http # документация
./funxy playground/playground.lang # веб-playground
Скачал или собрал сам — работает.
Пример: JSON API
import "lib/json" (jsonEncode)
users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]
fun handler(method, path) {
match (method, path) {
("GET", "/api/users") -> {
status: 200,
body: jsonEncode(users)
}
("GET", "/api/users/{id}") -> {
status: 200,
body: jsonEncode({ userId: id })
}
_ -> { status: 404, body: "Not found" }
}
}
// Тест роутинга
print(handler("GET", "/api/users"))
print(handler("GET", "/api/users/42"))
print(handler("DELETE", "/api/users/1"))
Стандартная библиотека
lib/http — HTTP-клиент и сервер. В сочетании со string patterns получается компактный роутинг без внешних зависимостей.
lib/json — кодирование и декодирование JSON. Records, списки, примитивы преобразуются автоматически.
lib/bits + lib/bytes — парсинг бинарных данных на уровне отдельных битов. Под капотом funbit — Go-реализация Erlang bit syntax. Удобно для сетевых протоколов и форматов файлов.
lib/sql — встроенный SQLite, работает сразу, без установки драйверов.
lib/task — асинхронные вычисления. async создаёт задачу, await ждёт результат, awaitAll — параллельное выполнение.
Полный список:
Модуль |
Что делает |
|---|---|
lib/list |
map, filter, foldl, sort, zip, range |
lib/string |
split, trim, replace, contains |
lib/map |
работа с ассоциативными массивами |
lib/json |
jsonEncode, jsonDecode |
lib/http |
httpGet, httpPost, httpServe |
lib/ws |
WebSocket клиент и сервер |
lib/sql |
SQLite |
lib/bits |
парсинг на уровне битов |
lib/bytes |
работа с байтами |
lib/task |
async/await |
lib/crypto |
sha256, md5, base64, hmac |
lib/regex |
регулярные выражения |
lib/io |
файлы и директории |
lib/sys |
аргументы, переменные окружения, exec |
lib/date |
дата и время |
lib/uuid |
генерация UUID |
lib/math |
математические функции |
lib/bignum |
BigInt, Rational |
lib/test |
unit-тесты |
lib/log |
структурированное логирование |
Как попробовать за 1 минуту
# Скачать релиз или собрать из исходников
git clone https://github.com/funvibe/funxy
cd funxy
make build
# Hello World
echo 'print("Hello, Funxy!")' > hello.lang
./funxy hello.lang
# или просто
echo 'print("Hello, Funxy!")' | ./funxy
# Или запустить playground
./funxy playground/playground.lang
# Открыть http://localhost:8080
Что в комплекте
Бинарник
funxy(macOS, Linux, Windows, FreeBSD, OpenBSD)Исходный код на Go
Документация: tutorial + справочник
Playground — веб-интерфейс для запуска кода
Поддержка синтаксиса для VS Code и Sublime Text
Производительность
Funxy — tree-walking интерпретатор. Это значит:
Где быстро:
TCO (хвостовая рекурсия) — миллион вызовов за 700ms
I/O операции — HTTP, файлы, SQL, JSON — bottleneck в I/O, не в языке
List операции — map/filter/foldl оптимизированы
Где медленно:
Интенсивные вычисления без TCO
Классический бенчмарк — числа Фибоначчи с наивной рекурсией O(2^n):
import "lib/time" (clockMs)
fun fib(n) {
if n < 2 { n }
else { fib(n - 1) + fib(n - 2) }
}
start = clockMs()
result = fib(35)
print("fib(35) = ${result}")
print("Time: ${clockMs() - start} ms")
Результат: ~17 секунд. Python делает то же за ~0.7 секунды (bytecode VM).
Это намеренно неэффективная реализация — стандартный бенчмарк, который показывает overhead интерпретатора на вызовах функций. С TCO-версией (аккумулятор) результат мгновенный:
fun fibFast(n, a, b) {
if n == 0 { a }
else { fibFast(n - 1, b, a + b) }
}
print(fibFast(35, 0, 1)) // 9227465, ~0ms
Для скриптов и утилит это не проблема — основное время уходит на I/O. Для числодробилок Funxy сейчас не подходит.
Ближайшие планы
Переключаемые бэкенды. Сейчас один интерпретатор (tree-walk). Планируется архитектура с несколькими бэкендами:
./funxy script.lang # default (tree-walk)
./funxy --backend=vm script.lang # stack-based VM
Stack-based VM — ожидается ускорение в 10-20 раз на вычислениях за счёт:
Линейного массива инструкций вместо обхода AST
Доступа к переменным по индексу вместо хеш-таблицы
Компактного представления (bytecode)
Tree-walk останется как reference implementation и для отладки.
Ссылки
Funxy на GitHub: github.com/funvibe/funxy
impwx
Любопытная идея, но кажется что могут быть неоднозначности, например:
Строка не будет матчиться с самой собой, если в ней есть фигурные скобки (вероятно стоит сделать два типа литералов, различающихся кавычками)
Как сматчить с существующим значением? Например,
не сработает, придется писать так?
Непонятно как работает вывод типов и их сопоставление, например у вас фигурирует тип
Option, а какого конкретно типа будет литералNoneи по каким правилам он выводится?