Chapter 12: I/O

Where you will learn how to interact with the outside world.


I/O (short for input and output) is a very important subject in programming.

Arguably, it's the only reason why we make programs in the first place.

Software wouldn't be very useful if we couldn't use files, or interact with foreign devices (like sensors, or other computers), or interact with our users through GUIs.

I/O is fundamental to software development; but, like every fundamental thing in programming, there are many approaches to it, and some of them are in opposition to others.

I would say there are 2 main schools of thought when it comes to the role of I/O in computer programs.

Implicit I/O

These guys say that all programs are fundamentally about the I/O.

Everything, in one way or another, revolves around I/O, and it is a pervasive force that defines what computing is about from it's very foundations.

Most programming languages you may be familiar with subscribe to this idea (either by choice of the language designer(s) or by simply following standard practice).

Here, we see operations which have side-effects (such as printing to standard output, or reading files, or connecting to another computer over the network) be treated like operations which lack them (like adding two numbers), with the side-effects being seen as some kind of magical property exhibited by those operations, which neither the language nor its libraries are obligated to handle in any special way.

Side-effects tend to be mentioned in documentation, but the lack of separation means that programmers must be wary of what they're using if the want to avoid the unfortunate consequences of careless coding.

A common pitfall happens to involve concurrency, where operations being executed on multiple threads, and which have access to common resources, can cause data-corruption and other problems during run-time.

However, programmers who subscribe to this philosophy don't tend to see that as a sign that their languages or tools lack anything, but that programmers need to be careful and mindful of what they code, and that the effectful nature of programming is something to be embraced, rather than constrained.

Explicit I/O

These guys are a very different breed.

Just like static typing is used to impose some order in programs and to make explicit that which only lived in the minds (and comments) of programmers; explicit I/O acknowledges that effectful computations are fundamentally different from pure computations (that is, computations which just process their inputs and calculate their outputs).

Making I/O explicit, rather than being a denial of the very nature of programs, is an acknowledgement of this reality, and a reification of something which only used to live in the minds of programmers (and their comments).

By making I/O explicit, it can be separated from pure computations (thus, avoiding many common mistakes); it can be isolated and handled in safe places (thus avoiding its often pervasive nature); and it can be better integrated into the design of programs by making it a more concrete element.

In a way, it can even be said that immutable data-types (a staple of functional programming that Lux embraces) are just another form of explicit I/O.

Being able to change data after it was created can easily propagate changes beyond your local scope and affect threads and data-structures you aren't even aware of in ways that may be troublesome.

But, just like having immutable data-types makes change explicit and a concrete part of one's design, making I/O explicit achieves a similar goal, with regards to one's interactions with the outside world.

While immutable data-types deal with the mechanics of the inner world of programs, explicit I/O deals with interaction with the outside world; from the very near to the very far.

Lux chooses explicit I/O as it's underlying model.

It may seem odd that I have to justify the choice of explicit I/O in Lux; but, at the time of this writing, implicit I/O is the industry standard, with many even doubting the benefits of an alternative approach.

The only major language which also adopts this model is Haskell (from which Lux takes heavy inspiration), in its efforts to maintain theoretical purity.

How does Explicit I/O Work?

Now that I have (hopefully) convinced you that this is a good idea, how does it work?

Well, by using types; of course!

If you head to the lux/codata/io module, you will find the following type definition:

(type: #export (IO a)
  (-> Void a))

IO is a type that embodies this model, by making it something that the type-system can recognize and work with.

But there is something odd with that definition: it's a function, but it also takes something... impossible.

The function part is easy to explain: to separate I/O from the rest of the language, effectful computations must be delayed until the proper time comes to execute them. That is the reason why Lux programs must produce values of type (IO Unit), instead of just running and returning anything. The run-time system can then take those values and execute them separately.

That might sound like a pointless task, but you may want to run those operations on different contexts (like running them on different threads/processes, for example).

Also, it may be interesting to know that IO happens to be a monadic type; allowing you to compose effectful computations with ease.

But what about Void over there? Didn't you say you can't create void values in chapter 6?

Good observation (and good memory)!

Lux takes care to do some... black magic to get its hands on a Void value in order to run those IO computations.

For the most part, that Void over there is just a means to enforce a separation between normal code and effectful code that most programmers will respect.

I like using Void instead of Unit (or any other type) because it gives I/O a sort of creatio ex nihilo vibe that's very epic, but also illustrates that these otherworldly values are not just calculating something in your programs, but may actually be pulling data out of nowhere by their interactions with the outside world.

Additionally, if you explore the lux/codata/io module, you'll find the means to "run" these I/O operations through the run function.

The io macro, on the other hand, gives you the way to label effectful code as such, by wrapping it inside the IO type.

However, it's unlikely you'll need to use the io macro yourself very often, as some tools we'll see later can take care of that for you.

What I/O Operations are Available?

Sadly, this is one area where Lux (currently) falls short.

There are 2 reasons for that:

  1. Lux's cross-platform ideals.
  2. Lux's youth.

Different platforms (e.g. JVM, Node.js, web browsers, CLR) offer different tools for doing I/O (or maybe even no tools), which makes providing cross-platform I/O functionality a bit of a challenge.

As Lux grows (as a language and as a community), many of the gaps regarding I/O will be filled and there will (hopefully) be some sort of standard I/O infrastructure that will be relatively platform-independent.

In the meantime, I/O will depend on the capabilities of the host platform, accessed through the mechanisms Lux provides for doing host inter-operation (which is, coincidentally, the subject of the next chapter).

This is not as bad as it sounds; and one way or another, programmers will always want to reach to their host platforms to access features beyond what Lux can provide.

And this is fine.

Rather than trying to be what everybody needs and wants all the time, Lux chooses to focus on the subset of things it can do really well, and leaves the rest on the hands of the platform, and the clever programmers who use both.

But, I must confess, there is one way of doing I/O Lux does provide in a cross-platform way, which is the log! function (which prints to standard output).

However, log! has type (-> Text Unit), instead of the expected (-> Text (IO Unit)). The reason is that it was meant for casual logging (as done while debugging), instead of serious I/O, and I felt that forcing people to use IO while logging would have made the process too tedious.


This chapter has been mostly theoretical, as I had to make the case for why explicit I/O is a valuable tool before I could actually explain what it was.

Here, you have learned how to walk outside and yell.

Next, I shall teach you how to walk inside and whisper... to the host platform.

See you in the next chapter!

results matching ""

    No results matching ""