Before we begin!
Quick reminder that Unison
is still in public alpha
!
Parts of the language, testing functionality and libraries that appear in this post might be subject to change.
In the following post I will do my best to refer to command-line user input and output as I/O when in free text mode, which also seems to be the preferred term of the Unison documentation and guide. In code snippets, the ability will be referred to as IO
with the base.io.
namespace omitted for brevity.
Problem statement: I/O operations in Unison.
I will try and frame the problem statement as a BDD scenario:
GIVEN An individual interested in the Unison PL
WHEN That individual investigates I/O
THEN They have an brief document available to guide them
I will concede at this point that the real problem statement is:
GIVEN I have done I/O before on numerous occasions
WHEN I need to remind myself yet again how to do it
THEN I have my notes kept somewhere
😇 and 🤦♀️
Do I need to understand abilities in depth to do I/O?
Strictly speaking: to get acquainted with I/O you will have to get acquainted with abilities.
If all you care to do is at this point is "read some input from the console and write some output to it" an in depth understanding of what abilities are or how they are handled internally is strictly not necessary. I have however helpfully added the documentation link for your convenience, as some familiarity with the syntax and the moving parts is necessary.
How does ucm
run a program?
Let's have a quick look at an excerpt of the output of the ucm help
command:
$ ucm help
...
ucm run .mylib.mymain
Executes the definition `.mylib.mymain` from the codebase, then exits.
ucm run.file foo.u mymain
Executes the definition called `mymain` in `foo.u`, then exits.
...
ucm
can be asked to run
an existing codebase definition if pointed to an entity via its full namespace path. Furthermore the run.file
flag type-checks the file ucm
is pointed to and then runs the existing codebase definition.
At the time of writing for ucm
to run an entity it must have a type of '{ IO } ()
So single ticks and curly braces?
It might look strange at first but abilities amongst other things are meant to allow "the same syntax for programs that do (asynchronous) I/O", to quote the documentation. In other languages you might see a signature like:
-
f : Int => Unit
for a synchronous function -
f : Int => IO [Unit]
for a function that performs IO
One could observe that "sure IO
involves input and output" but is that synchronous or asynchronous?
By using abilities and handlers the requirement for asynchronous context during the steps in the body of f
would be expressed as:
-
f: Int -> {e} ()
(e
being the empty set of abilities) -
f: Int -> {IO} ()
(still synchronous but involves I/O) -
f: Int -> '{IO} ()
(asynchronous and involves I/O)
While trying to not stray and discuss abilities in general, for the strict scope of I/O the last remaining bit to explain is the single tick. It denotes a delayed computation which is the Unison way of doing asynchronous computations.
How can I get IO
(the Unison ability) in my codebase ?
At the time of writing IO
comes as part of the ucm
base
package that can be pulled while running ucm
.
.> pull https://github.com/unisonweb/base .base
And what can IO
do ?
A complete list of what IO
can do can be found by means of looking under base.io
. The focus for a simple input-manipulate-output
program will be the two functions that allow reading and writing to the console:
base.io.readLine : '{IO} Text
base.io.printLine : Text ->{IO} ()
So how that documentation example?
use io
program : '{IO} ()
program = 'let
printLine "What is your name?"
name = !readLine
printLine ("Hello, " ++ name)
- the function is declared as a delayed side effect
'{IO} ()
- the body must begin with a "delayed let"
- delayed calls must be forced with
!
in this instance!readLine
It might seem a bit heavy to digest in one go but what you have at this point is a program that performs I/O by using the mechanics of the IO
ability. The syntax is such that functions that require synchronous IO
are called directly whereas functions that require asynchronous IO
are forced (at the "end of the world" when the I/O is handled by the runtime).
Run it!
- Copy paste the code into
scratch.u
-
add
theprogram
function as an entity in your codebase (its namespace will be.
by default, so it will live under.program
)
Followed by one of these commands:
ucm run.file scratch.u program
ucm run .program
The output will be something along these lines:
$ ucm run .program
What is your name?
X
Hello, X
Top comments (0)