# Purity vs Effects
In Haskell there is a clear separation, which is applied through the
typing system and the compiler, between pure code: it is always evaluated
with the same output value given the same input and does not cause any side
effects such as mutation of mutable objects or output to I/O devices; and code
that produces effects:
| Parent calls child | Parent with effects | Parent Pure |
|---|---|---|
| Child with effects | Code with effects | Compiler error |
| Child pure | Code with effects | Pure code |
In some cases, to increase performance, this clear separation can somehow be
bypassed with referential transparency. For example:
λ> import System.IO.Unsafe
λ> reftrans = unsafePerformIO $ pure =<< getChar
λ> :t reftrans
λ> reftrans :: Char -- IO effects can't be seen in the signature
When this happens, we can no longer see the side-effects in the function
signatures and the type system and compiler, can’t no longer help us.
Note: All Haskell applications have a parental code branch with input and output I/O effects. If this were not the case, we could not provide inputs or see the output of the calculation and, therefore, it would be a waste of time to execute any application
# Isolation and granulation
In Haskell, the bridge that is responsible for binding the pure code in
combination the with code containing effects, is called monads.
Monads are structures that represent calculations defined as a sequence of
steps.
As mentioned earlier, this bridge can do so gradually allowing us to make
sure that if we allow a part of the code that can access the network, you can
only perform that side-effect and not others.
In addition, we also want to provide the possibility to exclude packages that
can’t be registered as trusted. This is achieved by introducing the concept of
restricted effects, as described in the article [Safe Haskell], to make sure
that only a minimum number of effects can be used. An example combining both
granulated as well as restricted effects can be seen in the following lines of
code:
- First we define the monadic effect we want to granulate and we mark the module
as
Safecode:
{-# LANGUAGE Safe #-}
--------------------------------------------------------------------------------
module Uniprocess.Effects.Granulated (..) where
--------------------------------------------------------------------------------
class (Monad m) => CmdM m where
out :: String -> m ()
- Next, we define the restricted effects layer as described in
[Safe Haskell]and we mark this module astrusted. As we don’t expose the constructorRestrictedIO, we need to implement the restricted instance of the monadic effect that we want to granulate:
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
--------------------------------------------------------------------------------
module Uniprocess.Effects.Restricted ( .. ) where
--------------------------------------------------------------------------------
newtype RIO a = RestrictedIO { rio :: IO a }
instance Functor RIO where
fmap f m = RestrictedIO $ f <$> rio m
instance Applicative RIO where
pure = RestrictedIO . pure
(<*>) f m = RestrictedIO $ rio f <*> rio m
instance Monad RIO where
return = RestrictedIO . return
(>>=) m f = RestrictedIO $ rio m >>= rio . f
--------------------------------------------------------------------------------
class Granulated.CmdM m => CmdM m where
out :: String -> m ()
instance Granulated.CmdM RIO => CmdM RIO where
out x = RestrictedIO $ putStrLn $ x ++ " [Restricted]"
- Now, we will be able to granulate the restricted effect by simply implementing the granulated monadic effect from the first step:
{-# LANGUAGE Safe #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
--------------------------------------------------------------------------------
module Uniprocess.Effects.Granulated.Instances where
--------------------------------------------------------------------------------
instance Granulated.CmdM Restricted.RIO where
out x =
Restricted.out $ x ++ " [Granulated]"
- And finally, we can now limit our application, to only use our subset of
effects, by restricting the signature of the only function that the
mainis bound to:
{-# LANGUAGE Trustworthy #-}
--------------------------------------------------------------------------------
module Main (main) where
--------------------------------------------------------------------------------
isolated :: Eff.RIO ()
main :: IO ()
--------------------------------------------------------------------------------
isolated = Uni.process
--------------------------------------------------------------------------------
main = Eff.rio . isolated
- The logic which is called by our application, can therefore be placed in a
Safemodule of code, which ensure that no undesired side-effects can be performed.
{-# LANGUAGE Safe #-}
--------------------------------------------------------------------------------
module Uniprocess.Process ( process ) where
--------------------------------------------------------------------------------
process :: ( Restricted.CmdM m ) => m ()
--------------------------------------------------------------------------------
process =
Granulated.out "Uniprocess with isolated (and granulated) side-effects."
This approach is well known in information security and computer science as
principle of least privilege (PoLP) where a process, a user, or a program
(depending on the subject) must be able to access only the information and
resources that are necessary for its legitimate purpose. Haskell, among
very few, can enforce this at compile-time.
Therefore, it is very easy to ensure that the design and architecture will
be applied throughout the entire application.
It will also be easy to see for the experts and maybe even for the users, that
the application really does what it was designed to do.
And if someone tries to modify the application, with bad intentions, it will
require major changes in the design and architecture, which can be easily spotted.
Thanks to the isolation of effects, it would be enough for companies to design the effects layers and outsource the development to anyone, even the
best black-hat hackers, with the necessary knowledge, knowing that the code they receive will comply 100% with their initial design.
Talking about how to do things right and thus ensure data protection by
design and by default.
Note: And the best thing is that you do not have to believe in my word, you just have to trust a piece of technology that is based on solid foundations of Mathematics and Computers Science
[Safe Haskell]: (David Terei, David Mazières, Simon Marlow, Simon Peyton
Jones) Haskell ’12: Proceedings of the Fifth ACM SIGPLAN Symposium on Haskell,
Copenhagen, Denmark, ACM, 2012