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 и для отладки.

Ссылки

Комментарии (1)


  1. impwx
    08.12.2025 20:33

    Любопытная идея, но кажется что могут быть неоднозначности, например:

    • Строка не будет матчиться с самой собой, если в ней есть фигурные скобки (вероятно стоит сделать два типа литералов, различающихся кавычками)

    • Как сматчить с существующим значением? Например,

      someAge = 18;
      match user {
          { age: someAge } -> ...
      }
      

      не сработает, придется писать так?

      match user {
          { age: tmpAge if tmpAge == someAge } -> ...
      }
      
    • Непонятно как работает вывод типов и их сопоставление, например у вас фигурирует тип Option, а какого конкретно типа будет литерал None и по каким правилам он выводится?