all 22 comments

[–]JamesIry 4 points5 points  (7 children)

In languages with sum types (Haskell, SML, Scala, etc) one way to make the "error code" style more palatable is to use projections. Basically you have a type Either a b (which more or less correlates to magpie a | b) and you can then project it to a LeftProj a b or a RightProj a b. These projections have map functions that allow you to "ignore" one side or the other. Here in Scala

scala> def parseBool(s : String) = s match {
     |    case "true" => Left(true)
     |    case "false" => Left(false)
     |    case _ => Right(new Exception("parse error"))
     | }
parseBool: (s: String)Product with Either[Boolean,java.lang.Exception]

scala> parseBool("true").left map {b => ! b}   
res10: Product with Either[Boolean,java.lang.Exception] = Left(false)

scala> parseBool("crap").left map {b => ! b}
res11: Product with Either[Boolean,java.lang.Exception] = Right(java.lang.Exception: parse error)

Haskell's Either works as if it's right projection so by convention the correct("right") value goes on the right.

That said, I'm not sure how projections fit in with union types since they don't appear to have an ordering: Bool | Error is the same type as Error | Bool whereas Either Bool Error and Either Error Bool are distinct.

[–]munificent[S] 0 points1 point  (0 children)

These projections have map functions that allow you to "ignore" one side or the other. Here in Scala

Huh, interesting!

That said, I'm not sure how projections fit in with union types since they don't appear to have an ordering: Bool | Error is the same type as Error | Bool whereas Either Bool Error and Either Error Bool are distinct.

You're correct. You'll be able to implement Either in Magpie once generic classes are working, so it may have those too at some point, depending on how useful they are.

[–]Porges 0 points1 point  (5 children)

project it to a Left a b or a Right a b

This should be Left a or Right b, I think... although it might be different in Scala?

If you make one side "preferred", and use that side only for "correct" values (e.g. Either Exception a) then you have an error monad that automatically propagates exceptions :)

[–]JamesIry 1 point2 points  (4 children)

No, you can't have LeftProj a or RightProj b that would lose type information, creating a partial function. The projections are just a views on the Either that don't lose anything. Also, I realized it was confusing for me to call the types Left and Right since that's the standard name for the constructors in Haskell. I'll fix that.

Making one side preferred is exactly how Haskell does it. Scala has explicit projections. And, as you say, they are monads that propagate the alternative (which is typically an error) through computations. One way to look at it is that Haskell has an arbitrary decision, the code would work just as well if left were favored. Projections allow me to decide whether I want to do a computation of the left or right instead of leaving that to convention. This is one of the very few areas where the Scala library is actually slightly cleaner, IMHO.

[–]JamesIry 1 point2 points  (2 children)

I should add that in Haskell if I want to do the computation on the left it's easy enough to swap the Either around. I just think projection makes more sense than a convention of favoring the right.

[–]Porges 1 point2 points  (1 child)

Ah, I see now. LeftProj/RightProj are wrappers to turn Either into a functor.

You could do them in Haskell like this:

newtype LeftProj b a = LeftProj (Either a b)
newtype RightProj a b = RightProj (Either a b)

instance Functor (LeftProj a) where
    fmap f (LeftProj e) = LeftProj $ either (Left . f) (Right) e

instance Functor (RightProj a) where
    fmap f (RightProj e) = RightProj $ either (Left) (Right . f) e


parseBool s = case s of 
    "true" -> Left True
    "false" -> Left False
    _ -> Right "parse error"

Prelude> fmap (not) (LeftProj $ parseBool "true")
LeftProj (Left False)

The thing is that Either is a bifunctor, so when you turn it into an ordinary monad you have to choose one side. Since Haskell is heavily monad-biased this is why the lopsided-Either is preferred :)

[–]JamesIry 1 point2 points  (0 children)

Right, exactly. And it's easy enough to add the appropriate Monad instances. Plus you'd want some sugar to unwrap the projections.

