It’s my contention that effect tracking is worthless precisely because if a developer has any idea about what a function is intended to do, then they already know with a high degree of certainty whether or not the function performs side-effects
Just blogged, 'Effect Tracking Is Commercially Worthless', in which I introduce…
:boom:Tagless-Final Effect-Tracked Java™:boom:
…and thoroughly reject use of 'IO' to track purity in functional code. :scream::smiling_devil:
Get it while it's hot! :point_down:
#scala #haskell #zio #fp
https://degoes.net/articles/no-effect-tracking
If the premise is that "effects are not about tracking side-effects", then I sorta agree - just IO by itself isn't that helpful if all the code you write is inside of it and effects being just "tags" wouldn't be that useful either - but effect systems aren't really about "tagging side-effects", they're more about clean composition of state/control flow, restriction of effects in specific contexts and sort of "parametrization" over implementation similar to dependency injection - that's sort of what author is saying about ZIO. Reason why multiple current solutions in Haskell are not really beginner-friendly and have their own problems is simply that these goals are hard to achieve and what "impure" languages basically do is that they "sweep them under the rug" by using one, big, shared "context". And as your program grows, interactions between different parts of code in this shared environment may become less and less clear...
Question is - if global shared state is fine, why do we care so much about encapsulation, separation of concerns or DI? What if we had model that not only let's us use convention to achieve them, but can prove that they hold and so some classes of bugs can't occur?
I don't really understand this article. The premise establishes that "effect tracking," is a problem we're wasting time on and the conclusion claims "effect tracking" is a misnomer. It also claims that the interesting and valid research being done in this area by both academics and industry users is... a myth?
There is much said about IO but... aren't algebraic effects about interpreters and free(r) monads? I thought the whole point of algebraic effects was that I could write a function that is polymorphic over some monad so long as it gives me the expected operations.
I think the commercial viability argument is even weaker. "Java would have implemented them by now," is a reductionist argument if I ever heard one. The worst thing you can say about the "commercial viability" of algebraic effects is that there's not enough data to say one way or another if they make developers more productive or reduce errors. Which isn't saying much. We also don't have enough data to know if static type systems have a statistically significant effect on error rates in software... even unit testing, a common practice, has very little evidence for this.
Showcasing how the Polysemy library can be used to implement a REST application conforming to the guidelines of the Clean Architecture model. - thma/PolysemyCleanArchitecture
Looking past the distractions (the tongue-in-cheek sales rhetoric, the conspicuous advertisement for his 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.
Just to clarify, by "enforcing purity" I'm referring to modeling effects in the type system. The term is intended to be contrasted with languages that allow side-effects in functions and how programmers still write pure functions in them because purity has value in those languages, too (e.g. function plus5(x) { return x + 5; } in JavaScript).
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: https://www.microsoft.com/en-us/research/wp-content/uploads/1993/01/imperative.pdf
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 return 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.
STM is guaranteed to be correct
Equational reasoning always holds (i.e. factoring out common code into a binding never changes program behavior)
The type of a function documents whether or not it interacts with the world
Programs tend to be written in such a way that interactions with the world are more separated from logic than they would be without enforced purity
There's a pervasive undercurrent of "it's good for you" in a "slavery is freedom" sort of way
It's the only lazy+pure language and a lot of learning has come out of programming with that mind-bending combination of properties (myself included)
Motivation for not enforcing purity: Starting with John's reasons, with a few I thought of:
If it were worthwhile, annotation processors would be in widespread use for mainstream languages and have IDE support (proof by contradiction)
Tooling alone using static call tree analysis would achieve the same goal without placing a burden on the programmer
PureScript went from pure+tags to pure (still enforces purity, but with less granularity)
Almost every other aspect of FP has been widely adopted while enforced purity has not: first-class and anonymous functions (e.g. in Java 8), lexical scoping (e.g. arrow functions for 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...
Programmers already know using intuition with a high degree of certainty whether or not a function interacts with the world. Functions that violations that intuition are avoided and are quickly rectified or worked around (e.g. java.net.URL#hashCode/equalshttps://news.ycombinator.com/item?id=21765788
Enforcing purity places burdens of boilerplate and ceremony on programmers (e.g. when to use let vs <-, doing acrobatics with <$>/fmap/=<</>>=/<*>/etc. to satisfy the type checker, 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, need to create a new variable binding for many trivial values such as count <- readIORef countRef)
Telling beginners "don't worry, you don't need to understand what 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 you hello world in a class"
I can't remember ever accidentally running IO at all or in the wrong order in any language (not just Haskell). It just doesn't seem to happen. I've been bitten by mutation quite a few times, but they were always a conscious choice of tradeoffs or something silly like JavaScript's array.reverse() which both mutates the array and returns a reversed shallow copy.
I'm considering modifying the Haskell backend of CodeWyng (commercial project I'm working on http://codewyng.io/) to use LANGUAGE Strict and insert unsafePerformIO in a bunch of places, especially usages of IORefs and MVars 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.
I'm having trouble following the argument that anything that hasn't been widely adopted by mainstream languages can't be commercially valuable. does that mean that lambdas only became commercially valuable once C++, Java etc. adopted them, and they had no value in the '80s? was the Maybe monad only valuable once C# introduced option chaining? and so on with the other features in the list. maybe purity will never be valuable, but maybe nobody has figured out the right way to introduce it to mainstream languages yet?
I don't know much about OCaml, but maybe the fact that people programming in an impure language sometimes use monads and have syntax extensions for do-notation suggests that purity and modelling your effects does have value in some situations
@Chris Wendt interesting, I didn't think anything in the post was particularly new. The usual argument I am familiar with is:
"OK, we accept that pure functions are useful. However, I can still have pure functions in JavaScript. But in Haskell the tracking/enforcing in the type system has such a high cost in terms of understanding (how to deal with all the ways things get weird). Is it really worth it? What value does that provide?"
There are a myriad answers to this, but I think the one that is most compelling to me personally, after having learned how to do it, it is really a nice way to program. Experientially it is a handy practice. And once you know how to work with IO, for example, the burden to deal with it is not high.
I'm considering modifying the Haskell backend of CodeWyng (commercial project I'm working on http://codewyng.io/) to use LANGUAGE Strict and insert unsafePerformIO in a bunch of places, especially usages of IORefs and MVars 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.
@Chris Wendt I'd love to see the follow up post if you end up following this plan! Also, I have a (possibly stupid) question:
Would LANGUAGE Strict be required for the whole code base or only restricted to modules that perform IO?
something I've noticed, in langs without effect tracking, if you do try to enforce pure/impure divide, the impure code grows and grows, becoming a mix of effect logic and what could be pure logic that is handy in the effect functions.
@Joel McCracken it's worth noting that this is also easily possible with Haskell - you end up with "IO blobs" where most of your code lives in IO (e.g. while "prototyping")
the benefit for me here is that when you want to actually "purify" your functions, it takes no effort to see where you're doing IO, whereas without the IO tag you would need to make a conscious choice while initially writing your IO blob to somehow document which parts are impure (or invest the effort later to check everything manually)
that is, if you say "oh ive already done this", finding it in an IO function, you can't just extract that little portion as a separate funciton on IO becuse that limits you to only use in IO functions
I've been bitten by impure IO in languages that don't have that concept. Multithreaded code in C++ is an absolute nightmare unless someone was smart enough to use monoids and enforce immutability... and be really, really careful.
in every impure language code base I have ever worked, I always feel the need to read every line of code basically for every function I need to call, and then transitiviely read every line of the functions called... because things are too ambiguous and its way too easy to throw IO in the middle of something
if everything gets inlined away, yeah the unsafePerformIOs would indeed be a problem (as is the usual problem with inlining unsafePerformIOs encountered when wanting to have a global MVar for example)
I'm considering modifying the Haskell backend of CodeWyng (commercial project I'm working on http://codewyng.io/) to use LANGUAGE Strict and insert unsafePerformIO in a bunch of places, especially usages of IORefs and MVars 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.
Oh, CodeWyng is interesting. What sort of GitHub API do you use for this if you don't mind me asking?
One thing I didn't see mentioned is that restrictions like purity make mathematically modeling the language a tractable problem. The less a language can do, the more connections there are to logic/category theory/algebra/etc. Of course this doesn't mean we can't do things like state modification etc. at all, it just means we need to maintain some kind of boundary between what is "within" the language (and hence must be accounted for by a mathematical model) and what is "without".
Purity is just one restriction of this kind btw. For example it may seem tedious and annoying to lose the ability to arbitrarily duplicate or discard values in our language (at least without some kind of explicit effect demarcating code that does this from the language itself). But what we lose in power _in_ the language, we gain in power _on_ the language, in that there are many optimizations that are sound for a linearly typed program but not for one that creates or destroys values willy nilly.
Tractable and sufficiently easy for practicing programmers. Even C and Javascript have been formally modelled but it takes significantly more effort to do.
https://degoes.net/articles/no-effect-tracking
It’s my contention that effect tracking is worthless precisely because if a developer has any idea about what a function is intended to do, then they already know with a high degree of certainty whether or not the function performs side-effects
https:
I completed my experiment with LANGUAGE Strict and unsafePerformIO on my commercial project CodeWyng:
Adding {-# LANGUAGE Strict #-} worked right away, I didn't notice any bugs
While replacing IO functions with impure non-IO versions, I replaced withMVar with an impure version and doing so caused an infinite loop, so I reverted that one
I found some dead code that was doing the equivalent of x <- readMVar xVar
Inlining variables into pure expressions helped align the code and prettify it
Some control functions like unlessM broke in unexpected ways
All I could think about for the last 25% was "man, all these tricks with >>=/<-/<$>/=<< to retain purity are completely unnecessary"
Honestly I'm worried this will break at some point - All Strict does is putting BangPatterns everywhere, so while it enforces strict evaluation, it language where bindings are not guaranteed to be evaluated in any specific order it may introduce some troubles
Plus there's stuff like let-floating which can move bindings to completely different places AFAIK
And all of the base is still lazy, so unless you switch to one compiled with Strict (that may possibly need some fixes too to work), you're going to have problems with standard functions/datatypes
Enforcing purity places burdens of boilerplate and ceremony on programmers (e.g. when to use let vs <-, doing acrobatics with <$>/fmap/=<</>>=/<*>/etc. to satisfy the type checker, 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, need to create a new variable binding for many trivial values such as count <- readIORef countRef)
This is only the case because Haskell lacks a built-in algebraic effect system. Boiler plate is not fundamental to the design space (see Unison Abilities).
The issue with giving up effect tracking is not just that it makes STM unsound, it also makes lazy data structures unintelligible (even in embedded in a strict language).
In fact the issue is even more general you lose everything that requires algebraic reasoning. All the good things listed about ZIO are only possible because of effect tracking. Look at Unison for example how could you distribute arbitrary effect full computations across multiple servers without fine-grained effects? To say effect tracking is commercially worthless, but ZIO is useful as the article does is a oxymoron.
I'm asking because the article claims that ZIO has the benefits you mentioned, but "without effect tracking". So since you said that that was unsound, I assume that ZIO must implement something that satisfies the same operational requirements.
I'm asking because the article claims that ZIO has the benefits you mentioned, but "without effect tracking". So since you said that that was unsound, I assume that ZIO must implement something that satisfies the same operational requirements.
It seems that the term "effect tracking" is ambiguous and causing us to talk past one another, so I propose we use these two terms instead:
Enforcing purity prevents the use of functions that interact with the world to be used in any context that doesn't allow it. The mechanism by which purity is enforced in Haskell is a combination of the type system (e.g. you can't use 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).
Dependency injection is a technique where the implementation of dependencies of a function can be changed at the call site. For example, the dependency on the database can be swapped out for a mock implementation in tests. Some mechanisms by which dependency injection can be performed include: passing an explicit argument 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.
Do these terms capture the meanings being used in this conversation?
Type classes are a poor mechanism for expressing effects. This is why effects are difficult in Haskell. Type classes are great when you want coherence, but are not the right fit for effects. Languages like Unison and it's forbearer Frank have effect inference, which allows you not to write out all your constraints, improves error messages and makes tracking affects first class.
Enforcing purity is tracking affects. You tack a effect system on ala ZIO analogous to typescript or flow, you can build a affect system on GADTs, type classes, and monads, or you can use a single monads at a time (concretely transformers, or abstractly MTL).
All ZIO does is track the affects you care about in a nice-ish way. If you track only the effects that affect concurrency mutation and exceptions as the article argues, you lose the ability to care about more effects later. In a strict language you gain less boiler plate and the ability to still think imperatively. In a lazy language, you can't sequence effects without tracking.
But languages don't need to trade off fine grain tracking for ergonomics as Unison demonstrates.
The following program defines two ability: State s and Error e. The Error e ability is row-polymorphic on the effects it can receive in its actions. use .base use .base.Either ability State s where...
Following up on "examples of bug that could have been prevented using effect tracking", I could not find the example I referred to. It would be nice to get concrete examples of bug that could have been prevented with an effect tracking system. However, even if such system does not prevent bugs directly, it seems like very useful for code review where change to non-IO function would take less time to review.
And from my limited understanding, anything that could improve code review is hardly worthless because code review can be tricky and very error prone on its own :-)
I'd be interested in more retrospectives on effect systems. Semantic had a neat one. For my own projects I have tried converting some mtl code to capabilities but I haven't gotten very far with it yet.
the fact that the Semantic Code team spends the majority of its time working on features rather than debugging production crashes is truly remarkable—and this can largely be attributed to our choice of language.
This quote here is amazing. This is one of those selling points of Haskell that needs to be pointed out more often
Nice point, everything would be worthless if the human being never made mistakes and always remembered what he was thinking when he wrote that piece of code :stuck_out_tongue:
As soon as you say “I’m okay with impure functions”, you’re choosing a default set of effects (such as mutability, I/O, and exceptions 😱) that are easiest to work with, and as a consequence, support for other effects either fall by the wayside (suddenly async effects have to be expressed differently, e.g. via callbacks or promises in JS, which then got syntactic sugar added for them) or they get subsumed by the default impure effects (e.g. state will often be tracked using mutability, even though that’s not always the best implementation, especially if you need backtracking, asynchronicity, or other effects that might interact with it in subtle ways). And one consequence of this subsumption of most effects into the default set is that they can no longer be disentangled, limiting your ability to think about them separately or refactor them neatly. So by choosing to not set impure functions as the default, Haskell does endorse tracking effects to a point where we can talk about effects separately and have tools to work with them fairly uniformly (Monad/Applicative classes and MTL, do notation, traverse, etc.), although it does still bless a set of default effects in the runtime with IO and STM (which interestingly are async, unlike most other languages whose impure functions are all sync).
You could probably set up a similar strawman saying that: Documentation is worthless. It doesn't help commercially. You can just figure out what the code does every time. Who needs it? Well, I say I do :)
I think a descriptive type system is really useful for documentation purposes. You could argue that if all of your top level type signatures don't lie it doesn't matter if you sprinkle in a few unsafePerformIO. I'd still disagree with that for the same reason I use semicolons when writing javascript. 99% of the time it doesn't cause problems, but when it does it's really painful, and typing a few extra characters doesn't waste that much time considering I spend most of my time debugging not typing.
https://degoes.net/articles/no-effect-tracking
https://twitter.com/jdegoes/status/1257022839443554312
Just blogged, 'Effect Tracking Is Commercially Worthless', in which I introduce… :boom:Tagless-Final Effect-Tracked Java™:boom: …and thoroughly reject use of 'IO' to track purity in functional code. :scream::smiling_devil: Get it while it's hot! :point_down: #scala #haskell #zio #fp https://degoes.net/articles/no-effect-tracking
- John A De Goes (@jdegoes)If the premise is that "effects are not about tracking side-effects", then I sorta agree - just
IO
by itself isn't that helpful if all the code you write is inside of it and effects being just "tags" wouldn't be that useful either - but effect systems aren't really about "tagging side-effects", they're more about clean composition of state/control flow, restriction of effects in specific contexts and sort of "parametrization" over implementation similar to dependency injection - that's sort of what author is saying about ZIO. Reason why multiple current solutions in Haskell are not really beginner-friendly and have their own problems is simply that these goals are hard to achieve and what "impure" languages basically do is that they "sweep them under the rug" by using one, big, shared "context". And as your program grows, interactions between different parts of code in this shared environment may become less and less clear...Question is - if global shared state is fine, why do we care so much about encapsulation, separation of concerns or DI? What if we had model that not only let's us use convention to achieve them, but can prove that they hold and so some classes of bugs can't occur?
I don't get what the goal is in that article, sounds just like a string of conflated semantics and terminology
I agree this post is not very concise
Isn't "effect tracking" a term invented in the blog post itself?
I don't really understand this article. The premise establishes that "effect tracking," is a problem we're wasting time on and the conclusion claims "effect tracking" is a misnomer. It also claims that the interesting and valid research being done in this area by both academics and industry users is... a myth?
There is much said about
IO
but... aren't algebraic effects about interpreters and free(r) monads? I thought the whole point of algebraic effects was that I could write a function that is polymorphic over some monad so long as it gives me the expected operations.I think the commercial viability argument is even weaker. "Java would have implemented them by now," is a reductionist argument if I ever heard one. The worst thing you can say about the "commercial viability" of algebraic effects is that there's not enough data to say one way or another if they make developers more productive or reduce errors. Which isn't saying much. We also don't have enough data to know if static type systems have a statistically significant effect on error rates in software... even unit testing, a common practice, has very little evidence for this.
I read that one reason for his getting banned from typelevel was that he kept on debating nonsensical problems in github issues
ґїззщшгнекуцй
I find this to be relevant: https://github.com/thma/PolysemyCleanArchitecture#readme
That's an awesome showcase
I have something not so well structured but I think is worth to share
https://github.com/bolt12/generic-crud
Looking past the distractions (the tongue-in-cheek sales rhetoric, the conspicuous advertisement for his 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.
Just to clarify, by "enforcing purity" I'm referring to modeling effects in the type system. The term is intended to be contrasted with languages that allow side-effects in functions and how programmers still write pure functions in them because purity has value in those languages, too (e.g.
function plus5(x) { return x + 5; }
in JavaScript).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: https://www.microsoft.com/en-us/research/wp-content/uploads/1993/01/imperative.pdfMechanism by which
IO
enforces purity: theIO
data type is essentially the state monad where the world is the statedata IO a = IO (World -> (a, World))
and the>>=
+return
interface exposed to the programmer only enables chainingIO
values together such that each value of typeWorld
is used exactly once, passing it from oneIO
action to the next in sequence. In other words, each function that takes aWorld
(e.g.getLine :: IO String
,getLine :: World -> (String, World)
) is never called with the sameWorld
value twice. All function calls that interact with the world take a differentWorld
value, and therefore it's impossible to observe the same function applied to the same arguments return 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
andlet
vsvar
in JavaScript), immutable annotations (e.g.const
vslet
in JavaScript), option chaining (e.g. C#'s and TypeScript's?.
operator), theMaybe
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
https://news.ycombinator.com/item?id=21765788let
vs<-
, doing acrobatics with<$>
/fmap
/=<<
/>>=
/<*>
/etc. to satisfy the type checker, need to add an import ofDebug.Trace
and use special functions to be able to inspect the execution of functions that definitely don't interact with the world, need to create a new variable binding for many trivial values such ascount <- 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 you hello world in a class"array.reverse()
which both mutates the array and returns a reversed shallow copy.I'm considering modifying the Haskell backend of CodeWyng (commercial project I'm working on http://codewyng.io/) to use
LANGUAGE Strict
and insertunsafePerformIO
in a bunch of places, especially usages ofIORef
s andMVar
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.I'm having trouble following the argument that anything that hasn't been widely adopted by mainstream languages can't be commercially valuable. does that mean that lambdas only became commercially valuable once C++, Java etc. adopted them, and they had no value in the '80s? was the
Maybe
monad only valuable once C# introduced option chaining? and so on with the other features in the list. maybe purity will never be valuable, but maybe nobody has figured out the right way to introduce it to mainstream languages yet?I don't know much about OCaml, but maybe the fact that people programming in an impure language sometimes use monads and have syntax extensions for do-notation suggests that purity and modelling your effects does have value in some situations
@Chris Wendt interesting, I didn't think anything in the post was particularly new. The usual argument I am familiar with is:
"OK, we accept that pure functions are useful. However, I can still have pure functions in JavaScript. But in Haskell the tracking/enforcing in the type system has such a high cost in terms of understanding (how to deal with all the ways things get weird). Is it really worth it? What value does that provide?"
There are a myriad answers to this, but I think the one that is most compelling to me personally, after having learned how to do it, it is really a nice way to program. Experientially it is a handy practice. And once you know how to work with IO, for example, the burden to deal with it is not high.
@Chris Wendt I'd love to see the follow up post if you end up following this plan! Also, I have a (possibly stupid) question:
Would
LANGUAGE Strict
be required for the whole code base or only restricted to modules that perform IO?something I've noticed, in langs without effect tracking, if you do try to enforce pure/impure divide, the impure code grows and grows, becoming a mix of effect logic and what could be pure logic that is handy in the effect functions.
@Joel McCracken it's worth noting that this is also easily possible with Haskell - you end up with "IO blobs" where most of your code lives in IO (e.g. while "prototyping")
the benefit for me here is that when you want to actually "purify" your functions, it takes no effort to see where you're doing IO, whereas without the IO tag you would need to make a conscious choice while initially writing your IO blob to somehow document which parts are impure (or invest the effort later to check everything manually)
yeah so it def happens, but whats nice is that you have to factor it out if you want to use that logic in a pure context
that is, if you say "oh ive already done this", finding it in an IO function, you can't just extract that little portion as a separate funciton on IO becuse that limits you to only use in IO functions
I've been bitten by impure IO in languages that don't have that concept. Multithreaded code in C++ is an absolute nightmare unless someone was smart enough to use monoids and enforce immutability... and be really, really careful.
and yeah what you're saying is :100:
isn't the scenario the article is advocating more like "what if Polysemy but using
unsafePerformIO
in the interpreters"?in every impure language code base I have ever worked, I always feel the need to read every line of code basically for every function I need to call, and then transitiviely read every line of the functions called... because things are too ambiguous and its way too easy to throw IO in the middle of something
Like, tell me,
reservation.reserveTickets(10)
, do you think this does IO?absolutely
i agree, but is it clear one way or the other?
how do you know its not implemented purely?
the "I know intuitively which function performs IO" does not hold for transitivity
yeah
I think the claim is that if you wrap every IO action with a (polysemy) effect constructor,
IO
becomes pointlessit feels like this should be easily contradictable, maybe @Love Waern (King of the Homeless) can make a sound argument
a naive argument would be "then
Sem
replacesIO
"but since everything gets optimized away when compiling, would everything just come crashing down?
if everything gets inlined away, yeah the
unsafePerformIO
s would indeed be a problem (as is the usual problem with inliningunsafePerformIO
s encountered when wanting to have a globalMVar
for example)Chris Wendt said:
Oh, CodeWyng is interesting. What sort of GitHub API do you use for this if you don't mind me asking?
@Chris Wendt Good analysis, I think you should make this into a blog post or something
One thing I didn't see mentioned is that restrictions like purity make mathematically modeling the language a tractable problem. The less a language can do, the more connections there are to logic/category theory/algebra/etc. Of course this doesn't mean we can't do things like state modification etc. at all, it just means we need to maintain some kind of boundary between what is "within" the language (and hence must be accounted for by a mathematical model) and what is "without".
Purity is just one restriction of this kind btw. For example it may seem tedious and annoying to lose the ability to arbitrarily duplicate or discard values in our language (at least without some kind of explicit effect demarcating code that does this from the language itself). But what we lose in power _in_ the language, we gain in power _on_ the language, in that there are many optimizations that are sound for a linearly typed program but not for one that creates or destroys values willy nilly.
Tractable and sufficiently easy for practicing programmers. Even C and Javascript have been formally modelled but it takes significantly more effort to do.
tl;dr: YouTube - Constraints Liberate, Liberties Constrain — Runar Bjarnason
I included some insights from the discussion here and posted https://chrismwendt.github.io/blog/2020/05/27/the-cost-of-enforcing-purity-in-haskell.html
@Chris Wendt Zulip link doesn't render correctly. By the way, you may want to link to https://funprog.srid.ca/haskell/effect-tracking-is-worthless.html instead as it doedsn't require login to view messages.
Thanks, fixed the link, it's live now
I completed my experiment with
LANGUAGE Strict
andunsafePerformIO
on my commercial project CodeWyng:{-# LANGUAGE Strict #-}
worked right away, I didn't notice any bugsIO
functions with impure non-IO
versions, I replacedwithMVar
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"Added these notes to https://chrismwendt.github.io/blog/2020/05/27/the-cost-of-enforcing-purity-in-haskell.html
Honestly I'm worried this will break at some point - All
Strict
does is puttingBangPatterns
everywhere, so while it enforces strict evaluation, it language where bindings are not guaranteed to be evaluated in any specific order it may introduce some troublesPlus there's stuff like let-floating which can move bindings to completely different places AFAIK
And all of the
base
is still lazy, so unless you switch to one compiled withStrict
(that may possibly need some fixes too to work), you're going to have problems with standard functions/datatypesHonestly, if you want to do haskell like that, I think it would be better to use Rust or OCaml
@Chris Wendt do you have any traversals in your codebase?
over IO I mean
Haskell definitely wasn't intended to be used like this, and it's not a huge pain point, so I'm not going to actually leave it strict and impure
Not sure what you mean by traversals, I have a few
forM_
s andsequence
sThose are what I meant. Do those interact properly with
unsafePerformIO
?Yes, as far as I could tell, I didn't notice a problem with them
This is only the case because Haskell lacks a built-in algebraic effect system. Boiler plate is not fundamental to the design space (see Unison Abilities).
The issue with giving up effect tracking is not just that it makes STM unsound, it also makes lazy data structures unintelligible (even in embedded in a strict language).
In fact the issue is even more general you lose everything that requires algebraic reasoning. All the good things listed about ZIO are only possible because of effect tracking. Look at Unison for example how could you distribute arbitrary effect full computations across multiple servers without fine-grained effects? To say effect tracking is commercially worthless, but ZIO is useful as the article does is a oxymoron.
@Avi Dessauer does that mean that your assessment is that ZIO's "runtime" implements a similar mechanism as Haskell's
IO
?The stronger the tracking the better libraries.
IO
is insufficient. I'm not assessing ZIO I'm describing why tacking is needed.I'm asking because the article claims that ZIO has the benefits you mentioned, but "without effect tracking". So since you said that that was unsound, I assume that ZIO must implement something that satisfies the same operational requirements.
I'm asking because the article claims that ZIO has the benefits you mentioned, but "without effect tracking". So since you said that that was unsound, I assume that ZIO must implement something that satisfies the same operational requirements.
That's not my interpretation of the article.
ZIO is a effect system tacked on to Scala it's not as sound as it would be in Haskell or Unison, but it's an affect system.
https://zio.dev/docs/overview/overview_creating_effects
It seems that the term "effect tracking" is ambiguous and causing us to talk past one another, so I propose we use these two terms instead:
IO Int
in a function likesum :: [Int] -> Int
) and the specific interface toIO
exposed to the programmer (i.e. all functions that interact with the world return something wrapped inIO
and there is noIO a -> a
to circumvent the intended interface, except for the backdoorunsafePerformIO
).runApp :: DB -> IO ()
, adding a type class constraintrunApp :: (HasDB m, MonadIO m) => m ()
, or mutating the global DB variable to set it to a mock implementation prior to running tests.Do these terms capture the meanings being used in this conversation?
Dependency injection is expressible as an instance of algebraic effects.
Enforcing purity is another instance of effects.
Type classes are a poor mechanism for expressing effects. This is why effects are difficult in Haskell. Type classes are great when you want coherence, but are not the right fit for effects. Languages like Unison and it's forbearer Frank have effect inference, which allows you not to write out all your constraints, improves error messages and makes tracking affects first class.
Enforcing purity is tracking affects. You tack a effect system on ala ZIO analogous to typescript or flow, you can build a affect system on GADTs, type classes, and monads, or you can use a single monads at a time (concretely transformers, or abstractly MTL).
All ZIO does is track the affects you care about in a nice-ish way. If you track only the effects that affect concurrency mutation and exceptions as the article argues, you lose the ability to care about more effects later. In a strict language you gain less boiler plate and the ability to still think imperatively. In a lazy language, you can't sequence effects without tracking.
But languages don't need to trade off fine grain tracking for ergonomics as Unison demonstrates.
Unison abilities look dope :sunglasses:
I should look back into its progress again
I was just reading about that last night, they do look really interesting
they don't work very well
they suffer from all of the problems effect handlers do
https://github.com/unisonweb/unison/issues/822
@Sandy Maguire that's very good to know.
Could eff's handler order independent approach be adopted by Unison?
Following up on "examples of bug that could have been prevented using effect tracking", I could not find the example I referred to. It would be nice to get concrete examples of bug that could have been prevented with an effect tracking system. However, even if such system does not prevent bugs directly, it seems like very useful for code review where change to non-IO function would take less time to review.
And from my limited understanding, anything that could improve code review is hardly worthless because code review can be tricky and very error prone on its own :-)
I'd be interested in more retrospectives on effect systems. Semantic had a neat one. For my own projects I have tried converting some mtl code to capabilities but I haven't gotten very far with it yet.
To follow up on the chat about effects at the video chat this last saturday, Semantic made a pretty interesting point about their use of Haskell and effect systems: https://github.com/github/semantic/blob/master/docs/why-haskell.md
I'm also interested in seeing what types of bugs are reduced by effect tracking, and fine-grained effect tracking in particular.
This quote here is amazing. This is one of those selling points of Haskell that needs to be pointed out more often
Lol the whole write up is amazing. That document needs to be pushed more often as a response to "Haskell has no killer applications"
I find myself referring to it a lot. I find parallels in my own adoption of Haskell as well.
wrt: what bugs are reduced by effect tracking, i don't think that's the goal
the goal is that it forces you to think through your architecture much more than you would otherwise
you don't prevent bugs; you prevent unmaintainability
I use this same line of reasoning when explaining formal methods. :smiley:
I also don't totally get why effect tracking being worthless is different from saying that type systems are worthless
Nice point, everything would be worthless if the human being never made mistakes and always remembered what he was thinking when he wrote that piece of code :stuck_out_tongue:
writing good code is commercially worthless, otherwise companies would have picked up on it
This assumes that companies in 2020 are all-knowing and perfectly wise
But anyway i think he is correct in so far as effect tracking is not directly worthy; it is valuable for its side effects however :P
As soon as you say “I’m okay with impure functions”, you’re choosing a default set of effects (such as mutability, I/O, and exceptions 😱) that are easiest to work with, and as a consequence, support for other effects either fall by the wayside (suddenly async effects have to be expressed differently, e.g. via callbacks or promises in JS, which then got syntactic sugar added for them) or they get subsumed by the default impure effects (e.g. state will often be tracked using mutability, even though that’s not always the best implementation, especially if you need backtracking, asynchronicity, or other effects that might interact with it in subtle ways). And one consequence of this subsumption of most effects into the default set is that they can no longer be disentangled, limiting your ability to think about them separately or refactor them neatly. So by choosing to not set impure functions as the default, Haskell does endorse tracking effects to a point where we can talk about effects separately and have tools to work with them fairly uniformly (Monad/Applicative classes and MTL, do notation, traverse, etc.), although it does still bless a set of default effects in the runtime with IO and STM (which interestingly are async, unlike most other languages whose impure functions are all sync).
You could probably set up a similar strawman saying that: Documentation is worthless. It doesn't help commercially. You can just figure out what the code does every time. Who needs it? Well, I say I do :)
I think a descriptive type system is really useful for documentation purposes. You could argue that if all of your top level type signatures don't lie it doesn't matter if you sprinkle in a few unsafePerformIO. I'd still disagree with that for the same reason I use semicolons when writing javascript. 99% of the time it doesn't cause problems, but when it does it's really painful, and typing a few extra characters doesn't waste that much time considering I spend most of my time debugging not typing.