3

Have you found side-effects a problem by increasing complexity in your code base...

 2 years ago
source link: https://lobste.rs/s/if8hle/have_you_found_side_effects_problem_by
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Have you found side-effects a problem by increasing complexity in your code bases?

Given that “Out of the Tarpit” says side-effects begat complexity, it doesn’t seem like most people outside of functional programming are concerned with it as a problem.

I’m wondering if it’s because it’s actually not a problem, and “Tarpit” is wrong, or if it is right, but it’s too hard to think about programming without state. What are your thoughts?

  1. koreth

    edited 29 hours ago

    | link

    Yes, but usually not in the functional-programming sense of the term “side effects.”

    On a lot of projects, I see easily an order of magnitude more problems caused by business-logic side effects than mutable-state side effects. The requirements get increasingly complex and as the code grows and evolves to meet them, different parts of it start to interact in ways that nobody anticipated.

    In these kinds of cases the code is performing exactly according to spec, and there isn’t any inconsistency in internal state from a data-integrity point of view. Operation A triggers operation B which triggers operation C which triggers operation D which messes up the result of operation A. Each of those triggers is correct, but the system is huge and the people who specified and implemented operation A didn’t stop to think about the C->D relationship because it seemed to have nothing to do with A.

    That’s not to say I never run into problems with “state mutated unexpectedly when I called this function” kinds of side effects. I do. But they are usually a very small percentage of the total number of problems I need to fix, and the cost of changing to a programming paradigm that makes them categorically impossible needs to be pretty close to zero to be a worthwhile tradeoff.

    My hunch is that people’s answers to this question will vary dramatically based on what kinds of projects they work on. If you are working on massively-parallel data processing pipelines with a ton of cross-thread communication, you’re going to have a different perspective on this than if you’re working on an enterprise web app that mostly exists to translate HTTP requests into complex SQL queries.

    1. plr

      edited 22 hours ago

      | link

      Out Of The Tar Pit categorizes complexity into accidental and essential.

      On a lot of projects, I see easily an order of magnitude more problems caused by business-logic side effects than mutable-state side effects.

      I believe that’s essential complexity in the ontology, and so it’s just part of the business you’re in, not a problem OOTTP seeks to address. Over-complex business practices notwithstanding, your “That’s not to say” paragraph makes your opinion clear. ;)

      My hunch is that people’s answers to this question will vary dramatically based on what kinds of projects they work on

      That has me nodding.

    2. iamwil

      edited 23 hours ago

      | link

      I can see how in a pipeline of operators A -> B -> C -> D, that even if it was all functional, you get “cross-talk” if D somehow munges the result A came up with downstream, esp if you didn’t consider it.

      Do you get a sense of why this happens? It sounds like it’s a coordination problem outside of code. Since there isn’t a clear separation of concerns between teams, or if it’s necessarily coupled, it’s easy for one person to munge the results of another’s. And this happens because no one person understands the entire pipeline, and only has a local view to what they’re working on?

      Based on your answer, and pkolloch’s, it seems like the request/response cycle + database allows you not to worry much about side effects.

      Also, thanks for the hunch, because I’m also beginning to think maybe the view depends on the type of work you do, and the shape of the problem space you work in, and the design of the systems that you have to interact will might dictate more, the influence of mutable side effects in the problems you encounter.

      1. Isaacy

        22 hours ago

        | link

        It’s the same problem as side effects in code, just at a higher level. It’s can be worse because the people “writing” the business logic “code” (rules?) are often not programmers so they less frequently have the vocabulary to talk about much less think about this without stepping through it each time.

        At some point your product becomes a virtual machine for a low-code/no-code runtime, sometimes with a poorly defined grammar and a language with sketchy logic rules

  2. plr

    edited 22 hours ago

    | link

    I’ve observed in industry that programs exist in only a couple of states with respect to maintainability:

    1. New programs which are maintainable because the person who wrote them (or so) is around.
    2. Old programs which are maintainable, despite the authors being absent, because they control and isolate side effects.
    3. Old programs which aren’t maintainable because they use side effects instead of function calls to communicate across parts of the code base.

    The third category is the most common because it is easier to add a pointer to two data structures than do a proper investigation and refactoring when a programmer is tasked with maintenance tasks but not given enough time. It’s the gravity of maintaining software at low cost to end up with badly written software. Programs in the first two categories fall into the third category over time.

    A language which controls side effects will push against this trend by forcing maintainers to build more explicit and maintainable a control flow, with clearer ownership of data. OOTTP is in my opinion very accurate.

    1. iamwil

      19 hours ago

      | link

      A language which controls side effects will push against this trend by forcing maintainers to build more explicit and maintainable a control flow, with clearer ownership of data. OOTTP is in my opinion very accurate.

      So my question is, if that’s the case how come more programmers in imperative languages don’t try to migrate to languages that make more explicit demands to have maintainable control flow or a clearer ownership of data?

      1. plr

        18 hours ago

        | link

        Learning the new model is difficult. It’s like relearning programming from scratch. I programmed in php, java, python and c for about 7 years before learning scheme, lisp and then Haskell and Rust. It wasn’t until Haskell that I had to do anything different. And it was very different. I have about three years of angry tweets complaining about Haskell, but I’d never go back. Using Haskell for 10 years now and it’s great. Learning Rust after Haskell was easy because it’s the best of both worlds.

  3. x64k

    25 hours ago

    | link

    Yes, and this is one of the reasons why pretty much every prototype I build is about as free of mutable state as it gets – as in, I try to have as little state as I can without bending backwards to remove it. That way, adding new features is bogged down by a lot fewer bugs, and I only waste spend time optimising code that’s worth optimizing.

    Depending on what kind of program I’m writing though, that amount differs tremendously. For many applications it’s basically everything except for some shared configuration data. For others (games, I guess, are the prime example?) it’s practically zero.

    I think most people outside functional programming understand that just fine. The reason why you don’t hear too many of us saying “it’s a problem” is the “without bending backwards to remove it” part. No reality modeling tool is “a problem”. Sometimes, mutable, shared state really is the best tool you have, or the only one you have. Obstinately avoiding it for a bunch of general reasons isn’t very productive.

  4. pyj

    23 hours ago

    | link

    it’s too hard to think about programming without state.

    State exists in all programs, in functional languages, it’s just embedded ephermally in the current computation–think of how church numbers encode state, but “aren’t state.” The lack of mutable state and side-effects does help reasoning about programs in general.

    it doesn’t seem like most people outside of functional programming are concerned with it as a problem.

    It is, and languages deal with with it in different ways, it just doesn’t usually receive explicit treatment. A lot of just “good engineering” follows practices to remove it, and some built-in techniques are so old now that people don’t even recognize them as mechanisms to help. Pass-by-name has nearly disappeared, being replaced by pass-by-value and pass-by reference, often with constant parameters. Goto is rarely used anymore. Usage of global data has been significantly constrained compared to the past, and encapsulation helps constrain who can modify what, though transitively it can be an issue. Value objects are now often used as well.

    C++ has a construct called const-correctness aimed at explicitly removing cases where state changes. Whereas it has move semantics, Rust has formalized this notion, to the neat idea of being able to describe “destroyed” (moved) state within a computation. Most people think of Rust’s borrow checker as a strength because of memory safety, but miss the notion of how it constrains programs to consider state and mutability in general at compile-time. Along with its notion of “immutable by default,” this is a huge understated advantage it has over other languages in problem solving, especially in constraining side-effects.

    To constraint state, Ada allows types to embed range information, invariants, and runtime-checked predicates. It also allows you to create distinct types with different semantics but the same underlying representation. This considerably reduces the runtime-allowed value range and performs runtime-checks to ensure they’re met. The SPARK subset of the language takes this further by allowing data dependency contracts describing which parameters must and can only affect which values, and allow specifying of any (forms of) global state may be used by a function. Also, pre/post conditions of Ada, along with predicates and invariants are formalized in SPARK and used in formal verification of correctness. A lot of other languages include an assertion mechanism which is often used for precondition checks.

    There’s a lot of techniques to deal with state and side-effects in imperative languages. One of the benefits of the functional programming community is that it provides more visibility to this important concept which sometimes isn’t discussed enough.

    1. iamwil

      19 hours ago

      | link

      It is, and languages deal with with it in different ways, it just doesn’t usually receive explicit treatment. A lot of just “good engineering” follows practices to remove it,

      Do you have a sense of why engineers wouldn’t want to give it explicit treatment? After a while, good engineering seems to get codified as a way to do generational transfers of knowledge, so I’d think engineers would move towards explicit. Perhaps we are wary of running head long into any technology that would constrain us as a rule, rather than an option?

      Thanks for the survey of these mechanisms.

      One of the benefits of the functional programming community is that it provides more visibility to this important concept which sometimes isn’t discussed enough.

      This gets at the core of my question. If there’s benefit to the visibility of this concept, why isn’t it discussed more outside of FP circles? You’ve suggested that there are enough ways for people to deal with it through language features, than any explicit banishing of mutable state, and maybe that’s enough for most people?

      1. pyj

        16 hours ago

        | link

        Do you have a sense of why engineers wouldn’t want to give it explicit treatment?

        State gets normally treated as a necessary evil and problems from it are usually described in terms of the mechanisms themselves to combat the problems. Past a specific point, it seems like removing state adds complexity back in or introduces inefficiency.

        If there’s benefit to the visibility of this concept, why isn’t it discussed more outside of FP circles? You’ve suggested that there are enough ways for people to deal with it through language features, than any explicit banishing of mutable state, and maybe that’s enough for most people?

        FP changes the primitive operations used during coding from explicit operations to things like filter, map, zip, fmap, take, etc. most of which a lot of imperative languages already have adopted. There’s also often a lot of hoop-jumping to do straightforward things when you try to remove all side effects. It’s a lot to throw out and new things to adopt, when programs eventually need to have to have some sort of side effect like writing to a network socket.

        I’ve run into cases where switching from pure functions to explicitly reusing resources (especially memory) have given program speedups of 10x in hot path code. In Haskell you can also have things like accumulation of thunks (iirc) which can result in high memory usage, e.g. I’ve seen a program reading a 200 MiB file hit 2 GiB of memory used during the parse due to this–the solution was beautifully elegant though.

        In general, the problem isn’t as straightforward as “remove all of the stateful things!” Different industries have standard languages and libraries in use as part of their ecosystem, and not using them can remove your efficiency and make you uncompetitive, or make it difficult to hire.

        There’s enough experience and techniques to deal with state to make it manageable (but not perfect). We’re seeing more functional elements used, but many people still seems to remember how “OOP all of the things!” turned out, so I think people are being more judicious in adoption of FP. We’ll probably see continued integration of FP concepts, especially as these concepts are more available to newer programmers, but they’ll just be grafted into imperative languages, Rust would be a prime example.

  5. tazjin

    27 hours ago

    | link

    Yes, but not in a program that is standing still.

    What I mean is that in my experiences, mutability and lack of static checks becomes a maintainability concern. Most codebases, even if they heavily rely on things like global state, will always kind of be in a state where they work - but at the cost of being unable to do large changes.

    1. iamwil

      24 hours ago

      | link

      To summarize, you mean a bigger problem than mutable state and side effects is not being flexible enough to make large changes?

      1. tazjin

        17 hours ago

        | link

        No, I think they lead to large-scale changes being hard/impossible because you end up in situations where you touch some innocent looking thing and then the whole application segfaults.

  6. It is hard to answer objectively. Also most programming style guides at least partially agree by discouraging global state and advocating some measures to restrict mutations through visibility,…

    For me, checking what state a part of a program consumes and changes is often a good first step to understanding it. I have found that first loading all needed data, then perform the logic and calculate necessary changes works well for business logic. The middle part is then usually nicely testable.

    I don’t think that I am far off the programming mainstream with that opinion so in the environments I worked in, there was some awarenes. So, it wasn’t often a problem. But maybe because it is a known problem that is avoided.

    In UIs confusing mutable state seems to be much more common than in backend software, in my limited experience.

    1. iamwil

      24 hours ago

      | link

      I see. Because backends are usually structured in a flow where: you get data from the db -> run it through some business logic in the controller -> render it, that you don’t see the issues of mutable state causing issues as much. That’s because any state that matters is coming from the DB, and most of the issue about writes are taken care of for you. In addition, in a request/response cycle of the web, most of the intermittent state can be thrown away.

      I can also see UIs wresting with mutable state. In fact, one of React’s main benefits is that it alleviates us from having to program in retained mode.

    2. plr

      22 hours ago

      | link

      Also most programming style guides at least partially agree by discouraging global state

      Have you worked in any golang codebases recently? Global state seems to be encouraged by patterns in the stdlib and commonly used third party libs: global logger struct, global “once” cells, etc.. Used judiciously it doesn’t cause problems, but many reach for it unthinkingly and go programs often end up write-once.

      1. No, didn’t work on golang code bases to any meaningful extend so that is interesting.

        I wondered how larger code bases in golang turned out since it superficially encourages a relatively simple but imperative coding style. Would be interested to read more about it but it is such a hard topic because it is by nature pretty subjective which programs are easy to understand or extend.

        1. plr

          18 hours ago

          | link

          My experience is that larger golang codebases require cohesive teams with low turnover to maintain properly. A secondary helpful thing is to have agreed-upon conventions for how state is managed and isolated in the codebase.

          We had several successful large scale projects at twitch. We also had several failed projects. The projects that failed usually failed because the person who best understood how the effects were managed was no longer with the company, and the project didn’t follow the conventions for effects which was already established. Those codebases became write-once. Nobody tried to understand them. Eventually they were rewritten from scratch.

          Languages which encourage good patterns for managing and isolating effects will push authors toward conventions which encourage maintainability.

  7. kornel

    edited 22 hours ago

    | link

    Conversely, reduction of side effects has made code simpler to reason about, and easier to test. Problematic side effects happen mainly when the code can affect some external objects (like singletons), which usually need to be set up beforehand and are an implicit context. OTOH side-effect-free code tends to only look at its direct inputs, which makes it clearer what the code depends on, and may be easier to set up in tests. And when the context is difficult to explicitly provide, it’s often a red flag, and an opportunity to rethink the design. Easier testability also has a positive effect on reliability and enables refactoring of the code with lower risk of regressions, which in turn delays the time until the codebase will be called “legacy” and die.

  8. akkartik

    edited 13 hours ago

    | link

    I’ve responded to you in a few places, and I find I still have more to say. It’s a large subject. I’ll try to summarize a list of opinions that I think are internally consistent and explain all observed phenomena.

    • Most people who know of “Out of the Tarpit” have not fully understood it. It’s a long paper, really two papers in one. The first half is a (weak) case against state, and the second half is a (very interesting) proposal for a new model of programming. I didn’t realize until last year, on like my sixth attempt at reading it, that the second half is not about Conal Eliot’s Functional Reactive Programming, in spite of sharing the same acronym. I wish they’d called it the MM system, for Moseley-Marks.

    • The fact that nobody programs in the MM system (there’s more to it than state) is evidence that it’s difficult to get people to try out new systems, nothing else.

    • The value of statelessness in large-scale software is overblown. The value seems entirely to be: it encourages people to think about architecture early. But you can get that in other ways. If you put off architecture planning as your system grows complex, you lose. That’s really the fundamental game of software. You can win playing it in C, you can lose it playing in React.

    • Languages like Haskell and Elm have value beyond statelessness.

    • When it comes to thinking proactively about architecture in large-scale software, social considerations vastly trump technical ones.

    • I think you can achieve a lot (anything?) with software while keeping code size and team size small. That way minimizes second-order social turbulence and allows you to focus on getting the architecture right. Minimizing the number of insiders also gives you more space to focus on the people actually using and affected by the software.

  9. The problem that side effects create is that they reduce observability. However the overall reduction varies between languages, frameworks, and what other tooling you use.

    My experience with trying to bring concepts like immutability, pure functions, or even managed effects into a team is that most of the teams I’ve been on don’t believe half of what I’m saying. They understand the existence of immutability and the idea of pure functions, but actually using them and having it be a gain instead of a loss to productivity seems implausible to them.

    I can kind of understand why they think this. Having recently started relearning C after nearly 15 years of not writing it, the tooling today feels a lot better than I remember it being. It feels better because it gives me more e information about what my program is doing, and it provides it earlier. Similarly, web development has gotten easier over the years because the browser’s have provided better debugging that allows the developer to see and interact with the state of their program. Allowing them to see where side effects are effecting their code.

    Whether or not this is a problem depends on where you’re standing. The person who creates a new app at a company and then moves on to the next greenfield project likely never sees the difference. The person who comes in after them and maintains the work only is aware of the difference if they’ve been exposed to other possibilities. If they’re ignorant of other possibilities then they may just accept that this is how things are done.

    1. iamwil

      19 hours ago

      | link

      They understand the existence of immutability and the idea of pure functions, but actually using them and having it be a gain instead of a loss to productivity seems implausible to them.

      Admittedly, it takes work to relearn other ways of thinking about programming. And due to the names like monad in FP, it probably doesn’t help.

      That’s an interesting perspective. Maybe even though the language model is lacking, we’ve made up for it with better tooling with insight into observability of the state of our programs. So it may be possible that while there’s a gap between better language model vs better tooling, it may not be as big as to warrant a language switch.

      My instinct is that there probably is a limit to the tooling, depending on the problem domain. Distributed systems comes to mind, where reasoning with imperative style and mutations just doesn’t cut it. But then if you’re never doing that sort of stuff, and instead mostly dealing with business logic, I can see that maybe you don’t think you’d need it.

  10. it doesn’t seem like most people outside of functional programming are concerned with it as a problem

    think about it as a self-selecting group; people that think raw pointers are a bad idea also stop using C

  11. iamwil

    23 hours ago

    | link

    While “Out of the Tar Pit” is cited often, it doesn’t seem to have much influence on mainstream programming. I started to wonder why. Because if its claim that mutable state is a large contributor to program complexity is true, and we all recognized it as such, there’d be more effort to tamp it down. But I don’t really see it in practice.

    I get the sense that people in pure functional programming languages accept this, and they have their ways of dealing with side effects. Elm asks you to send side effects as commands to the runtime, where the runtime executes it for you. Haskell has monads to construct representations of side effects as commands/data, and the underlying runtime executes it when it gets to it. Clean uses Linear Types, so that you only ever get to use a variable once, and Koka, Eff, and Unison use Algebraic Effects, which also split up the asking for the result of a side effect from the execution of a side effect. But the users of FP are smaller in number (though imperative languages have started pulling concepts from FP more and more).

    In the imperative world, there doesn’t seem to be as much attention paid to it, with the exception of small pockets here and there. I’m not sure exactly why, other than, “Side effects are where the work happens”. But I do see some attempts to root it out for people that have been burned by it.

    The hexagonal architecture is about compartmentalizing parts of the system that perform side effects, like fetching, and talking to the database to a limited part of the code base, and everything else is a pure function of that data, making it maintainable and testable.

    As mentioned in a different thread, UIs have a tangle of states. It was difficult to keep your application state in sync with the DOM state. So React came along and insisted that application programmers views the components as a pure function of state. This view found widespread adoption in the web front end world, because simplified the syncing of application state to DOM state. Game programmers have found this to be true too, and call it immediate mode (as opposed to retained mode). (Fun fact: React Hooks take much inspiration from Algebraic Effects)

    Even then, I don’t see a lot of concerted effort on this front. I suspect that side effects are a problem, but most people just deal with it, because a) it’s not a problem in the small so by the time it’s a problem, it’s too late! b) imperative style is still the best known form to express temporal logic for human comprehension.

    Mutating state is an easy way for us to think, so we gravitate towards it. It’s unnatural for us to think about everything as a value (a la FP) or everything as declared (a la prolog) in certain domain problems.

    I had wanted to get other people’s thoughts on it, and what your experience has been given the problem domains that you work in.

    1. Mutating state is easy to understand at a small scale. It is easy to understand at a larger scale with a huge amount of structure and discipline and boundaries preventing disasters.

      That discipline and those boundaries will always have cracks, though, after team movements and time and deadlines. We just keep building more walls between systems to stop the rot spreading.

      Microservices are a good example. I work with microservices and they’re great for my org, but one (just one) of the reasons they fit is that our teams write code in languages that encourage mutable state all over the place, and we need a way to wall that off.

      It sounds like you’re saying that languages that make state more explicit or compartmentalised as part of their design are not (as) popular because … if their paradigms were indeed better, they would be more popular. There does seem to be a feeling among many developers who’ve worked with different paradigms that we would all be better off going ‘pure functional’, and a slight bafflement that we haven’t.

      I think the answer is, as ever, not black and white, but your suggestion that it’s to do with how easy things are seems reasonable. I would be inclined to be even more simplistic about it, however, and suggest that really we just use what we can learn and use quickly, even if it means we end up having to build guards around it later to deal with our mess.

      That’s not to say we currently have terrible languages. C# (and .NET) and JavaScript (and npm and node) have evolved to provide a huge amount of power to developers, with an incredibly small learning curve for those just getting started - and reasonable paths to doing things in ways that make you extremely productive.

      If we keep making more ‘non mutating’ languages, frameworks and platforms user friendly, easy to learn and powerful, though, I don’t see a reason why we might not see a decent proportion of code written in something like Elixir or Elm within the not too distant future.

    2. akkartik

      edited 13 hours ago

      | link

      I’m not convinced that side effects are what separates large messy codebases from small clean ones. A better answer seems to be architecture. Games can be clean because the right architecture has been figured out to a far greater extent by trial and error. Architecture is a sense of what subsystems are allowed to talk to each other. If you figure that out, state management becomes a detail. If you instead focus on how React is stateless or something, you’re more likely to allow any subsystem to talk to any other. And you end up with spaghetti React, which seems quite common.

    3. Game programmers have found this to be true too, and call it immediate mode (as opposed to retained mode).

      Point of fact: in gamedev we’ve known about this for quite some time, going back to at least the 90s.

      IIRC the React folks were partially inspired by one of the idTech engines.

  12. dfa

    11 hours ago

    | link

    I hit a handful of state bugs at work in the last two weeks. One is a tree invariant not holding, one is a dangling reference, one is a null deref. The first wouldn’t be solved by a pure PL, the second would, the third would at least not crash the server.

    Some PLs have separate syntax for effectful computation. It’s similar to the distinction between a “statement” and “expression.” The distinction at the syntax level is carried into the type level.

    The “Modernized Algol” and “Assignable References” chapters of “Practical Foundations for Programming Languages” lay out a couple small example languages that do this. My understanding is that the “bnd” command in the book corresponds with Haskell’s “do” notation.

Moderation Log Hats Tags

Wiki Privacy About


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK