Token-based IO
Note: Silver now has support for Monads, so this threaded-token approach to IO is mostly1 obsolete.
You can now just definemain
as
fun main IO<Integer> ::= args::[String] =
...;
See here for more details.
Silver’s IO support iswas terrible. Let’s get that out of the way right from the beginning. Silver programs are largely intended to (1) read a file in its entirety and (2) write a file in its entirety, while (3) maybe printing some messages to the console. If you try to be fancier, you’ll be all smdh.
Start with main
:
function main
IOVal<Integer> ::= args::[String] ioin::IOToken
{
return ...;
}
or
fun main IOVal<Integer> ::= args::[String] ioin::IOToken =
...;
You get an input IO token ioin
. You are then responsible for threading this through every IO function you call, in the correct order, and then out through the standard “IO and also some other value” type called IOVal
.
Lets take a look at some types:
abstract production ioval
top::IOVal<a> ::= i::IOToken v::a
function printT
IO ::= s::String i::IOToken
function readFileT
IOVal<String> ::= s::String i::IOToken
function writeFileT
IO ::= file::String contents::String i::IOToken
So, first off, if you’re getting used to Silver conventions, you’ll notice that ioval
has its parameters backwards from what you’d expect. This is indefensible. Sorry.
Next, you can see how every function follows the same pattern: you pass in the token, then you either get a token back, or an IOVal
back.
This is partly because we didn’t have a unit
type yet, so print couldn’t return IOVal<Unit>
or whatever. You know, to be consistent. Oh well.
Here’s a helpful google images search:
https://www.google.com/search?tbm=isch&q=facepalm
Could be worse. Back in the day we had type like IOString
and IOBoolean
and IOInteger
and of course, names of the attributes were different on each. You kids these days are your IOVal
…
If you don’t pass your IO tokens around properly, things can happen weird. Sometimes the order of stuff might get switched around like time travel. Or things don’t get done.
But most often, the issue people hit is the performing of IO actions getting driven by the demand for values other than the IO token. (e.g. you read from a file, and instead of the IO token being what causes the read to happen, it’s the demand for the file’s contents as a string.)
I invite you to marvel at this code:
{--
- Run units until a non-zero error code is encountered.
-}
function runAll
IOVal<Integer> ::= l::[Unit] i::IOToken
{
local attribute now :: Unit;
now = head(l);
now.ioIn = i;
return if unsafeTrace(null(l), i) -- TODO: this is just to force strictness...
then ioval(i, 0)
else if now.code != 0
then ioval(now.io, now.code)
else runAll(tail(l), now.io);
}
The call to unsafeTrace
demands the IO token i
before returning the other parameter. Why? Well, you’ll notice that accessing now.code
may depend upon that Unit
’s IO actions… which means we’d be demanding actions get taken via their return value, not their IO token. Which means ordering can get ~wonky~.
This is evidence that our token passing model is fundamentally broken. Take a laugh.
How did pre-monad Haskell get around this? Our IOVal
type isn’t really adequate. It’s a pair. pre-monad Haskell would use a function. i.e.:
abstract production ioval
IOVal<a> ::= f::(Pair<a IO> ::= IOToken)
How demand was driven (demanding the value or the IO token), in this case, doesn’t matter, because both will trigger calling the function, which will trigger demanding the previous IO token. All good.
Silver doesn’t have good enough lambda support to do this yet. We probably could stick an unsafeTrace
inside the ioval
production for accessing the iovalue
attribute, though. Someone should do that. Maybe.
Always bind IOVal
returning functions to a local. That is:
local file :: IOVal<String> = readFile(filename, ioin);
Why? Because that way, there’s exactly one decoration of the IOVal
undecorated “tree”. And therefore, there’s one decorated copy (the one implicitly created by the local.) And therefore, there’s one cached value of the IO token.
If you, somehow, manage to get two decorated trees corresponding to a single IOVal
undecorated tree, you may get multiple evaluation of IO functions! Having fun yet?
Here’s a forced example, continuing from the above:
local readFileTwice :: String = new(file).iovalue ++ new(file).iovalue;
Bonus: this will actually cause the file to be read three times, when later on file.io
is demanded.
Double Bonus: The IO actions that will be repeated will be those all the way back to the last properly cached IO token value. So you might even execute more than just a read action three times here!
Aren’t you lucky?
-
There still might be some cases where token-based IO is more appropriate - e.g. an analysis that requires performing IO might be cleaner using an IO token passed through the tree with separate threaded attributes than “infecting” everything with an IO monad (although implicit monads should also be considered as a solution.) The
IO
monad itself is implemented using token IO, so this won’t be going away any time soon, at least. ↩︎