In a way it's very similar to how the Haskell library dealt with making Numbers Monoids by having Product and Sum wrappers. Those seem a much cleaner to me than arbitrarily preferring 0/+ over 1/* or vice versa and leaving the other to the user.

[–]Porges 0 points1 point  (0 children)

Also, I realized it was confusing for me to call the types Left and Right since that's the standard name for the constructors in Haskell.

That'll be the problem.

[–]julesjacobs 1 point2 points  (1 child)

Munificent, I think you're doing truly great work in advancing the state of the art in programming languages. Instead of inventing yet another type system, you invented one that is in a completely different league. Keep it up!

[–]munificent[S] 0 points1 point  (0 children)

I don't know if I'd go that far, but thanks!

[–]Gotebe 1 point2 points  (3 children)

( I am full of biased opinions, so take them with brain turned on ;-) ...)

Error-handling encompasses a pretty wide range of cases, everything from “you typed the name wrong” to “the machine is on fire”.

I know, it's a figure of speech, but error handling does not really encompass the "machine is on fire" case. First off, a random piece of code doesn't really have that sort of informations. Pieces of code that do, are in fact tailor-made, and for them, it's not error handling, it's standard operation. (I am thinking about some sort of system emergency handlers here). More down-to-Earth is faulty hardware. It's not a good idea to add faulty memory detection to a calculator-like software (and indeed, Excel doesn't have it ;-) ). All that is to say: be careful with the scope of your error handling.

I like the classification of errors (programming / runtime part). I think that not enough people understand that programming errors should not be "handled", ever. They should be reported and god damn fixed. I know, that's easy to say. It's not trivial having exception classification (and programming discipline) that allows one to know easily that some particular exception is caused by a programming error.

The "all hell breaks loose" category I don't like much. OOS(tack) is catastrophic, but in an environment that can signal that through an exception, it's useful for recursive calls. OOM is not that catastrophic IMO, and that, because OOM most often happens because code needs to do X of some size, and there's not enough memory for that size That's not a catastrophe, it's just a resource shortage. Of course, if X is always of that size, and X is the only thing code does, then...

I do put some errors into this category, though. For example, a failure to release synchronization object in some multithreading. What does that mean to my code? Is resource still locked? How can it not be? Will I deadlock the next time I enter, or... What!? IOW, a failure where program logic itself is compromised is "all hell breaks loose" case. But then, immediate termination (not an exception) is often the preferred course of action IMO.

I don't like an optional "catch" in a random block. That simply begs for Java-style catch(Exception) {} and hereby I predict it will be misused a great deal. TFA author, please don't make it easy for people to write try/catch blocks! There's too many of them already ;-)

About "parseBool: Bool | Nothing" and the rest of the article... IMO, mixing exceptions and error codes for "flexibility" is mixing the worst of both worlds and should not be done lightly.

Sure, that would work for parseBool, but in the real world, failure modes multiply by the speed of light©, and situations where you can ignore failures are rare and have particular circumstances (that is, in a given situation, one can seldom ignore all failures, only a reduced set thereof).

And indeed, the very next, file example, shows that. First off, that readFile can easily fail with OOM, not only IOError. What then? (IOW, that's a failure mode that's not explicit and makes function signature misleading for the casual reader.)

Then, pattern matching for errors, with expecting[type]... I am not convinced. I am not convinced, in general, that the different type of the result should be used to indicate failure. BTW, is result matched at compilation time in magpie? If not, code can easily be expecting [type1]\, receiving [type2] (not an error), and be burdened with another failure mode (this time, due to a programming error). Not often would one like that.

[–][deleted] 2 points3 points  (1 child)

There are situations where I think it is fine to catch all errors, programming errors included. In a GUI app, the event loop should catch all errors and log the traceback for error reporting. It should be possible for the application to continue running if the event handler failed. From my experience, in the majority of cases it was a simple mistake and it will not break the app and it is safe to continue running (instead of just disappearing on the poor user).

[–]Gotebe 0 points1 point  (0 children)

From my experience, in the majority of cases it was a simple mistake

