Let's first think about how we would write that second example in an imperative language (loosely modeled after Java but don't yell at me if it doesn't compile):
Header parseHeader(ByteInputStream bytes) {
int size = bytes.readWord8().toInt();
Word16[] fields = new Array();
for (i : (1...size)) {
fields.add(bytes.readWord16le());
}
return new Header(fields);
}
but that's no good in FP, since we're modifying state on a byte input stream, so let's try to make it more functional (let's make some syntax allowances for that, so it will look less like Java):
Now we avoid mutation, so the FP gods are happy, but we have to thread the state through the computation (look at how many variables we used) and it will quickly get out of hand if the structure becomes more complicated. Plus because so many of these variables have the same type, it's easy to accidentally introduce errors.
The example with the "Get Header" monad has the advantage of being as simple to read as the imperative example, but purely functional. That works because the Get monad builds up a description of steps to perform, so is purely declarative. It is only executed when I later call it on some actual byte string (e.g. by "runGet parseHeader myBytes"). As an added benefit, this means that I can define the parsing logic without actually calling it. I could also write additional code that takes this representation and does other things on it instead of just running it (e.g. running it with extra logging, or doing some kind of dry-run, etc.)
Let's first think about how we would write that second example in an imperative language (loosely modeled after Java but don't yell at me if it doesn't compile):
but that's no good in FP, since we're modifying state on a byte input stream, so let's try to make it more functional (let's make some syntax allowances for that, so it will look less like Java): Now we avoid mutation, so the FP gods are happy, but we have to thread the state through the computation (look at how many variables we used) and it will quickly get out of hand if the structure becomes more complicated. Plus because so many of these variables have the same type, it's easy to accidentally introduce errors.The example with the "Get Header" monad has the advantage of being as simple to read as the imperative example, but purely functional. That works because the Get monad builds up a description of steps to perform, so is purely declarative. It is only executed when I later call it on some actual byte string (e.g. by "runGet parseHeader myBytes"). As an added benefit, this means that I can define the parsing logic without actually calling it. I could also write additional code that takes this representation and does other things on it instead of just running it (e.g. running it with extra logging, or doing some kind of dry-run, etc.)