DEV Community

Pete Tsamouris
Pete Tsamouris

Posted on • Updated on


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 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 .base
And what can IO do ?

A complete list of what IO can do can be found by means of looking under The focus for a simple input-manipulate-output program will be the two functions that allow reading and writing to the console: : '{IO} Text : 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 the program 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?
Hello, X

Discussion (0)