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

> But it does because their type says so:

Sure. But I remember doing this with checked exceptions, mousing over each method call one by one to figure out which ones could or couldn't throw. It's much nicer if you can see which methods are the throwey ones immediately - =/<- is a real sweet spot in terms of having the difference be visible but not overly intrusive.

> But that's the opposite of a problem. That's like using `IO a` outside of an IO monad. You want the function to only be called in the right context (an appropriate handler is in effect).

Disagree. I want to be able to have a value of type IO a outside a method - e.g. in a static field. More generally it's really huge to be able to just treat these things as ordinary values that I can manipulate using the ordinary rules of the language.

> I'm not sure if the same effects would make sense, though. For example, Either is a monadic value which doesn't make sense in an imperative setting.

The two effects are "possibly skip the rest of the computation, returning an Error instead" and "record a programatically-accessible Log" - I think those both make sense. The point is the reason you have to use the horrible monad transformers is that the order in which you evaluate these effects matters - if there's an early error, are later log statements recorded, or not? There are more complicated cases when e.g. catching I/O errors.



> It's much nicer if you can see which methods are the throwey ones immediately - =/<- is a real sweet spot in terms of having the difference be visible but not overly intrusive.

Well, I guess that's a matter of taste, but 1/ what information does "effectful" convey if you don't know what the effect is until you mouse-over? 2/ I think that's a very, very low price (if it is a price at all) to pay for something that fits much better with non-pure languages.

> I want to be able to have a value of type IO a outside a method - e.g. in a static field

But continuation effects don't have `IO a`, just `a`. The effect comes with the context. An `IO a` would translate to a lazy value, namely a function, which could be a continuation. There's nothing stopping you from storing a continuation in a field. You can manipulate the `a` just like any value, and the "IO" with handlers -- just as you do with monads.

> possibly skip the rest of the computation, returning an Error instead

Yes, but that's not all Either says. It says "return an error or a value of type a". In imperative code that makes less sense. What you want to say is: "throw an exception or don't (and return whatever value it is that the function returns)". The type of the normal return value is not recorded in the Either.

> The point is the reason you have to use the horrible monad transformers is that the order in which you evaluate these effects matters

You need monad transformers not because of ordering, but because monads don't generally compose. Or it's a bit reversed: because monads don't compose they are sensitive to accidental ordering (whether you care about the order or not), which makes monad transformers tricky. With continuations you have the choice of when to be sensitive to order and when not to.

For example, in the imperative case, you could encode the information "if there's an early error, are later log statements recorded, or not?" directly in the type, (I think) but I don't see any reason to. In Haskell you must because the types need to be nested in one particular way according to the transformer.

In any case, my claim is as follows: if you generally prefer the imperative way of doing effects (like in Java, OCaml, F#, Scheme, JS, Python, Clojure, Erlang etc.) -- as I do -- then continuations are far more natural than monads. If you prefer the PFP way, you should stick to monads (or if using Haskell, must stick to monads).


> An `IO a` would translate to a lazy value, namely a function, which could be a continuation. There's nothing stopping you from storing a continuation in a field. You can manipulate the `a` just like any value, and the "IO" with handlers -- just as you do with monads.

I think the Java world is finally accepting that methods are not a substitute for first-class values; one of the biggest remaining problems with Java lambdas is that it's awkward to use them with checked exceptions (e.g. do you to create a named @FunctionalInterface for each input/output/exception combination you want to store? You can use generics but they don't let you abstract over arity). Handling effects this way would presumably have the same problems?

Put it this way: Either[MyError, A] is much more useful to me than a checked MyException, because 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.

> You need monad transformers not because of ordering, but because monads don't generally compose. Or it's a bit reversed: because monads don't compose they are sensitive to accidental ordering (whether you care about the order or not), which makes monad transformers tricky. With continuations you have the choice of when to be sensitive to order and when not to.

What does the choice look like in code then? Under monads I'd write:

    Left(MyError()).liftM[WriterT[...]]
    WriterT.tell("Here".point[...])
or

    EitherT.left(MyError().point[...])
    Writer.tell("Here").liftM[EitherT[...]]
and while it's clunky it's clear which one's which. Under your approach if I write:

    throwc(new MyException());
    add(Writer.class, "Here");
then which semantics do I get, and how do I get the other one?


> 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: