I/O definition vs execution

I/O definition vs execution
Photo by Sigmund / Unsplash

In previous examples of IO I've been using the null-type to make a pipe of operations execute. Let's break this down a bit and find a better way to do that.

people | [age ≥ 18] | stdout

To better understand how the flow operates we should look at the input/output types of these three objects.

// people
(Array of &Person)

// [age ≥ 18]
&Person -> Boolean

// stdout
&OutputStream

The first problem with our intention is that it's ambiguous. Do we want to print out the booleans or the people that are age ≥ 18? A less ambiguous version would be this:

people | [age ≥ 18] | [person: &Person] [person] | stdout

Here the input to the second block must be a person, which means the output of the [age ≥ 18] test must be the input. Is that even something we can define?

// [age ≥ 18] | [person: &Person] [person]
[left | right]
[left:  left arguments[0] → Boolean
 right: left arguments[0] → left arguments[0]
 ->     right]
[...]

So it is possible to define a method to make the connection between the two. But it's a little awkward to have to code this way just to do a 'filter' operation in the I/O flow.

If we consider that | will act as a 1:1 mapping operation, then filter should be a different binary keyword. Let's go with / to indicate a subset of the left hand side comes out to the right hand side:

people / [age ≥ 18] | stdout

Great! Now let's look at the ø problem:

people / [age ≥ 18] | [mark-as-adult] | ø

The solution to this is pretty simple. Either the right hand side is an 'output' such as a collection or a stream; or the right hand side is a function that doesn't return anything. Therefore we can write:

people / [age ≥ 18] | [mark-as-adult]

The function infers it has no output because nothing uses its output, otherwise it'd return whatever mark-as-adult returns. If it returns nothing then it is also a terminal for the flow.

If mark-as-adult does return something and we assign it to a variable we are not executing yet but defining something that will execute when we give it a terminal, eg:

flow := people / [age ≥ 18] | [mark-as-adult];
flow | stdout

We can be explicit though:

flow := people / [age ≥ 18] | [person -> ø] [mark-as-adult]

In this case flow is assigned ø which isn't an error but isn't helpful. If we intended to get a stream back but made a mistake and wanted an error we could set the type of the flow:

flow: io stream = people / [age ≥ 18] | [person -> ø] [mark-as-adult]

Now we will get a compilation error because the return type of the flow doesn't match the type of the variable flow.

A slightly longer example where we iterate through all our blog posts, select only the ones that are about stz, then grab its title, capitalise it, and output it:

posts / [tag = 'stz'] | [title] | capitalize | stdout