We all want our code to be safe/secure and efficient, among other things. This is especially true for any code dealing with blockchain. We don’t want a race condition to cause double-spending, neither do we want our system to leak sensitive data or keep the user in suspense for hours while his/her transaction is being processed. In other words, we seek a balance between two contradictory sets of requirements: safety/security on one side and performance on the other.
And so do the big players in blockchain industry like Block.one, Coinbase, or Cumberland. However, from a certain point of view, the technologies they address the challenge with don’t seem ideal: it’s mostly C++ and Ruby. And then there is IOHK; they based their blockchain solution on Haskell; and Haskeller’s is precisely the point of view from which those technologies don’t seem to be the best fit for the task.
Let me elaborate on the point made above. What Haskell offers and the mainstream technologies don’t is a principled way to track side effects, like writing to shared memory or sending data over network, without affecting run time performance. More precisely, the side effects your program performs are encoded in types so that the compilation entails finding a proof that your code performs only those side effects specified in the types. Think of the types as a specification and the compiler as a technology which is there to prove you conform to the specification. Since it all happens during compilation, this checking incurs no run time penalty.
In Haskell, there are basically two approaches to type-level tracking of side effects: monad transformers (explained here) and algebraic effects. In this and the following post, I will focus on the latter since it offers many advantages over the former (see Kiselyov, Sabry & Swords (2013)).
I will illustrate the idea of algebraic effects on a Haskell function with the following specification
makeFileWriter :: FilePath -> FileWriter
The function takes path to a directory and returns a compound algebraic effect:
type FileWriter = Eff (StateC Integer (Eff (TraceByPrintingC (Eff (LiftC IO))))) ()
The type tells us that the effect consists of mutating an integer variable, logging to the standard output, and doing some I/O. Note that this tells us nothing about the time ordering of the effects; it merely says that from the myriad of possible side effects out there only those three are allowed in the output of our function.
With the specification in place, let’s try not to conform to it (punk is alive!):
makeFileWriter dir = tell "Smoke me a kipper, Mama, I'll be back for breakfast."
Sorry, no talking to your Mama during the working time, says the Glorious Glasgow Haskell Compiler:
Seems like punk is dead after all and we can but conform:
makeFileWriter dir = foldl1 (>>) $ replicate 5 $ do (i :: Integer) <- get let filePath = makeFilePath dir i trace $ "Will write " ++ filePath ++ "." liftIO $ writeFile filePath ("You've just opened " ++ filePath ++ "!") trace $ "Wrote " ++ filePath ++ ".\n" modify (+ (1 :: Integer)) where makeFilePath dir i = dir ++ "/" ++ show i ++ ".txt"
Now, the compiler is happy but you probably aren’t since you have no idea what the function does! Let’s dissect it line-by-line, beginning with the second:
(i :: Integer) <- get
Here, the current value of the integer variable is read.
let filePath = makeFilePath dir i
Here, we construct a path to a file we are going to write to soon (see the ‘where’ clause on the last line of the function definition).
trace $ "Will write " ++ filePath ++ "."
Here, we log our intention to write to the file.
liftIO $ writeFile filePath ("You've just opened " ++ filePath ++ "!")
Here, we actually write some useful stuff to the file.
trace $ "Wrote " ++ filePath ++ ".\n"
Here, we log that we’re finished writing to the file.
modify (+ (1 :: Integer))
Finally, here we increase the value of the integer variable by one, since we are going to run all this again (incidentally, 5 times in total, see the first line of the function definition).
Now it’s time to take a break and reflect on what we’ve accomplished so far; we’ve a function which, given a path to a directory, returns an effectful computation that conforms to the specification. But wait; how do we actually run the computation? Like this:
writeFiles :: FileWriter -> IO () writeFiles = runM . runTraceByPrinting . evalState (1 :: Integer)
writeFiles (makeFileWriter $ "test" </> "data")
If the effectful computation were an onion and the constituent side effects its skins, each of the three functions from which
writeFiles is composed would peel off one layer of the skins until only a bare, runnable onion remains (whatever that means). This idea of separating the definition of an effectful computation (
makeFileWriter) from its implementation (
writeFiles) is the true essence of algebraic effects.
Before concluding this post, a technical remark is in place; there are several alternative implementations of algebraic effects in Haskell. Here, we used fused-effects, which seems to have an edge on the other implementations performance-wise (see this benchmark). For completeness, here is the full import list:
import Control.Effect (Eff, LiftC, runM) import Control.Effect.Carrier (Carrier) import Control.Effect.State (State, StateC, get, put, modify, evalState) import Control.Effect.Trace ( Trace, TraceByPrintingC, trace, runTraceByPrinting) import Control.Monad (mapM_) import Control.Monad.IO.Class (liftIO) import System.IO (writeFile)
For me, writing Haskell is like driving my Mitsubishi Lancer; what my vanilla Lancer lacks in power compared to the Evo, it makes up for in maintenance costs. Similarly, what Haskell lacks in expressive power compared to Python, it makes up for in costs of handling bugs in production.
In this post, we’ve given Haskell an eco run; in the next, we will floor it.