I/O

"Summon" functions

In most programming languages, all functions can do I/O. (Meaning, nothing about the function signature tells you whether it does I/O.)
In crow, ordinary functions can't initiate I/O. Functions that do so must be marked summon.

import crow/io/print: out, print main void() summon out print "Hello, world!"

print is a summon function. It can only be directly called by another summon function, which is why we marked main.

A function not marked summon can still call lambdas which do come from a summon function.

import crow/io/print: out, print main void() summon 3 times () => out print "This is I/O" # not 'summon' times void(n nat, f void mut()) for _ : 0::nat .. n # but does call into code from a 'summon' function f[]

In the above example, times is an ordinary function. Yet, running it causes I/O to happen due to calling f.
Still, times is considered ordinary because its I/O is fully controlled by its caller.

It's called summon because it allows a function to "summon" new I/O capabilities ex nihilo, as opposed to receiving them as lambdas through parameters.

If an ordinary function can't reach any lambdas through its parameters, it can't perform any I/O (except log).

Interfaces

Crow has no "interface" keyword, but it does have a pattern that accomplishes that.
In case passing a separate lambda for each possible I/O action is excessive, they can be joined together in a record.

import crow/io/print: out, print main nat(_ string[]) summon make-connection use-connection () connection record shared read string? shared(key symbol) write void shared(key symbol, value string) make-connection connection() summon db string mut[symbol] = () read: shared key => out print "read {key}" db[key] write: shared (key, value) => out print "write to {key}: {value}" db[key] := value () use-connection void(a connection) a write "key", "value" info log "value is {a read "key" ?? "not found"}" ()

In the above example, connection is the "interface": a record full of far lambdas.
It allows use-connection to be written without summon.
Ideally, a small amount of code should be summon and the rest should use interfaces.

Observe that in this example we're using a map to fake most of the I/O.
Since normal functions can only do I/O through their parameters, you can make them into pure functions by passing parameters that do nothing (or fake I/O).
This is useful for tests.

Overriding 'log'

Logging is always allowed, but you can temporarily change the log handler when calling a function.
Currently this can only be done for native code, as JS logging always uses the JS console.

main void() native extern with : (_ => ()) temp-log-handler info log "this won't be logged" info log "done"

For details, see log.