A library that turns impure code pure.
npm install @funkia/io




IO is a structure for expressing imperative computations in a pure
way. In a nutshell it gives us the convenience of imperative
programming while preserving some of the properties of a purely
functional programming. Most notable code that uses IO can be tested
in a purely declarative way without actually running side-effects.
* Installation
* Tutorial
* API
* Contributing
* Provides a declarative and pure way to express code with side-effects
* Has a nice API for easily testing IO code without running side-effects
* Ships with both CommonJS and ES2015 modules for tree-shaking
* Is written in TypeScript so comes with full comprehensive type
definitions
IO can be installed from npm. The package ships with both
CommonJS modules and ES6 modules
```
npm install @funkia/io
Let's say we have a function fireMissiles that takes a number nn
and then fires missiles. If fewer than n missiles are available
then only that amount of missiles is fired. The function returns the
amount of missiles that was successfully fired.
`typescript`
function fireMissiles(amount: number): number { ... }
Certainly that is a very easy way of firing missiles. But
unfortunately it is also impure. This, among other things, will make
it tricky to test code using fireMissiles without actually firing
missiles every time the tests are run.
To solve the issue IO provides a method called withEffects. ItfireMissiles
converts from an imperative procedure, that actually
fires missiles, to a pure function that merely returns a _description_
about how to fire missiles.
`typescript`
const fireMissilesIO = withEffects(fireMissiles);
fireMissilesIO has the type (amount: number) => IO. HereIO means an IO-action that does something and then producesnumber
a value of type . The crucial difference aboutfireMissilesIO is that it has no side-effects and that it always
return an equivalent IO-action when given the same number. It is pure.
At first this might seem like nothing but a neat trick. But it
actually allows us to construct imperative computations in a
functional way. To work with IO-actions we can use the fact that IO
is a functor, an applicative and a monad. Thus we can for instance use
it with go-notation.
`javascript${n} missiles successfully fired
const fireMissilesAndNotify = fgo(function*(amount) {
const n = yield fireMissilesIO(amount);
yield sendMessage();`
return n;
});
Here sendMessage has the type (msg: string) => IO. It takes
a string and returns an IO-action that sends the specified message.
Notice that the above code _looks_ like imperative code. In a sense it
_is_ imperative code. It's a functional way of writing imperative
code. Since sendMessage is pure it satisfies referential
transparency. Instead of this:
`javascript`
go(function*() {
yield sendMessage("foo");
yield sendMessage("foo");
});
We can write this:
`javascript`
go(function*() {
const sendFoo = sendMessage("foo");
yield sendFoo;
yield sendFoo;
});
If sendMessage had been impure this refactoring would not havesendMessage
worked–the side-effect in would only have been carried
out once. But since it's pure it's totally fine. In the dumb example
above it only made a small difference but in a real program being able
to perform such refactorings can be very beneficial.
IO-actions can be asynchronous. This makes it possible to express
asynchronous operations very conveniently. Instead of withEffects wewithEffectsP
can use to turn an impure function that returns a
promise into a pure function.
`javascript`
const fetchIO = withEffectsP(fetch);
This creates a function with the return value IO. If the
promise returned by the wrapped function rejects the IO-computation
will result in an error. Error handling is described in the next
section.
The IO monad comes with error handling features. It works throughthrowE
the functions and catchE. They resemble throw and catchIO
but instead of being language-features they are built into the
implementation.
A value of IO can not only produce a value of type A. It may
also produce an error.
To throw an error inside you use throwE:
`javascript`
const sendFriendlyMessageTo = fgo(function*(name, message) {
if (message.indexOf(":)") === -1) {
yield throwE("Please include a friendly smiley :)");
}
const exists = yield checkUserExistence(name);
if (!exists) {
yield throwE("User does not exist");
}
return yield sendMessageTo(name, message);
});
Once an error is yielded the rest of the computation isn't beingIO
run. The resulting value will produce an error instead of a
value.
To catch an error you use catchE. As its first argument it takes aIO
error function handling. As its second argument it takes an IO
computation. It returns a new computation.
`javascript`
const sendFriendlyMessageWithUnfriendlyError(name, message) {
return catchE(
(error) => "Some error happened. I won't tell you which!",
sendFriendlyMessageTo(name, message)
);
}
Here is an example of using fetchIO with error handling. Sincefetch
parsing the body from a response as JSON is an asynchronousresponseJson
operation we define an additional function .
`javascript
const responseJson = withEffectsP((response) => response.json());
const fetchUsersPet = fgo(function*(userId) {
const response = yield catchE(
(err) => throwE(Request failed: ${err}),`
fetchIO(usersUrl + "/" + userId)
);
if (response.states === 404) {
yield throwE("User does not exist");
}
const body: User = yield responseJson(response);
if (body.pet === undefined) {
yield throwE("User has no pet");
} else {
return body.pet;
}
});
An IO-action can be run with the function runIO. The functionIO
actually performs the operations in the IO-action and returns a
promise that resolves when it is done or rejects is the producesrunIO
and unhandled error. is an impure function.
Besides running IO-actions we can also test them. Or "dry-run" them.
To see how this works consider one of the previous examples with a
small bug added in:
`javascript${amount} missiles successfully fired
const fireMissilesAndNotify = fgo(function*(amount) {
const n = yield fireMissilesIO(amount);
yield sendMessage();`
return n;
});
The error is that we don't send a message about how many missiles
where actually fired. Instead we send the number of missiles that
where requested to be fired. We can test the function with testIO:
`javascript10 missiles successfully fired
it("fires missiles and sends message", () => {
testIO(fireMissilesAndNotify(10), [
[fireMissilesIO(10), 10],
[sendMessage(), undefined]`
], 10);
});
The first argument to testIO is the IO-action to test. The second is
a list of pairs. The first element in each pair is an IO-action that
the code should attempt to perform, the second element is the value
that performing the action should return. The last argument is the
expected result of the entire computation.
However, the test above doesn't uncover the bug. Let's write another
one that does:
`javascript5 missiles successfully fired
it("fires missiles and sends message", () => {
testIO(fireMissilesAndNotify(10), [
[fireMissilesIO(10), 5],
[sendMessage(), undefined]`
], 5);
});
Here we specify that when the code attempts to run fireMissilesIO(10)5
it should get back the response . After this the next line willsendMessage
throw because our implementation passes a string to that10
mentions instead of 5. Therefore testIO will throw and our
test will fail.
Converts any value into a IO that will return that value.
Converts an impure function into an IO
Converts a Promise into an IO
Once an error is yielded the rest of the computation isn't beingIO
run. The resulting value will produce an error instead of a
value.
As its first argument it takes a
error function handling. As its second argument it takes an IOIO
computation. It returns a new computation.
The first argument to testIO is the IO-action to test. The second is
a list of pairs. The first element in each pair is an IO-action that
the code should attempt to perform, the second element is the value
that performing the action should return. The last argument is the
expected result of the entire computation.
Contributions are very welcome. Development happens as follows:
Install dependencies.
``
npm install
Run tests.
``
npm test./coverage/
Running the tests will generate an HTML coverage report in .
Continuously run the tests with
``
npm run test-watch
We also use tslint` for ensuring a coherent code-style.