Asynchronous IO (Input-Output)
import io
import io/filesystem
import io/error
import io
import io/error
import io
import io/error
def readFile(path: String): String / Exception[IOError] =
if (internal::existsFile(path)) internal::readFile(path)
else do raise[IOError](ENOENT(), message(ENOENT()))
def writeFile(path: String, contents: String): Unit / Exception[IOError] =
internal::writeFile(path, contents)
namespace internal {
extern js """
const filesystem = {
"test1.txt": "Hello world!",
"test2.txt": "Oh, how wonderful IO is!",
"test3.txt": "Another file, another content.",
"test4.txt": "What's gonna be in this file?"
};
function delayed(k) {
const delayInMs = Math.random() * 250;
setTimeout(k, delayInMs);
}
"""
extern def existsFile(path: String): Bool = js "!!filesystem[${path}]"
extern async def readFile(path: String): String =
js "$effekt.callcc(k => delayed(() => k(filesystem[${path}])))"
extern async def writeFile(path: String, contents: String): Unit =
js "$effekt.callcc(k => delayed(() => k(filesystem[${path}] = ${contents})))"
}
Usually, filesystem operations in the Effekt standard library are only available when using the Node.js backend. For the sake of this tour, we stub the filesystem in the browser and prefill it with the following files:
.
├── test1.txt
├── test2.txt
├── test3.txt
└── test4.txt
Effekt establishes the principle of direct-style IO first.
This means, if we want to open a file, we do not have to choose between a callback-based variant, a promise-based one, or a direct-style one. All of them are derivable from a single operation – in direct style:
import io/error
on[IOError].panic { readFile("test1.txt") }
Since readFile
might fail, we need to handle the IOError
exception using the panicking handler defined in the standard library.
Let’s now write a simple function that swaps the contents of two files.
def swap(file1: String, file2: String) = {
with on[IOError].panic;
val contents1 = readFile(file1)
val contents2 = readFile(file2)
writeFile(file2, contents1)
writeFile(file1, contents2)
}
This implementation is fully direct style and synchronous. All IO operations will be performed sequentially, one-after-another.
swap("test1.txt", "test2.txt")
Swapping twice concurrently
If we want to swap two pairs of files, we can do this sequentially, as well.
def swapSequential() = {
swap("test1.txt", "test2.txt")
swap("test3.txt", "test4.txt")
}
We can also reuse the existing implementation of swap
and run the two swaps concurrently:
def swapConcurrently() = {
val p1 = promise(box { swap("test1.txt", "test2.txt") })
val p2 = promise(box { swap("test3.txt", "test4.txt") })
p1.await;
p2.await
}
swapConcurrently()
So, while the individual file operations in each call to swap
are sequential, the two
swap
s will be interleaved.
Fully concurrent swap
Finally, we can write a concurrent version of swap
that performs both reads and writes concurrently:
def concurrentSwap(file1: String, file2: String) = {
val p1 = promise(box {
with on[IOError].panic;
val res = readFile(file1);
println("Done reading " ++ file1);
res
})
val p2 = promise(box {
with on[IOError].panic;
val res = readFile(file2);
println("Done reading " ++ file2);
res
})
val contents1 = p1.await
val contents2 = p2.await
val p3 = promise(box {
with on[IOError].panic;
writeFile(file2, contents1);
println("Done writing " ++ file2)
})
val p4 = promise(box {
with on[IOError].panic;
writeFile(file1, contents2);
println("Done writing " ++ file1)
})
p3.await;
p4.await
}
concurrentSwap("test1.txt", "test2.txt")