The full-stack programming language

Firefly code runs in the browser and on the server, or even at build time. You can implement highly interactive webapps without resorting to JavaScript. The basic skeleton of a webapp looks like this:

dependency ff:webserver:0.0.0

// Runs on the server
nodeMain(system: NodeSystem) {...}
    
// Runs in the browser
browserMain(system: BrowserSystem) {...}

// Runs at build time
buildMain(system: BuildSystem) {...}

When starting out, you can put everything in a single .ff file, including your dependency list. As the code base grows, you can split it into multiple files and packages. Read on for a tour of the language.

Concise type definitions

Model your types in a brief format that fits multiple definitions on one screen. Be precise about whether things can be missing with Option[T] and make invalid states unrepresentable with variants and type parameters.

data User(
    id: UserId
    name: String
    email: Option[String]
)

data BlockElement {
    Paragraph(text: String)
    Code(code: String, type: Option[String])
    Video(url: String)
}

Deep pattern matching

You can directly pattern match on function arguments, even in lambda functions. The compiler checks for exhaustiveness, ensuring that all possible cases are covered. Pattern guards are supported, so you can extract things with arbitrary logic.

blockElements.map {
    | Paragraph(text) => 
        renderParagraph(text)
    | Code(code, Some(type)) => 
        renderHighlighted(code, type)
    | Code(code, None) => 
        renderCode(code)
    | Video(url) {vimeoId(url) | Some(id)} => 
        renderVimeo(id)
    | Video(url) => 
        renderVideo(url)
}

Convenient collections

Immutable and mutable collections are part of the standard library. Maps, sets, arrays, lists and streams come with a rich set of methods that you can use to write code that's instantly clear to the reader.

let emails = houses
    .flatMap {_.owners}
    .map {_.email}
    .filter {!_.endsWith("example.com")}
    .toSet()
    
emails.each {email =>
    sendNeighborhoodNewsletter(email)
}

Edit time error detection

In Firefly, a large class of errors is detected by the IDE as you type. You can usually fix these without even reading the error message. And when you need to read the error messages, they're short and to the point.

Type driven autocompletion

The language server comes with type driven autocompletion. It instantly presents a very precise list of completions, and the expected type is used to preselect a likely completion.

No async dilemma

In Firefly, there's no async or await syntax. Instead, the compiler infers which calls are asynchronous and automatically generates the appropriate code. A method like .map on lists is called asynchronously only when the lambda function you pass is asynchronous.

let files = ["a.txt", "b.txt"]
// async .map call
let contents = files.map {file =>
    system.path(file).readText()
}
// sync .map call
let upper = contents.map {content =>
    content.upper()
}

No hidden I/O

The main function is passed a system argument that represents all the I/O you can do. It's a plain object, and you can simply wrap it to create a new object with less capabilities. You can tell what effects a top level function can have simply by looking at what arguments it receives.

nodeMain(system: NodeSytem) {
    let html = fetchSite(system.httpClient())
    system.writeLine(html)
}

// this function can only do HTTP requests
fetchSite(httpClient: HttpClient): String {
    let url = "https://www.example.com/"
    httpClient.get(url, []) {_.readText()}
}

Structural equality

Traits are used for equality, ordering etc., and the core traits are automatically implemented for data types if you don't supply your own implementation. They make == and < type safe, unlike in most languages, and they're a lot simpler than the traits you find in Rust.

trait T: Order {
    compare(x: T, y: T): Ordering
}
    
instance Bool: Order {
    compare(x: Bool, y: Bool): Ordering {
        | False, True => OrderingBefore
        | True, False => OrderingAfter
        | _, _ => OrderingSame
    }
}

The Firefly Stack

The Firefly Stack is a set of packages for building webapps. The packages are maintained by the developers of Firefly. You can use them individually or together. Here's what you can do with that.

Interactive webapps

The ff:lux package provides a declarative DOM framework for building highly interactive webapps. Tasks that belong to removed nodes are automatically cancelled. All without any virtual DOM.

lux.useState(0): count, setCount => 
lux.button {
    lux.text("Clicked " + count + " times")
    lux.onClick {event =>
        event.preventDefault()
        setCount(count + 1)
    }
}

Type safe RPC

All your custom data types are automatically serializable in Firefly. With ff:rpc you can set up type safe remote procedure calls from the browser to the webserver or between services. You can even go to definition across RPC boundaries.

data Message(text: String)
instance Message: Rpc[Int]

// the browser sends a Message
let client = Rpc.newClient(...)
let messageId = client.call(Message("Hello"))

// the webserver replies with an Int
let server = Rpc.newServer(...)
server.add {| context, Message(text) => 42 }

WebSocket server & client

The ff:webserver package comes with WebSocket support. In just a few lines of code, you can start serving WebSockets. The ff:websocket package allows you to connect to any WebSocket server. If both ends are running Firefly, you can use the built-in binary serialization.

let server = WebServer.new(system, host, port)
server.enableWebSockets()

server.listen {request =>
    let ws = request.openWebSocket()
    ws.subscribe("chat")
    while {True} {
        let message = ws.readText().grab()
        ws.publishText("chat", message)
    }
}

Database pooling

The ff:postgresql package lets you create a connection pool for a PostgreSQL database. You can then execute transactions and build stateful applications.

let pool = Pg.newPool(...)

let emails = pool.transaction {connection =>
    connection
        .statement("""
            select email from users
            where id <= $maxId
        """)
        .withInt("maxId", 100)
        .map {_.getString("email").grab()}
}

What Firefly doesn't have

  • No function overloading. Overloading leads to uninformative "No matching overload" errors.
  • No implicit casts. Ever recieved an email addressed to "Dear None"? Static types fail to prevent this if you can implicitly cast an Option[T] to a String.
  • No dynamic typing. Dynamic typing causes runtime errors and encourages hidden and underspecified contracts that makes refactoring harder than it needs to be.
  • No nulls. Eliminating null ensures types accurately represent known values, reducing runtime errors and redundant checks.
  • No subtyping. Subtyping introduces complexity via type bounds and variance. It also allows mixing loosely related types, promoting incomplete runtime type checks.
  • No inheritance. Inheritance can lead to overgeneralization and scattering of business logic, making it hard to get the full picture at any level.
  • No macros. Macros let you define whole new languages, that are typically wildly undertooled compared to the base language.
  • No type level programming. Type level programming leads to inscrutable function signatures and hard to understand error messages.