4 ms·
Perhaps a bit of a different viewpoint, but one of the reasons that I keep coming back to Haskell is that it lets me refactor things things that I can't in othe
by rprospero 11y ago
Perhaps a bit of a different viewpoint, but one of the reasons that I keep coming back to Haskell is that it lets me refactor things things that I can't in other languages. Take the following C code:
char* response(messy* message) {
body* b = message->body;
if(b==NULL) {
return NULL;}
char** lines = b->text;
if(t==NULL) {
return NULL;
}
if(b->line_count < 11) {
return NULL;
}
return t[10];
}
Writing all of those if statements grows tedious. A lazier programmer could just drop them, but then the program would crash whenever the program received malformed data.
With Hackell, though, I can refactor out the if statements and get the following equivalent code:
response :: Messy -> Maybe String
response message = do
b <- body message
lines <- text body
lines `atMay` 10
I want to be clear on a couple of points. First, the Haskell code performs all of the same "null" and bounds checks that the C code made, so this is an apples-to-apples comparison. Furthermore, this isn't using exceptions, like a similar piece of C++ code might try. There's no stack unwinding and nothing can escape to contaminate the rest of your program. Finally, this isn't just a weird special case built into the language. With a minimal amount of effort, I could have each failure return a unique error Code.
errorCode :: Maybe a -> b -> Either b a
errorCode (Just value) _ = Right value
errorCode Nothing code = Left code
response :: Messy -> Either Int String
response message = do
b <- body message `errorCode` 1
lines <- text body `errorCode` 2
(lines `atMay` 10) `errorCode` 3
I don't know a way of doing that in any of the C style languages. In those languages, you're forced to add in another if statement every time your code touches message->body to ensure that you're not going to segfault. With Haskell, I can refactor away those if statements and I don't have that code being duplicated all throughout my project.
EDIT: improved readability of errorCode
- mightybyte 11y agoJust a small side note, you don't even need to write your errorCode function. That and a number of other useful functions are provided in the errors package (http://hackage.haskell.org/package/errors http://hackage.haskell.org/package/errors). Also, I like to use string error messages because they're easier to grep for. Here's what your response function would look like with those changes. response :: Messy -> Either String String response message = do b <- note "error in body" $ body message lines <- note "error in text" $ text b note "error in atMay" $ lines `atMay` 10
- rprospero 11y agoFor anyone reading this, I want to confirm that mightybyte's code would be far more idiomatic. I would never use integer error codes in a Haskell program, but I wanted to make the clear connection to C idioms. Also, using the errors package would be more appropriate than rolling your own function there, but I wanted to emphasize that it was simple, two line function, and not any fancy wizardry that requires caring about category theory. EDIT: rechecking the errors library, the final line would probably be more clear with atNote "Not enough lines" lines 10
- axman6 11y agoI've used a similar construct in dealing with some JSON data recently, using Either String a: (?) :: Maybe a -> String -> Either String a Nothing ? s = Left s Just a ? _ = Right a foo :: Value -> Either String Int foo val = do nested <- val ^? key "foo" . key "bar" ? "Could not find foo.bar" mid <- nested ^? nth 2 . _Integral ? "Second element of foo.bar was not an Integral value" return mid which has really helped deal with the stringly typed nature of JSON. I should also mention I stole the idea (and operator name) from one of the Haskell parser combinator libraries which allowed you to name a whole parser expression, so errors would say "could not find ident" instead of "Expected on of a,b,c,d,e,d,f,g,h...., found X"
- AnimalMuppet 11y ago> Furthermore, this isn't using exceptions, like a similar piece of C++ code might try. There's no stack unwinding and nothing can escape to contaminate the rest of your program. It always escapes to contaminate the rest of your program. In the C code, it escapes as a returned NULL. In C++/Java, it would be a thrown exception. In Haskell, it's the Maybe. The Maybe is pretty close to isomorphic to a checked exception. The caller either has to handle it, or to return a Maybe/declare that it throws an exception itself. At some layer, you have to handle the Nothing/catch the exception. The Nothing short-circuits part of the computation; so does the exception. From where I sit, they're playing almost exactly the same roles. (So is the NULL in C, but it's clumsier than either an exception or a Maybe.)
- rprospero 11y agoYou're right that the Maybe and Either types are very similar to checked exceptions. And, from that perspective, you're absolutely right that it contaminates the rest of the code around above it, similar to a checked exception. However, I would subjectively argue that Maybe is the less clumsy alternative. For the lazy programmer, the easy way to handle exceptions is to ignore them and let upstream deal with it. Even for checked exceptions, ignoring the problem is just a throws statement away. My subjective argument, though, is that monads are just slightly awkward enough to handle that you're encouraged to deal with the problem, instead of just ignoring it. I'd much rather handle whatever condition occurred than constantly deal with liftM and >>=, so I make sure to handle my "execeptions" as soon as I have enough information, just to make my own life easier. Your mileage may vary.
- GregBuchholz 11y ago>I don't know a way of doing that in any of the C style languages http://www.boost.org/doc/libs/1_49_0/libs/optional/doc/html/index.html http://www.boost.org/doc/libs/1_49_0/libs/optional/doc/html/... https://www.google.com/#q=maybe+in+c%2B%2B https://www.google.com/#q=maybe+in+c%2B%2B ...and also I couldn't resist... char* response(messy* message) { body* b = message->body; return b && (b->text) && b->linecount < 11 && b->text[10]; } ...for a C-ish like language. And that's without creating a "do-notation" macro to erase the &&, as the Haskell do-notation erases the >>=.
- evincarofautumn 11y agoThese aren’t quite the same. With “x && x->y”, you’re using a boolean to encode the assumption that “x” is valid, but with “x >>= getY”, the type system has a proof, which makes refactoring safer.