“Effect Tracking Is Commercially Worthless” argues that enforcing purity is not commercially valuable, and this struck a chord with with me because I had already recently started to question the usefulness of enforcing purity in the context of a commercial project CodeWyng that I’m working on.
Looking past the distractions (the tongue-in-cheek sales rhetoric, the conspicuous advertisement for the ZIO project, and the vacuous is-Haskell-really-pure debate), there are some compelling arguments against enforcing purity that I’ve never heard before. The novelty of the counterarguments has drawn my attention to studying this subject further.
Because “effect tracking” is ambiguous and causes confusion, these terms might be useful in discussion:
IO Int
in a function like sum :: [Int] -> Int
) and the specific interface to IO
exposed to the programmer (i.e. all functions that interact with the world return something wrapped in IO
and there is no IO a -> a
to circumvent the intended interface, except for the backdoor unsafePerformIO
).runApp :: DB -> IO ()
, adding a type class constraint runApp :: (HasDB m, MonadIO m) => m ()
, or mutating the global DB variable to set it to a mock implementation prior to running tests.As an aside, I don’t think John is saying that the dependency-injection-like property of effects systems is worthless, but rather that any restriction-of-side-effects property (just like IO
has) is worthless.
Original motivation for IO
in Haskell: ensure that expressions that describe interactions with the world are evaluated in a deterministic order (specified by the programmer) even under lazy evaluation and in the presence of an optimizing compiler, while maintaining purity. Paper for reference: Imperative functional programming
Mechanism by which IO
enforces purity: the IO
data type is essentially the state monad where the world is the state data IO a = IO (World -> (a, World))
and the >>=
+return
interface exposed to the programmer only enables chaining IO
values together such that each value of type World
is used exactly once, passing it from one IO
action to the next in sequence. In other words, each function that takes a World
(e.g. getLine :: IO String
, getLine :: World -> (String, World)
) is never called with the same World
value twice. All function calls that interact with the world take a different World
value, and therefore it’s impossible to observe the same function applied to the same arguments returning a different value. That’s precisely the definition of referential transparency, A.K.A. purity.
Motivation for enforcing purity: I found less information on this topic, so I’m mostly going off of intuition here.
Motivation for not enforcing purity: Starting with John’s reasons, with a few I thought of:
this
and let
vs var
in JavaScript), immutable annotations (e.g. const
vs let
in JavaScript), option chaining (e.g. C#’s and TypeScript’s ?.
operator), the Maybe
data type (e.g. Optional
in Java), parser combinators, QuickCheck, STM, list comprehensions, pattern matching, parametric polymorphism (A.K.A. generics), type inference, the list goes on…java.net.URL#hashCode/equals
let
vs <-
, doing acrobatics with <$>
/fmap
/=<<
/>>=
/<*>
/etc. to satisfy the type checker, the need to add an import of Debug.Trace
and use special functions to be able to inspect the execution of functions that definitely don’t interact with the world, and the need to create a new variable binding for many trivial values such as count <- readIORef countRef
)IO
is or how it works, just use these combinators you won’t understand for the first 20 hours of learning Haskell” is Haskell’s equivalent of “oh just ignore all that public static void main String[] args
stuff for now and don’t forget to wrap your hello world in a class”array.reverse()
which both mutates the array and returns a reversed shallow copy.Experiment with strict impure Haskell To get another data point, I’m considering modifying the Haskell backend of CodeWyng to use LANGUAGE Strict
and insert unsafePerformIO
in a bunch of places, especially at usages of IORef
s and MVar
s to see 1) if it works 2) if there are any bugs 3) if it’s easier or more intuitive to program that way. Will post back here if/when I do. Update:
{-# LANGUAGE Strict #-}
worked right away, I didn’t notice any bugsIO
functions with impure non-IO
versions, I replaced withMVar
with an impure version and doing so caused an infinite loop, so I reverted that onex <- readMVar xVar
unlessM
broke in unexpected ways>>=
/<-
/<$>
/=<<
to retain purity are completely unnecessary”Value of enforcing purity in CodeWyng The first version of the part of CodeWyng that is now written in Haskell was originally written in TypeScript, and I switched exclusively for Haskell’s vastly superior runtime support for concurrency and the Haskell ecosystem’s stream parsing libraries. I definitely didn’t switch in order to prevent myself from inadvertently mixing pure/impure code, and if it were to be rewritten in some other language (TypeScript/Go/etc.), I wouldn’t consider the loss of enforcement of purity a downside.
Conversation on Zulip