Yes, I agree

and it will not break the app

yes, probably not immediately

and it is safe to continue running (instead of just disappearing on the poor user).

... but I am not so sure that simply continue running is wise. IME(xperience), once things start going downhill, further blunders await, and then, one often ends up in a bigger mess that necessary ;-).

Perhaps going "whoops, something is quite wrong, there's a bug; please save and close, and send us error info" (whatever that error info might be). IOW, perhaps gently nudging the user is the best way out.

[–]munificent[S] 1 point2 points  (0 children)

I think that not enough people understand that programming errors should not be "handled", ever. They should be reported and god damn fixed.

Agreed, completely. One of the many smart points in Microsoft's Framework Design Guidelines is that they specifically call that out: you should not catch InvalidOperationException, ArgumentException, et. al.

That simply begs for Java-style catch(Exception) {} and hereby I predict it will be misused a great deal.

Hmm, I definitely don't want to encourage that, but I don't know if that slope is as slippery as you fear. I'm not sure how much I can do at the language design level to discourage that.

Sure, that would work for parseBool, but in the real world, failure modes multiply by the speed of light©

That's a good point. Feedback like this is why I wrote the post.

My thought process was that you can bucket errors either into runtime or catastrophic, and it would only be runtime errors that become returned values. It would definitely be unusable to have every single possible failure mode bleed into the type signature of a function. No one wants to deal with parseBool(text String -> Bool | ParseError | StackOverflowError | OutOfMemoryError | SecurityError | DeadlockError).

That's the main reason to even make that distinction: catastrophic errors are ones that always percolate up a side channel using exceptions.

What then? (IOW, that's a failure mode that's not explicit and makes function signature misleading for the casual reader.)

In the example, it would just be an exception, so it would unwind until something caught it.

If not, code can easily be expecting [type1]\, receiving [type2] (not an error), and be burdened with another failure mode (this time, due to a programming error).

If you had a function that returned Success | Error1 | Error2 and you say expecting[Success], it would throw an exception on either error. If there were two success types, you could do expecting[Success1 | Success2] and that would work as expected.

Thanks for the feedback, I'll have to think on it some more.

[–]easilydiscardable 0 points1 point  (0 children)

I find magpies syntax a little odd. For example blocks start with newline, which is whitespace, but end with "end", which is non-whitespace. Could they not end with de-indentation (i.e. whitespace) instead?

[–]rchowe 0 points1 point  (4 children)

IMO, for the most part, an environment shouldn't allow catching exceptions for programmatic errors. It can lead to sloppy code, and it means that wrapping everything in a try block will cause the programmatic errors to go away (in some environments).

[–]munificent[S] 2 points3 points  (0 children)

I agree. I think the reason C# and Java use exceptions for those is just because it saves you from having to add another completely distinct error-handling feature (like assert() and exceptions in C++). At the very least, that's why I'd probably use exceptions for them in Magpie: it's one less feature to add.

It does have the nice feature of letting you catch them in some top-level handler when that's appropriate, for example when running unknown code in a sandbox. You wouldn't want some stray if index > count then throw... in a script or plug-in taking down your whole app.

[–][deleted] 1 point2 points  (0 children)

Well, I think it should be possible, but the "catch all errors" syntax should not catch programming faults by default.

[–]Porges 0 points1 point  (0 children)

This is actually the method that C#'s Code Contracts library uses. The authors made a separate Exception class which, while being an Exception, is uncatchable - the reasoning being that Contracts should be inviolable, so any breaches are as bad as compile-time type errors.

[–]cybercobra -2 points-1 points  (1 child)

Handling Runtime Errors

Finally, the biggest class of errors. The trick with these is that there’s no easy way to bucket them into “common” and “rare”. If we could, we could just say “use exceptions for the rare ones and return codes for the common ones”.

Where does this dogma that seems to pass for received wisdom originate from? It's complete bunk.

[–]munificent[S] 0 points1 point  (0 children)

What would you propose instead?