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

> Handling effects this way would presumably have the same problems?

First, I didn't suggest actually using checked exceptions as they are now to model "typesafe continuations" in Java. I'm perfectly fine with continuations not being fully type-checked (I like types, but I'm far from a type zealot).

But more to the point, such an effect system (or actually even Java's checked exceptions) won't pose such a problem at all. On the contrary. Just like you can't store a monadic value in a variable of a "plain" value, it makes sense to reject `int foo() throws IO` from a computation that doesn't support IO. It is a problem in Java with checked exceptions because `int foo() throws InterruptedException` would work where `int f()` is expected, and having it rejected (and requiring a wrapper) is indeed a nuisance. That's not the same issue if effects are involved.

> I can use the former as a value whereas the latter I can only throw from methods. That's the difference I'm worried about here.

I'm not sure I understand the distinction. You can always use something like Optional (Maybe) if you want a value. Alternatively, no one is stopping you from using the monadic Either even in an imperative context (although you may lose the stack trace). You can still use it this way if that's how you like doing things. I don't.

> then which semantics do I get, and how do I get the other one?

    throwc(new MyException());
    add(Writer.class, "Here");
would never call the writer (like your first example, I believe), while

    tryc(() -> {
           add(Writer.class, "Here");
           ...
        },
    // catch:
        (MyException e) -> 
             add(Writer.class, e));
would log the exception, as in your second example.

You don't even need to think about it. It works just like plain imperative code. Continuations are the general (mathematical if you will) formulation of how imperative code already behaves.



> I'm not sure I understand the distinction. You can always use something like Optional (Maybe) if you want a value.

I mean that the "value or error" that I get back from a function that returns Either is itself a value, that I can use in the normal way I would use a value. I can pull it out into a static field. More importantly I can pass it back and forth through generic functions without them having to do anything special about it (e.g. as a database load callback), because it's just a value. I notably can't do this with exceptions (or e.g. if I'm emulating Writer with a global or ThreadLocal variable), though checked exceptions will at least fail this at compile time.

> would never call the writer (like your first example, I believe)

Hmm. If you're willing to fix the order in which effects happen (and explicitly handle when you want to change it, as in your example) then you can write something much simpler than the usual monad transformers, so make sure you're comparing like with like.


> I can pull it out into a static field. More importantly I can pass it back and forth through generic functions without them having to do anything special about it (e.g. as a database load callback), because it's just a value. I notably can't do this with exceptions (or e.g. if I'm emulating Writer with a global or ThreadLocal variable), though checked exceptions will at least fail this at compile time.

I understand, and as I said, I normally prefer not working this way. Others -- like yourself -- do. Both ways are perfectly valid. I prefer exceptions to not be values, as, to me, they capture a failed computation not a missing result. For a missing value, I can use null/Optional etc.. But in any case, it's not an either/or thing. You can always catch exceptions and store them if you want; you can even use Either if that's how you prefer to work.

> If you're willing to fix the order in which effects happen (and explicitly handle when you want to change it, as in your example) then you can write something much simpler than the usual monad transformers

How? You need to chain functions, each emitting some set of effects, and you don't control in advance the order of the effects in each function.

I am not fixing the order of effects any more than in your examples. The reason the examples look different is because I've chosen to represent an error not as a value but as a control-flow construct, but I don't have to, so let's look at other effects:

If I have two functions,

    void foo() { print("hi"); log(1); } 
    void bar() { log(2); print("bye"); }
This is how I compose them:

    foo(); bar();
With monads you need to make sure that they both return the same nesting of Log[Writer] or Writer[Log], but I don't need to care.


> foo(); bar();

The whole point of composition is that foo(bar()) is foo(x) where x = bar(), which means that you can understand the combined program by understanding foo and bar independently. If you can't represent the result of bar() as a value x then that breaks down, in which case I don't see what this whole project is about? I mean being able to implicitly pass down a handler is cool, and there's some value in being able to swap out e.g. a "real" and "test" version, but it sounds like the effect sequencing is still directly coupled to the code operation.

> With monads you need to make sure that they both return the same nesting of Log[Writer] or Writer[Log], but I don't need to care.

You only don't need to care if the effects commute - and in that case you can actually automatically move them past each other in the monad world (my scalaz-transfigure library does this, at least at the proof-of-concept level). For noncommutative effects you can do the same thing if you're willing to fix the nesting order (e.g. say that an exception always trumps a later log, or always doesn't).


> which means that you can understand the combined program by understanding foo and bar independently. If you can't represent the result of bar() as a value x then that breaks down

Can you not understand this program by understanding both statements separately?

    print("hi") ; print("there")
Why is that more understandable if the result of print is a value? (obviously, the result of read() is). In any case, describing everything as a value is a PFP idea (note that neither the Lisps nor the MLs do it), that some people like and others (like me) don't. In fact, I find the notion of treating every computation as a value (rather than possibly wrapping it as one), a not too great idea. Values are equational, computations are not (even pure ones). It's a nice abstraction to have at your disposal, but enforcing it everywhere is too much.

> You only don't need to care if the effects commute - and in that case you can actually automatically move them past each other in the monad world

You could, obviously, but when working with continuations, i.e. imperatively, or with Kiselyov's freer monads (which are inspired by continuations) you don't have to.


> Can you not understand this program by understanding both statements separately?

Maybe that one - I/O is a bad example, I don't really see the value of monads for I/O and I don't tend to use an I/O monad in my programs. Programs I can't understand by understanding statements separately are something like:

    openTransaction()
    writeToDatabase(1)
    rollbackTransaction()
or

    val myLog = Buffer()
    myLog :+= "step 1"
    myLog :+= "step 2"
    doSomethingWith(myLog)
and these cases (database transaction, writer) are things I do find it useful to manage with a monad.

> Values are equational, computations are not (even pure ones). It's a nice abstraction to have at your disposal, but enforcing it everywhere is too much.

You need some way to encapsulate/isolate the relevant state of a program partway through running - I think imperative programmers agree that global mutable state is bad. Mostly I see monads as a way to turn global mutable state (whether in the form of global variables, control flow, or something external to the program) into local state, so that you can see which bits of state you need to understand to understand a given line of code.

I think that state has to be equational if we're to have any hope of being able to understand programs - if you can't say whether this time at line 20 the program is in the same state it was last time at line 20, or a different state from last time, how can you even begin to debug?

> You could, obviously, but when working with continuations, i.e. imperatively, or with Kiselyov's freer monads (which are inspired by continuations) you don't have to.

But will those approaches stop me from tripping myself up when effects don't commute? In scalaz-transfigure I do that with the Distributive typeclass - if one effect distributes over another then there will be an instance and you can freely move them past each other, if not then you get a compile error when you try and have to clarify what you want to happen.


> these cases (database transaction, writer) are things I do find it useful to manage with a monad.

Yeah, I know, but some of us don't :)

> You need some way to encapsulate/isolate the relevant state of a program partway through running

That's exactly what continuations do. The question of global mutable state is an orthogonal one.

> I think that state has to be equational if we're to have any hope of being able to understand programs - if you can't say whether this time at line 20 the program is in the same state it was last time at line 20, or a different state from last time, how can you even begin to debug?

Well, we have been successfully writing programs like that for a long time. Now, don't get me wrong -- the equational abstraction is often very useful, but it has its price, and it's not a complete representation of the computation.

> But will those approaches stop me from tripping myself up when effects don't commute?

I don't understand. We don't trip over ourselves when writing imperative code, and when we do, it mostly has to do with IO or other OS state, and monads won't help you there. Linear types ("typestate") might, but even they are limited.


> We don't trip over ourselves when writing imperative code

Speak for yourself. Though I guess I'm more concerned with refactoring than with writing. In particular in imperative code it's very hard to tell the difference between semantically important ordering and accidental ordering.

> when we do, it mostly has to do with IO or other OS state, and monads won't help you there.

Yes they will? It's very normal to encapsulate something of that nature with a monad.


> it's very hard to tell the difference between semantically important ordering and accidental ordering.

Not when effects are clearly marked. There is zero difference in that respect between continuations (i.e. imperative) and monads. It's only syntax and whatever your type system is.

> It's very normal to encapsulate something of that nature with a monad.

Sure, but a monad won't help you ensure their proper ordering.


> Not when effects are clearly marked. There is zero difference in that respect between continuations (i.e. imperative) and monads. It's only syntax and whatever your type system is.

Well, my experience is that refactoring code with an explicit =/<- distinction is much safer than refactoring in the presence of Java checked exceptions. That's my argument against any "effectfulMethod(); effectfulMethod();" style.

I find it hard to understand what the change (compared to monads) you're advocating is. If you want to make the evaluation of a function not a value, then what semantics does it have? (and surely however elegant they are, having different semantics for the return value and the effects of a given call is always going to be more confusing than finding a way to express both in the same model).

> Sure, but a monad won't help you ensure their proper ordering.

It does. You can offer only safe operations - e.g. because I can use a monad to compose a sequence of database operations and store that as a value, I don't have to expose "begin transaction" and "end transaction" operations, only a "do this operation (potentially a composition of several operations and/or pure computations) in a transaction" operation.




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

Search: