[HN Gopher] Effect Systems vs. Print Debugging: A Pragmatic Solu...
       ___________________________________________________________________
        
       Effect Systems vs. Print Debugging: A Pragmatic Solution
        
       Author : degurechaff
       Score  : 79 points
       Date   : 2025-09-22 17:54 UTC (3 days ago)
        
 (HTM) web link (blog.flix.dev)
 (TXT) w3m dump (blog.flix.dev)
        
       | svieira wrote:
       | I wonder if this will wind up being a category of problems and
       | the solution is a separate "system" set of effects (effectively
       | `({user}, {system})`) or if this one-off extension is all that
       | will be needed.
       | 
       | Either way, extremely well explained both in motivate and
       | implementation!
        
         | epolanski wrote:
         | I think the solution is to not have side effects in pure
         | functions really.
         | 
         | At some point you're composing your pure functions and _have_
         | to call them by some effectful function (otherwise they're
         | never executed or you're doing computations that aren't
         | consumed by anything).
         | 
         | The only sane alternative is to have a debug effectful variant
         | where you turn off these checks. But then why would `stdout`
         | debugging fine, and not say writing to a different stream or
         | file?
        
         | vilunov wrote:
         | > I wonder if this will wind up being a category of problems
         | and the solution is a separate "system" set of effects
         | (effectively `({user}, {system})`) or if this one-off extension
         | is all that will be needed.
         | 
         | It already is, kinda. In my practice very often you have global
         | singleton values that are either defined as static variables,
         | or passed as arguments to nearly _all_ functions in a module.
         | Since implicit presence of `Debug` effect is already a
         | compilation parameter, it could be generalized to support any
         | sets of implicit effects. Thus you might design a module that
         | has implicit Logger and Database effects in all its functions.
        
           | epolanski wrote:
           | Singleton pattern isn't really used in functional-effect
           | systems, dependency injection is generally used instead.
        
           | yccs27 wrote:
           | > Thus you might design a module that has implicit Logger and
           | Database effects in all its functions.
           | 
           | Logging does seem like a very similar case to debugging, only
           | that you expect to leave it on in production. On the other
           | hand an implicit Database effect kind of defeat the point of
           | an effect system.
           | 
           | I think the key is that Debug and Logger effects don't really
           | affect the rest of the code - if you remove all debug/log
           | statements, the only thing that changes is the debug/log
           | output (and slightly faster execution probably).
        
             | saghm wrote:
             | > Logging does seem like a very similar case to debugging,
             | only that you expect to leave it on in production
             | 
             | In a lot of logging systems, debug is one of the common
             | levels of logging. I'm not even convinced that the term
             | "debug" is unambiguous to clearly refer to something that's
             | not a subset logging. Presumably the difference is intended
             | to mean things printed that are unconditionally going to
             | stdout rather than into some system that might change the
             | output location and filter/annotate things, but I have to
             | imagine that it might just make more sense not to have
             | separate models for them at all
        
         | jerf wrote:
         | The other major use case that leaps to mind is "observability";
         | I want to be able to poke a metric without the function
         | becoming impure, since it is not uncommon for something deep
         | down the call stack to poke a metric and I don't want it to
         | propagate up the stack. You can also make a case for logging
         | that isn't just debug logging. Propagating up a new effect just
         | because some deep function needs to push a log is not very
         | friendly or useful.
         | 
         | Perhaps there is two or three more, but I do think this is a
         | finite set that we can write a language around and just sort of
         | consider them "ambient effects". While metrics and logging are
         | nominally impure, in that they are certainly mutations, if you
         | can't _read_ the logs or the metrics without an effect, you
         | still retain the really important aspect of purity, which is
         | that the pure code can 't cause a change that is observable by
         | that or other code, with the very specific exception of the
         | logging stream and metrics.
         | 
         | I wouldn't be quite ready to put all my chips on this, but I
         | think "the inability to create changes that can be witnessed"
         | is actually the true goal, not "the inability to create
         | changes" with no qualifications. Pure code already necessarily
         | creates changes in a system, the key is that while the CPU
         | registers may change and other parts of the system may mutate,
         | the code can't _witness_ those changes and conditionalize
         | future execution on it. All effects-based systems have already
         | agreed that there are things that are mutations in something
         | real in the physical world they aren 't going to consider
         | effects, adding a couple more categories is not going from 0 to
         | 1 but 12 to 15. It's not a strict purity question but a
         | cost/benefits question.
         | 
         | It occurs to me as I type this that a really ambitious language
         | with a strong enough type system might even be able to turn
         | this into a completely safe proposition, allowing users to
         | declare effects with some sort of very safe "sink" associated
         | with them that the type system checks can not escape out into
         | the rest of the code in any visible way and constraining the
         | visibility somewhere that is contained in the conventional
         | effects system. All these things I'm talking about here and in
         | the blog post are taking the form of values that simply
         | disappear into the ether from the point of view of the creating
         | function. I'd like the language to make it easy to query a
         | function for which ambient effects it uses (as a development-
         | time operation, not a run-time one), and I think that would
         | clean up most of the rest of the practical problems.
        
           | ramses0 wrote:
           | 100% well said! Exactly how to handle log/print/metrics (as
           | results/returns from functions) has been on my mental back
           | burner for years.
           | 
           | It's proper (but goofy) to pass in a `logger` object to every
           | function, but the practicality of plumbing a "logger"
           | everywhere is disgusting. There's a ton of value to be able
           | to "capture" what was logged by a function, but as you
           | mentioned with "metrics", the invasiveness of plumbing a
           | metrics object "through" your code is really gross.
           | 
           | Maybe a straw man syntax like:
           | this.__classHooks__.logger           = new
           | AbstractLogger(...)        this.__classHooks__.metrics
           | = new AbstractMetrics(...)
           | 
           | ...like a puzzle piece, if the container (parent object)
           | wants to "attach" to the exported interface, it can kindof
           | dependency injection, otherwise logs and metrics might fly
           | off into the void.
        
           | sdeframond wrote:
           | Are you saying, essentially, that if we split the "I" and the
           | "O" in "IO", then ignore the the "O" in the way described in
           | the article, the resulting program would be "safe-enough" ?
        
       | throwawaymaths wrote:
       | Honestly a pragmatic solution would be to have printing to stderr
       | (debug print, logs, whatever) not be an "effect". then as an
       | added benefit if its context is elided out in an optimization you
       | know.
        
         | jorkadeen wrote:
         | The challenge is that the compiler uses the type-and-effect
         | system for many of its tasks, including whole-program
         | optimization and code generation.
         | 
         | If printing- or logging statements have no effect, the compiler
         | might reorder them or even remove them.
        
         | maleldil wrote:
         | This is similar to what Haskell has with trace[1]. It pretends
         | to be a pure function (so it doesn't need the IO monad), but
         | prints to stderr.
         | 
         | [1]
         | https://hackage.haskell.org/package/base-4.21.0.0/docs/Debug...
        
           | JoelMcCracken wrote:
           | And, just like tfa alludes, it is awkward because of
           | laziness; in non trivial scenarios it can be quite surprising
           | how your trace isn't tracing.
           | 
           | This is bc of non strict eval in Haskell, but still the core
           | is that the lang is designed to not eval stuff when it's not
           | necessary.
        
         | naasking wrote:
         | It needs to be an effect so the compiler knows how
         | optimizations are permitted to handle it. Otherwise your print
         | statements might appear in totally the wrong order or even not
         | at all.
         | 
         | Imagine saying that certain memory stores are allowed to cross
         | a memory barrier operation, you'd completely lose the ability
         | to reason about concurrency around those stores.
        
       | noreplydev wrote:
       | As I understand, Flix implements a system to type not only a
       | return type but an Effect too. If that's the case, It's not an
       | overkill solution? and why?
        
         | wk_end wrote:
         | It makes it easier to reason about a program. For an dev, you
         | can immediately understand what global state, if any, can
         | affect a function's behaviour. And for the compiler, knowing
         | about what effects an operation has can enable optimizations
         | like dead code removal and autovectorization; the article goes
         | into that.
        
       | podgorniy wrote:
       | Cool idea to "type" side effects
       | 
       | I'm curious what other languages are trying to achieve such?
        
         | jorkadeen wrote:
         | A few other languages are:
         | 
         | - https://effekt-lang.org/
         | 
         | - https://koka-lang.github.io/
         | 
         | - https://www.unison-lang.org/
         | 
         | - https://antelang.org/
        
           | ux266478 wrote:
           | Oh I hadn't heard of Ante before. This looks very close to
           | the language I wanted out of Rust. Haskell's module system,
           | row polymorphism, linear types, no sepples glyph soup. That's
           | an instant bookmark save. Will be watching that space _very_
           | closely.
        
         | ux266478 wrote:
         | Koka[1] jumps to mind as a language built around effect types.
         | Without having used Flex or Koka, I'm not sure how their effect
         | types actually differ from the normal monad gauntlet you get in
         | pure languages.
         | 
         | [1] - https://koka-lang.github.io/koka/doc/index.html
        
         | davidatbu wrote:
         | Koka, Roc-lang come to mind.
        
         | ookdatnog wrote:
         | AFAIK the first somewhat widely-known language to do this was
         | Haskell (through libraries). I'm not 100% clear on the entire
         | history, but I think it goes something like:
         | 
         | 1. Initially there was no way to do effects in Haskell,
         | everything was pure.
         | 
         | 2. Then it was realized that IO can be modeled with monads, so
         | the IO type and do notation were added.
         | 
         | 3. Gradual realization that monads can be used to also
         | constrain effects, ie you can construct a type of "stateful
         | computations" that can read and write to a specific state, but
         | not touch other states or write to disk or something.
         | 
         | 4. Monad transformers are invented, which allow stacking monads
         | on top of eachother to support multiple effects. Together with
         | type classes, this gets us pretty close to extensible effects
         | (the approach used in Flix, if I understand it correctly). So
         | for example you can express that your function needs to write
         | to a log _and_ may exit early with an error message with the
         | constraints `(MonadWriter w m, MonadError e m) = > ... -> m
         | resultType`, and you can then use monad transformers to build a
         | stack that provides both of these effects.
         | 
         | 5. Monad transformers have some issues though: they affect
         | performance significantly and the interaction between effects
         | is tricky to reason about. So an alternative is sought and
         | found in extensible effects. The initial proposals were, iirc,
         | based on free monads, but those aren't great for performance
         | either, so ever since there has been a whole zoo of different
         | effects and handlers implementations that all make different
         | trade-offs and compromises, of which I think the `effectful`
         | library is now the de facto default, and I think what it offers
         | is quite similar to the Flix language's effect system (I'm not
         | sure on what finer points it differs).
        
           | naasking wrote:
           | I don't think performance was a motive for effect systems in
           | Haskell, it was more about making effects easier to compose,
           | understand and debug.
        
             | ookdatnog wrote:
             | I don't think it was a primary motivation, but at least the
             | slowness of mtl is mentioned as one of the motivations for
             | the existence of the library.
             | 
             | https://hackage.haskell.org/package/effectful#what-about-
             | mtl
        
           | tome wrote:
           | > I'm not 100% clear on the entire history, but I think it
           | goes something like:
           | 
           | You can see my talk "A History of Effect Systems" for a
           | synopsys of the history. I gave it at Zurihac this year. It's
           | very close to the history you gave (though I think point 1 is
           | not right: Haskell always had a way to do IO)
           | 
           | https://www.youtube.com/watch?v=RsTuy1jXQ6Y
        
         | anentropic wrote:
         | http://ocamlverse.net/content/future_ocaml.html#typed-algebr...
        
       | ducdetronquito wrote:
       | Pleasant and interesting read !
        
       | vlovich123 wrote:
       | > Hence, when the compiler is run in production mode, we disable
       | the lie that allows the implicit Debug effect. As a result, using
       | dprintln in production mode causes a compilation error.
       | 
       | The team can't seem to make up its minds of the language is
       | intended for high performance or not. They talk about the
       | importance of purity for automatic optimizations but in the real
       | world there's all sorts of practical reasons for needing to debug
       | production compiled code (eg imagine something like a browser and
       | you're trying to figure out some weird behavior that's difficult
       | to catch in a debugger but too slow to reproduce in a debug build
       | or even not reproducible due to different timings resulting in
       | different race conditions)
       | 
       | Also blaming the users of your language for your language not
       | being able to meet their needs isn't a good look. It suggests the
       | language is probably attracting the wrong users or positioning
       | itself incorrectly in the market place.
        
         | jorkadeen wrote:
         | We think that functional programmers should be able to write
         | e.g. `List.count(x -> x > 5, l)` (or e.g. use pipelines with
         | |>) and have it run as fast as an ordinary imperative loop with
         | a mutable variable. The Flix compiler gives them that-- but it
         | requires the program to undergo certain transformations that
         | may require expressions to be moved around and eliminated. It
         | is dangerous to perform such optimizations with incorrect
         | assumptions about types or effects. How to support that
         | together with print-debugging is the challenge.
         | 
         | For systems in production, we have the `Logger` effect and
         | associated handlers.
        
           | vlovich123 wrote:
           | And yet modern optimizers don't actually seem to have a
           | problem with a transformation like that as you must know. Try
           | list.iter().filter(|x| x>5).count() in Rust
           | 
           | And yes, Rust doesn't have an effect system yet, but others
           | have mentioned Haskell and how it handles tracing and logging
           | and the limitations of effect systems interplaying with such
           | things.
        
             | jorkadeen wrote:
             | It was a simple example; whether a specific optimization
             | applies is very tricky. We have to look at the details.
             | When can Rust move or eliminate a binder? Does Rust support
             | automatic parallelization? What happens if you use unsafe
             | blocks to lie to their type and ownership system? I think
             | many of the same issues will surface.
             | 
             | To be me, the interesting question is: What happens when
             | you lie to the type (and effect or ownership) system?
        
               | vlovich123 wrote:
               | > Does Rust support automatic parallelization
               | 
               | No, and there's no indication that automatic
               | parallelization is at all worth the effort vs having the
               | author explicitly annotate which things need
               | parallelization (e.g. Rayon is drop-in for many tasks
               | where you know you'll need it). Otherwise you're at the
               | mercy of heuristics baked into the language which in
               | practice never work out well and also slow down the non
               | multithreaded use-cases.
               | 
               | > To be me, the interesting question is: What happens
               | when you lie to the type (and effect or ownership)
               | system?
               | 
               | As others have said, just having the print get elided if
               | the operation gets optimized out would be fine. That's
               | what Haskell does. It's a weird choice to look at the
               | challenges of effect systems and conclude the effect
               | system idea is perfect it's the programmers who are wrong
               | and not that the effect system has gaps that can't be
               | addressed and solve it in other less surprising ways.
        
         | benschulz wrote:
         | This seems like an uncharitable reading of the post.
         | 
         | > They talk about the importance of purity for automatic
         | optimizations but in the real world there's all sorts of
         | practical reasons for needing to debug production compiled code
         | 
         | I imagine they're talking about their defaults. One can
         | commonly reconfigure how different build profiles work.
         | 
         | > Also blaming the users of your language for your language not
         | being able to meet their needs isn't a good look.
         | 
         | Isn't that what the whole post is about though? They even say
         | the following.
         | 
         | > Returning to earth: we may be academics, but we are trying to
         | build a real programming language. That means listening to our
         | users and that means we have to support print debugging. The
         | question is how?
        
           | vlovich123 wrote:
           | > What do you mean turning off the fuel for the engines
           | crashes the plane? I thought you said this was a safe
           | airplane?!
           | 
           | Things like this - they're painting users of programming
           | languages as the ones being unreasonable.
           | 
           | > I imagine they're talking about their defaults. One can
           | commonly reconfigure how different build profiles work.
           | 
           | From the article:
           | 
           | > We don't want published packages to (a) lie to the type and
           | effect system, or (b) contain print debugging statements > As
           | a result, using dprintln in production mode causes a
           | compilation error.
           | 
           | There is no documentation about the existence of build
           | profiles or how they might work. I think you're reading too
           | charitably.
        
       | meisel wrote:
       | How does Flix decide if it's worth it to auto parallelize a map
       | operation? Eg, if it's a very trivial map operation, it may add
       | overhead to parallelize it
        
       | meisel wrote:
       | Is there a target use case(s) for this language, at least
       | initially? It seems to focus on programming in a platform-
       | independent sense, but where is it most likely to actually
       | succeed? Are there particular OS's or other languages that they
       | try be particularly compatible with?
        
         | wk_end wrote:
         | It runs on the JVM, so I suppose that's the "OS" it's
         | compatible with and Java is the language. It's still mostly
         | academic AFAICT, but I could see experimenting with it for
         | backend stuff.
        
       | matheusmoreira wrote:
       | To do this you made the Debug effect a special case in the
       | compiler. Won't users want to create their own special effect
       | types? Could these special effects be expressed within the
       | language itself instead of being compiler builtins?
        
         | jorkadeen wrote:
         | We can certainly extend it to a language feature-- if there are
         | good use cases.
         | 
         | Do you have some specific special effects in mind?
        
           | morcus wrote:
           | Disclaimer: I don't know much about this programming language
           | or about Effects, so there may be a better way to do this
           | already
           | 
           | something I'd sometimes like to do when I'm profiling complex
           | code will be to have an (essentially) global variable
           | tracking the sum of how long a function took to execute over
           | all invocations.
           | 
           | I am guessing that mutating the global counter would count as
           | an effect, and I wouldn't really want to add the effect all
           | the way through the call graph. I think this is something
           | where the handling ought to be similar to how you're handling
           | Debug.
        
       ___________________________________________________________________
       (page generated 2025-09-25 23:01 UTC)