# 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
Safe
code:
{-# 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
main
is 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
Safe
module 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