Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> in imperative languages (with meta-programming) you can disentangle mixed IO and logic even without changing the code

I don't understand what you mean by "mixed", "disentangle" and "change".

I would say "mixed IO and logic" is when one block of code describes both how to calculate some value and which side-effects to perform. "Disentangling" these would mean separating the description of the calculation from the description of the side-effects. Doing that "without changing the code" doesn't make sense to me.



What I mean is that if your code (or a library you call) mixes logic with IO, meta-programming lets you capture the IO calls in a mock (like turning it into an IO monad), which you can then query and verify, without changing the code and the API's it's using.

E.g. on the JVM you do that with Mockito (similar mocking libraries exist for other languages/runtimes). Say you have a (silly) method that does computation and IO:

    void printDouble(int x, PrintStream out) {
        out.println("Result: " + (x * 2));
    }
You can then do:

    @Test
    public void testPrintDouble() {
        PrintStream out = mock(PrintStream.class);
        
        printDouble(3, out);

        verify(out).println("Result: 6");
    }
No output is actually done. The call to println (which may be deep in some library printDouble calls rather than directly in the tested code). We've essentially replaced any object that does direct IO with something akin to an IO monad.

We can query the mock in more sophisticated ways, too. Here's a very contrived example:

    void printDoubleWithLog(int x, PrintStream out, PrintStream log) {
        log.println("Computing...");
        out.println("Result: " + (x * 2));
        log.println("Done.");
    }

    @Test
    public void testPrintDoubleWithLog() {
        PrintStream out = mock(PrintStream.class);
        PrintStream log = mock(PrintStream.class);
        
        printDouble(3, out, log);

        InOrder ordered = inOrder(out, log);
        ordered.verify(log).println("Computing...");
        ordered.verify(out).println("Result: 6");
        ordered.verify(log).println("Done.");
    }
This contrived example also verifies the relative ordering of IO done on two separate streams. Again, no API change is necessary, and the actual IO calls may be deep in some library.


OK, so you're on about a physical/temporal separation of IO and logic: the logical calculations can be performed separately to the IO actions, and vice versa.

I view that as pretty trivial though; it's just a case of switching out the language's IO primitives, which are effectively free variables. With most "large" languages (Java, Haskell, etc.), the implementations are usually gnarly enough to make such substitution a significant engineering accomplishment, but that's an artefact of the situation, not the approach. As an analogy, the task of fitting corrective optics to the Hubble space telescope was only difficult because it was the Hubble space telescope; fitting adaptive optics is trivial enough that bespectacled children do it every day.

In light of this, I think that disentangling mixed-up definitions is a much more important and interesting problem. It's non-trivial to extract pure computations from imperative code (whether they're written in Java, or Haskell, or whatever), yet there is a great potential benefit for improving code, in terms of understandability, modularity, re-use, maintainence, etc. The fact that pure code is easier to test, or that side-effects can be substituted for the purposes of tests, is really a minor point in comparison. They are workarounds for running code which has too many responsibilities. Much better to avoid the problem in the first place by writing separate code for separate concerns.


Yes, I agree that it's better to write good code than bad code :)

Clear separation of concerns is an important aspect of good code, and every language teaches this practice. I don't think Haskell has any advantage there.

I also think -- and this is probably a point of contention -- that the way Haskell separates pure computation from side-effects is a bit arbitrary. Haskell defines computation in a way similar to lambda calculus, which designates as impure any change to something which may then affect a function's result not through its arguments. In short, it equates the mathematical notion of the function with the software notion of the subroutine. Haskell subroutines are mathematical functions, period (well, except for unsafe etc.). I don't think that this is the best way to describe code, or to define separation of concerns (It also makes Haskell jump through hoops when it deals with things that are natural for computation but don't fit well with the notion of a mathematical function). A good separation of concerns in Haskell is not necessarily the best organization for Java/Python.


What do you think would be a better way to enforce this separation of concerns? (genuinely curious)


I don't know, other than good supervision by an experienced developer :) This one, though, isn't that good, IMO. Perhaps future effect systems will show us the way.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: