Skip to main content
Elara is a purely functional language, meaning functions have no side effects by default. All side effects are explicitly tracked in the type system using the IO type.

The IO type

Values of type IO a represent computations that:
  • May perform I/O operations (reading files, printing, network requests)
  • Will produce a value of type a when executed
  • Are not executed until the runtime runs them
def main : IO ()
let main = println "Hello, World!"

def readInput : IO String
let readInput = readFile "input.txt"
IO () represents an I/O action that produces no meaningful value (the unit type ()).

Pure vs impure functions

Pure functions

Pure functions have no side effects:
def add : Int -> Int -> Int
let add x y = x + y

def factorial : Int -> Int
let factorial n = if n == 0 then 1 else n * factorial (n - 1)

def map : (a -> b) -> List a -> List b
let map f list =
    match list with
        [] -> []
        x :: xs -> f x :: map f xs

Impure functions (IO actions)

Functions that perform I/O have IO in their return type:
def println : a -> IO ()
let println = elaraPrimitive "println"

def readFile : String -> IO String
let readFile = elaraPrimitive "readFile"

def getArgs : IO (List String)
let getArgs = elaraPrimitive "getArgs"
You cannot call an IO function from a pure function. The type system enforces this separation.

Sequencing IO actions

Use the bind operator >>= to sequence IO actions:
def (>>=) : IO a -> (a -> IO b) -> IO b
let (>>=) = elaraPrimitive ">>="

-- Read arguments and print them
let main : IO ()
let main = 
    getArgs >>= (\args -> println (toString args))

Chaining multiple actions

let main =
    readFile "input.txt" >>= (\content ->
    println content >>= (\_ ->
    println "Done!"))

The monadic bind (>>=)

The bind operator >>= allows you to:
  1. Execute an IO action
  2. Extract its result
  3. Use that result to determine the next action
def echo : IO ()
let echo =
    getArgs >>= (\args ->
    println ("You said: " ++ toString args))
>>= is pronounced “bind”. The left side is an IO a, and the right side is a function a -> IO b.

Sequencing operator (>>)

Use *> when you don’t need the result of the first action:
def (*>) : IO a -> IO b -> IO b
let (*>) m1 m2 = m1 >>= (\_ -> m2)

let main =
    println "First" *>
    println "Second" *>
    println "Third"
From the examples:
let main =
    println ("10 / 2: " ++ describe res1) >>
    println ("10 / 0: " ++ describe res2)
The >> operator in the Prelude is actually function composition. For sequencing IO, use *> or >>=.

Common IO primitives

Elara provides several primitive IO operations:

Printing

def println : a -> IO ()
let println = elaraPrimitive "println"

let main =
    println "Hello" *>
    println 42 *>
    println [1, 2, 3]

Reading files

def readFile : String -> IO String
let readFile = elaraPrimitive "readFile"

let main =
    readFile "data.txt" >>= (\content ->
    println content)

Command-line arguments

def getArgs : IO (List String)
let getArgs = elaraPrimitive "getArgs"

let main =
    getArgs >>= (String.join ", " >> println)

Combining pure and impure code

Pure functions can be used within IO actions:
def processData : String -> String
let processData s = s ++ "!"

let main : IO ()
let main =
    readFile "input.txt" >>= (\content ->
    let processed = processData content  -- Pure function
    println processed)
Real example:
def safeDiv : Int -> Int -> Option Int  -- Pure
let safeDiv x y =
    if y == 0 then None else Some (x / y)

def describe : Option Int -> String  -- Pure
let describe opt =
    match opt with
        Some val -> "Got value: " ++ toString val
        None -> "Got nothing"

let main =  -- Impure
    let res1 = safeDiv 10 2
    let res2 = safeDiv 10 0
    println ("10 / 2: " ++ describe res1) *>
    println ("10 / 0: " ++ describe res2)

Let-bindings in IO

Use regular let-bindings for pure computations within IO:
let main =
    getArgs >>= (\args ->
    let count = length args
    let message = "Got " ++ toString count ++ " arguments"
    println message)

Monadic comprehension notation

Elara supports special syntax for working with monads:

Let-bang (let!)

Extract values from monadic computations:
def sequenceActions : Monad m => [m a] -> m [a]
let sequenceActions list = match list with
    [] -> pure []
    (x:xs) ->
        let! x' = x
        let! xs' = sequenceActions xs
        pure (x' : xs')

Do-bang (do!)

Execute actions without binding their results:
def sequenceActions_ : Monad m => [m a] -> m ()
let sequenceActions_ list = match list with
    [] -> pure ()
    (x:xs) ->
        do! x
        sequenceActions_ xs
Monadic comprehension syntax is advanced syntax that requires understanding of type classes and monads.

Multi-line IO blocks

Use multi-line syntax for complex IO sequences:
let main =
    let! args = getArgs
    let! content = readFile "data.txt"
    
    let processed = processContent content
    let result = combineResults args processed
    
    println result

The main function

Every Elara program must have a main function:
def main : IO ()
let main = println "Hello, World!"
main is the entry point of your program. It must have type IO () and is executed by the runtime.

Practical patterns

Error handling with Option

def safeReadFile : String -> IO (Option String)
let safeReadFile path =
    -- Implementation would catch errors and return None
    readFile path >>= (\content -> pure (Some content))

let main =
    safeReadFile "data.txt" >>= (\result ->
    match result with
        Some content -> println content
        None -> println "File not found")

Composing IO actions

def printList : List String -> IO ()
let printList list =
    match list with
        [] -> println "Done"
        x :: xs ->
            println x *>
            printList xs

let main =
    getArgs >>= printList

Working with multiple files

let main =
    readFile "file1.txt" >>= (\content1 ->
    readFile "file2.txt" >>= (\content2 ->
    let combined = content1 ++ content2
    println combined))

Why IO matters

The IO type provides several benefits:
  1. Referential transparency: Pure functions always return the same output for the same input
  2. Testability: Pure functions are easier to test without mocking I/O
  3. Reasoning: You can understand code by looking at types
  4. Safety: The type system prevents accidental side effects
-- This function is guaranteed to be pure
def calculate : Int -> Int -> Int
let calculate x y = x + y

-- This function may perform I/O
def process : Int -> IO Int
let process x = println x >> pure (x + 1)
The type system makes side effects explicit, helping you understand and control program behavior.

Build docs developers (and LLMs) love