I/O definition vs execution
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