Why the world needs Haskell
devalot.com
devalot.com
The idea of managing side effects as a type (monads) still hasn't seemed compelling to me in terms of development time. Here I agree with Liskov in saying that it's a bit over the top[0]. Most of the quality I see with Haskell has to do with strict types, not the handling of I/O errors as types themselves.
Not that I want to bash it.. Learning haskell has actively changed how I approach all my C/C++ development, and gotten me far far far into the weeds of now learning Agda as a prototyping language for designs/semantics[1].
The world may or may not need Haskell. However it's certainly a better place now that it has it.
[0]: She said this at a talk she gave at work.. It was similar or the same as her "The Power of Abstraction" talk, dunno if she makes the same comment in every presentation though.
While using the type system to implement an effects system is theoretically elegant, I think it's a beautiful hack that has made the language fussier and more obtuse in practice.
At the end of the day most of us have differing opinions on what constitutes simplicity and elegance. It's certainly true that a "pure" annotation like you're proposing is a much smaller change to introduce in an imperative setting. I recollect D or Rust or something is doing this. But in the functional programming context the monadic solution is more general, and a two-function type class with 3 (IIRC) algebraic laws is not considered an overbearing amount of complexity, though there are of course interesting alternatives with their own merits.
The D language "fixes" this by making const transitive (and also adding an immutable annotation, which means the object is truely read-only, as in "read-only memory").
Personally, I find monadic I/O theoretically elegant, but it comes at the cost of clumsiness when applied to real-world programs. To me, Haskell's "do" blocks feel like an implicit admission of this clumsiness; they are a crutch to work around the fact that having to constantly wrap and unwrap data is something of a chore.
That said, the kinds of things I do on the side with Haskell tend to have a small, well-defined I/O surface, so it could be that I'm spared the worst of it by my interests. I suppose if that weren't the case I'd probably favor OCaml more than I do.
Haskell allows you to annotate classes of side effects such as "changes state" or "might throw exception", not necessarily the full IO monad, so annotating as pure doesn't make sense.
>>> map print ["hello","world","!"]
<interactive>:2:1:
No instance for (Show (IO ())) arising from a use of `print'
Possible fix: add an instance declaration for (Show (IO ()))
In a stmt of an interactive GHCi command: print it
What's going on here? Let's look at the type of (map print) to find out: >>> :t (map print)
(map print) :: Show a => [a] -> [IO ()]
(map print) is a function which takes a list of values of type a -- such that a can be shown as a String; hence the Show a => constraint -- and returns a list of values of type IO () (pronounced IO unit). These are monadic values representing computations that perform the actual IO. Hence, (map print) is a pure function which carries no side effects.So, what the heck do we do with this strange list of IO ()s? Well, one answer is to pass them to sequence_:
>>> sequence_ (map print ["hello","world","!"])
"hello"
"world"
"!"
Ahhh, so now we get to the impure function: sequence_! Actually, sequence_ is a pure function as well. Its type is: >>> :t sequence_
sequence_ :: Monad m => [m a] -> m ()
sequence_ merely takes a list of monadic values and combines them into a single monadic value, discarding any of the elements' return values and returning () instead.So if everything is a pure function, how do we actually perform the side effects? The simplest way to think of it is that our whole program is a bunch of pure functions which construct a single value representing all of the side effects that will take place over the lifetime of the program. This single value is called main:
main :: IO ()
main = sequence_ (map print ["hello","world","!"])
With this idea in mind, we can think of Haskell's runtime as taking this single value main and performing the side effects specified throughout our program.So when people say that some functions in Haskell are "impure" they mean that they produce IO actions that, if sequenced, will depend upon or cause IO effects. Thus, both
map print ["hello","world","!"] :: [IO ()]
and mapM_ print ["hello","world","!"] :: IO ()
are equally pure or impure: They both produce actions that have side effects if sequenced. It's just that the first must be sequenced differently than the second since it produces a list of actions and not a singleton action. Since singletons can be sequenced with (>>) and (>>=) you can insert them directly into do notation, which makes many people believe that they are somehow different in terms of purity. (But they are not.) mapM_ f as = sequence_ (map f as)Function can manage state of variable destructively if the variable is declared unique (there can't be other references, so there can't be side effects from destructively modifying the value).
It's easy to understand. It opens more avenues for fast code, and it keeps the purity.
Looking at Clean's documentation it certainly seems nice, but I know of no other language that had adopted Uniqueness typing.
[Clojure's Transients (http://clojure.org/transients) perhaps?]
No. IO is not the only monad encoding side effects.
The compiler remains vigilant and uncompromising forever.
It's best to automate everything you can, and ask your team for discipline only as a last resort.
It's always struck me as strange that value (as in a typical type system) and effects would be controlled through the same system. The type signature of a function and it's effects seem very much orthogonal to me.
If we want to be controlling side effects then we really ought to be using a separate effect system[1]. There's a scala plugin demonstrating this (although I've not tried it)[2].
With separate type and effect systems I should be able to define a pure function fib(n) and call it like this fib(getValueFromUser()) that is without having to use special operators to get at the value which can only be used in certain contexts a la Haskell.
x = fib(getValueFromUser())
x is not pure, but fib is. OK, the compiler can figure that out without requiring the programmer to write a special "bind" operator.How about this:
dofib(argumentProvider) = fib(argumentProvider())
dofib(lambda: 1) // pure
difib(getValueFromUser) // effectful
Is dofib pure or not? That depends on the value of argumentProvider cannot be determined statically.[1] https://github.com/lrytz/efftp/wiki/Relative-Effects
edit: That's effectively (sorry!) higher-order effects, behaving just as you'd expect.
Food for thought:
1. The easy but limited solution is that this code doesn't compile, because argumentProvider must have a single type/effect and you couldn't have both a pure and an impure function with that type/effect.
2. Is purity that important? Ultimately we care about avoiding our programs doing unintended things, and often effects are just fine as long as they don't misbehave in some way. Purity is a means to an end.
3. It's fascinating to extend the ideas of generic programming from mainstream type systems to effect systems. I suspect there is a lot of potential benefit to be had if we can figure out how to do this without introducing a lot of boilerplate code, in the same way that we can write code using generic types to various degrees today but have type inference spare us a lot of keyboard bashing.
I doubt we'll see anything like this in a mainstream language anytime soon.
Some of the related pages don't seem to be available at the moment, but from what I could see it looks as if that approach still gets hung up on questions of decidability, and doesn't have a very powerful concept of the regions where effects apply, which has been another interesting aspect of the wider research so far. It's good to see someone else working on the field, though.
The issue of composing effects, along with the widespread fear of monads, are enough for me to conclude that monads are not the best way to control effects.
I don't remember ever seeing any solutions other than monads and deprecate effect systems, hence my preference for the later.
I don't yet know whether fear of monads is something the programming community will grow out of, or whether there is a more fundamental reason people have difficulty with them. From what I've seen, the response of most people on finally "getting" monads is "Wait that's it?", so I'm inclined to believe a significant part of the hurdle is the fear itself.
Reminds me of the story of the old monk who emerges from the basement of the monestary holding an ancient parchment. Tears are streaming down his face. The student asks "What's wrong?" The old monk replies "All this time! We were supposed to be celebRate!"
You can't define RealWorld, though, nor (IIRC) is IO really a state monad.
You cannot, so far as I know, implement `>>=` for the IO type defined in the above link within Haskell, which means you can't actually use it to do IO.
newtype IO a = IO ((a -> IOResult) -> IOResult)
data IOResult
= Hugs_ExitWith Int
| Hugs_Catch IOResult (Exception -> IOResult) (Obj -> IOResult)
| Hugs_ForkThread IOResult IOResult
| Hugs_DeadThread
| Hugs_YieldThread IOResult
| Hugs_Return Obj
| Hugs_BlockThread (Obj -> IOResult) ((Obj -> IOResult) -> IOResult)I don't have to resort to documentation or code reviews to ensure that my teammates write fast, reliable tests. This isn't possible in a language that doesn't restrict side effects.
The way haskell handles this reminds me of checked exceptions in Java. In java if you read from a file, your code won't compile unless you have a catch block that handles the possibility of an IO exception. This is called a checked exception because you have to check for the possibility or else your code won't compile.
I know many Java programmers handle checked exceptions by wrapping the checked exception in a unchecked exception so they don't have to deal with it. Don't Haskell developers end up doing the same thing with their Maybe concept?
Haskell avoids null by using a Maybe type/class (I always forget the terminology). A Maybe can evaluate to either a value or a type that represents the absence of a value. (This is an oversimplification for the consideration of people who know nothing about Haskell)
For example, you've got an associative map data type and you find an element in there. At the time of writing the code you "knew" that the element "has to be there". Haskell makes you deal with the possibility that it's not. Won't most developers just end up throwing an exception in that case so they don't have to deal with the impossible possibility? Then, x months from now when the code gets changed so that the map won't have the element there, all the sudden your code gets an error. How is this different from a null pointer exception in any other language?
(Part of me is ignorant and part of me is playing the devil's advocate.)
a.) Haskell gives you tools to deal with it. Unlike checked exceptions which must be restated and dealt with in every function along the chain - Haskell provides powerful abstraction capabilities to manage this. (You can use maybe values while most of the functions know nothing about them)
b.) This is a tool that can help you if you don't shoot yourself in the foot. The compiler can provide guarantees that no one "just throws an exception" by throwing warnings/errors on partial functions and or the use of unsafePerformIO. These are the only two escape hatches that would allow you to do such a thing but they are both heavily frowned upon and detectable by the compiler.
In practice, most of the time, you end up either coming up with a default or returning the Maybe. This is greatly helped by the fact that just propagating Maybe value is really easy because Maybe is a member of a bunch of standard typeclasses like Applicative, Alternative and Monad. Thanks to this, I've found most of my code follows a simple pattern: it pushes the maybe values through until it has a meaningful default. This is safe, simple and fairly elegant. At some point, I either pattern match against it or use a function like fromMaybe, which then allows me to plug everything back into normal code.
It would be really annoying if Java's Map type threw a checked exception when you called get() because try/catch is so verbose and difficult to refactor around. But if it weren't annoying, it would probably make the API better and save me from shooting myself in the foot.
I don't understand. You're saying that there is a mismatch between what the programmer knows and what the compiler can deduce? That you know, from the state the program must be in, that the map has to contain the value associated with the given key? I can't think of an example were that would be the case off the top of my head.
I guess a similar case is if you have to perform the head function (take first element) on a list, and you know that the list is non-empty. If you weren't sure, then if you don't check that the list is non-empty before taking head on the list, you might get an error at runtime from taking head on an empty list, which is undefined in Haskell, but in a dependently typed language is impossible to even do at runtime (much like null pointer exceptions are impossible to get in Haskell).
Why would you throw an exception in Java? If you're sure that a value will be returned (as opposed to a sentinel value... aka null pointer), then just happily dereference it.
You seem to be coming at this from a weird angle. The Maybe type let's you statically mark all things that might be "null", which is a big improvement from the Wild Wild West of everthing might be null. Now you seem to be coming at this from the "how does this make dealing with the semantics of absent values easier?", to which I guess, no, it doesn't. You still have to mull out what you should do if things are missing, or if they deviate from the normal. But you can throw exceptions, define things that you don't want to deal with or that truly are undefined, as simply "undefined", much like in most other languages.
Maybe you just put it there or it wasn't Maybe when you checked it?
However, in other situations the Haskell approach is much better. Say you have a Java class with five fields, three of them are nullable (i.e. have a sensible notion of being null) and two are not (have to be non-null unless there is a programming error). There are tons of places where you can mess up: assign null to an invalid variable, assign "x = f()" not noticing f can return null, call "y.f()" forgetting that y can be null etc. However, if your type system handles nullability, you will get a compile-time error on each of them. More: if you change nullability of a field, compiler errors will point out places that have to be changed.
By throwing an exception when you "know" an element in the hashmap has to be there, you resign from safety offered by the compiler. In Haskell this is a code smell; Haskellers are less content with such hacks and might try to redesign the structure so that the invariants are enforced by the type system.
if (foo==null) {return null} else {...}
This pattern is handled automaticly by maybe; If you try applying a function to Nothing (maybe's version of null), then the result of that function is also Nothing, even if the function itself was not designed to handle Maybes.
Additionally, as a matter of culture, Haskell programmers rarely throw exceptions.
The writers of unix and other base libraries would disagree. See for example `head`. Or most functions that calls into C where the C function fails with an error. In that case Haskell blows up into your face, and you have to wrap the function invocation in a catch.
"Rarely" is relative. Certainly far, far less than other languages, but many otherwise useful libraries are still unfortunately littered with them.
The real benefit is the ability to have things that aren't Maybe. I would say that in any language, most functions don't have meaningful answers for null inputs (especially when you count the 'this' object input in object oriented languages). Most functions also don't produce nullable results. Yet, in C# or Java, reference types could always be null. The cases where that nullability is a real possibility that you need to handle are difficult to identify.
Having nullability be opt-in with Maybe, rather than an inescapable part of all reference types, lets you leave worrying about handling the null case to the typically smaller amount of times where it can actually meaningfully occur. Hopefully that makes doing the handling correctly more likely.
If types aren't non-nullable by default then any function parameter or return value can potentially be null and a robust program practically needs to have null checks _everywhere_. Otherwise, when your program throws a NullPointerException you'll have no way to know where that null originated from.
In Haskell, the types guarantee that you never ever have to check for null (in fact, you can't even) unless the function's type explicitly allows for the possibility of having a null. In addition, you have several ways to compose function calls that might return null in such a way that you don't have to check for each null case separately.
Actually, it's the other way around: NullPointerExceptions are typically trivial to track down.
Finding out at what part of a monadic computation the return of a function put a None when there should have been a Some is much more tricky.
The point is you don't have to, because you don't use Maybe where you know statically that there can't be Nothing!
let x = y
is not a lot more complicated than x <- y clearCode = funcWithMaybe <=< (return . somePureStuff) <=< anotherFuncWithMaybeThis is where Either comes in handy. Either allows you to attach extra information to the error condition (such as a String). This lets you have the same properties of Maybe but with more information when you need to disambiguate.
Without it you can easily hand null to anyone who doesn't expect it, and forget to think about null when receiving it.
It's Java-like language (no need to manage pointers), and it doesn't have null-pointer problem, but it's still high-level imperative OOP language.
While many people say that they're code in Haskell is easier to read, since it has this "side-effect-free" guarantees, for me it seemed as not true for some time recently. In Haskell, when your code gets complicated (and starts to have some patterns you want to omit in typing), you start writing Monads. And when you start writing monads, your code gets harder to read since you need not only consider the code, but also keep (>>=) operator (all of them, if you use multiple monads combined via transformers) in your head for every pair of lines. Your code can suddenly have something like global variables (dynamic scoping) hidden in monad (as with State monad), it's flow can be changed dramatically and different other surprises.
That said, I agree that Haskell code is typically dense, and suffers from readability problems.
https://gist.github.com/k-bx/594a415a06fdd0fc3841
could easily put 2 different values into `a` on each line. Of course, technically everything is still correct, and getA still returns same result for each call.
foo.hs:3:9: Warning:
This binding for `a' shadows the existing binding
bound at ...
when you compile with -Wall. foo = do
a <- getA
putStrLn ("Hello, " ++ a ++ "!")
where
getA = do putStrLn "What is your name?"
getLine
Is equivalent to: bar = do
a <- do putStrLn "What is your name?"
getLine
putStrLn ("Hello, " ++ a ++ "!")> The point I wanted to make is that it's the same getA, but anyone reading the code should understand that result binded to a and b will be different.
So, while referrential transparency is still in place, programmer who reads the code will most likely care not about getA's result, but rather what will a and b get binded to, and in those terms it's just the same as good old
a = getA(); b = getA();
You can write tests with 100% coverage that don't use a single assertion. Test coverage is a completely useless metric, but it is an easy one to measure and understand, which is why it is so popular in pseudo-QA and the management tier.
If we're talking about statement coverage then I agree that achieving 100% statement coverage then calling it a day really isn't as helpful as it might sound.
[1] https://en.wikipedia.org/wiki/Code_coverage#Basic_coverage_c...
[2] https://en.wikipedia.org/wiki/Code_coverage#Modified_conditi...
Also, if you need a language to dictate your coworker's behavior, that says something more about your coworker than the language. Why doesn't he just choose to write good code? (barring arguments regarding the activation energy of good versus bad code)
Since I recently went through coworkers writing "bad" (by which I mean non-idiomatic) Scala, and repairing the situation, I can say that it was surprisingly easy to lay down "functional constructs" and the result was amazing. The code became reliable, well-structured, and easy to leverage. Switching to Scala for our projects -- including introducing procedurally minded programmers to it -- was a huge win for us.
- Don't do any processing after the pattern match within a function; break into smaller functions
- Reach for pattern matching before if/else
- Avoid the use of var
- Avoid dropping through to wildcard pattern. (This tip was from Yaron Minsky of OCaml fame)
In conjunction with preferring the use of combinators over loops, and using Option (and being forced to think about None), we got surprisingly functional Scala code in a short time.
But idiomatic writers do feel like natives!
Haskell goes out of its way to make it hard to do "dangerous" things by accident.
Even in Haskell you can cast out of your type system, or unsafePerformIO, no?
It still has nullable types, side-effects aren't tracked in the type-system (so any function can potentially do anything) and the presence of sub-typing has several unpleasant consequences, for example: lack of full type-inference, generally more complex type signatures than in Haskell and having to deal with covariance vs contravariance.
Anyway, while I generally agree, I'd wish that people would stop bringing up type-inference in these discussions. It always makes me question whether those persons have understood the topic at all.
I intend to learn some Haskell but I keep wondering if it might be more useful to spend some time with Clojure or F#
So do I, which is why I program in Haskell rather than the other languages which make me worry about a ton of irrelevant things like the global state of my program and don't let me enforce invariants of my code in the type system, guaranteeing I can't violate without making the type checker complain.
I like my languages straight forward, that's why I stick to English.
Hague defines puzzle languages by referencing the experience of realizing you're going down the wrong path and having to completely restructure your code; I have had this experience in Python and Java in the past, and conversely, I program in Haskell more or less daily and therefore almost never have that experience there any longer.
Programmers in my previous job would feel pleased with themselves if they'd solved the "puzzle" of an array processing loop which was a simple recursive function in disguise.
boost::optional<User> get_user_by_name(string);
boost::variant<string, User> get_user_by_name(string);
Optional is making its way into the standard library.If all pointers can be null, then every pointer type is an implied optional type. Haskell's advantage is that it allows us to define types which cannot be null.
All pointers can be null, but unlike (for example) Java, we don't have to deal with pointers to objects all the time. C++ has value types and it's perfectly possible to pass objects that cannot be null.
But T could be a pointer to a valid object or a null pointer, or am I wrong?
C++ has value types and it's perfectly possible to pass objects that cannot be null.
Only if those values live on the stack. What about heap objects?
#include <cstdio>
using namespace std;
class Type {
public:
Type(int a):_a(a){};
int _a;
};
Type& getType(int a) {
Type* t = NULL;
t = new Type(a);
return (*t);
}
Type& doSthWith(Type& t) {
//no need (and no way) to check if t is reference to proper object
t._a=t._a+1;
return t;
}
int main() {
Type& t = doSthWith(getType(5));
printf("%d\n",t._a);
return 0;
};
This can be broken of course, for example by Type& getType(int a) {
Type* t = NULL;
return (*t);
}
But that's another matter.Compare this with a function that returns a string:
string get_name();
In C++, the caller of this function knows that a string is always returned. Null isn't part of the picture. The string object itself holds a pointer to some heap data, but that's not something the user has to deal with.PS: I've done my fair share of programming in Haskell so I know the advantages it has :-)
double = (3*)I like some of the Haskell concepts, and it seems to attract very smart folks that are heavy on math and CS jargon. This reflects on the materials and channels around the Haskell community and IMHO makes everything more intimidating than it has to be.
First of all - no static typing system, whatever sophisticated it is, could protect from incompetence, lack of knowledge of underlying principles and plain stupidity. The dream that idiots will write a decent code will never come true, no matter how clever tools could be, just because it is impossible to write any respectable code without understanding hows and whys.
But, for those who managed to understand the core ideas and concepts on which programming languages were based (immutability, passing by reference, properties of essential data-structures, such as lists and hash tables) it is possible to write sane and reasonable code even in C, leave alone CL or Erlang, and the type system will become a burden rather than advantage.
So, Haskell is really good to master Functional Programming (which is much better to learn using old classic Scheme courses), to understand the ideas it rests on. To realize what is function composition, environments, currying, partial evaluation, closures, why they and when they are useful and handy, and how clear and concise everything (syntax and semantics) could be if you just stop here - just skip the part about Monads - they are just over hyped fanciness to show off.
Learning Haskell after Scheme/CL really clarifies one's mind with realizations how the same foundation ideas work in a alien (static-typing) world, and how, everything is clean and concise, until you're starting messing everything up with "too advanced typing".
Again, it is much better to learn the underlying ideas (why it is good to separate and pay special attention to functions that performing IO, what is recursive data structures and why null-pointers do exist in the first place) instead of stupid memes like "monads are cool" or "Haskell prevents bugs".
The trick is that it is that dynamic languages with proper testing (writing tests before code) is not worse than this "static typing safety", and that the very word "safety" is just a meme.
It's much harder (in my experience) for very smart people to write decent code in C, C++, Python etc than in Haskell, simply because so much of their smartness is consumed by having to constantly think about what code might break their program.
Haskell, it seems, while using the same slogans, plus FP and lazyness buzzwords, is really doing the job when you follow its conventions.
Actually, that part which re-implements standard FP techniques with very clever and concise, based on familiar conventions (currying, laziness) syntax, along with ability to write "curried type signatures" is remarkable. It feels much better than SML.
It is definitely the language worth of learning after Scheme/CL.)
The fact that corporate overlords selling Java made certain overhyped promises does not have any bearing whatsoever on the merits of Haskell.
Let's break it down: Java fixed memory management at a significant performance cost, but they didn't solve null references, and in practice it was a big enough of an advancement to actually get a foothold against C++.
Haskell on the other hand actually solves null references as well, so the bugspace that it actually eliminates is easily an order of magnitude bigger than Java's, and it does so without being particularly slow or verbose.
Now as to the rest of your argument, of course it's true that FP principles can be applied anywhere, and given the correct discipline you can achieve much of the same benefits through careful architecture and coding practices. However what you're dismissing is the value of the compiler guarantee, and that should not be minimized. Of course we can look at any code sample and reason about how it should be structured to minimize side effects. But the problems with mutability and null references are endemic to large systems, not isolated features. Where Haskell's guarantees start to shine is when your code base goes over 100kloc and no one person understands the whole thing anymore and the preconditions informing the design no longer apply and things have been hacked from a dozen different perspectives. In this case those guarantees have non-trivial value.
Yes, there are trade-offs in terms of programmer convenience, which is why type-inference research exists.
However, getting bug reports from a static analyzer that runs before your code goes into production and proves strong, complex theorems about your code before allowing it into production will always beat having to manually write extensive test suites.
In fact, speaking of test suites, I recommend checking out Haskell's quickcheck library, and the Scala equivalent whose name I forget at the moment. They use the type system to help automatically generate better test suites, for those properties of your code you cannot yet prove correct.
Static analyzers exist for many languages. Clang toolset has a nice one.
As for testing, keeping functions small and doing just one thing will keep amount of tests small-enough.
Let's say that systematic approach of HtDP/Racket guys allows you write decent programs without necessity of proving any theorems about it.)
That's because that cost isn't actually necessary, it's just Haskell being Haskell. There are plenty of other languages with different type systems that allow for heterogeneous lists very easily (for example, Scala will let you write List[Any] and it will type-check). And some of us research guys are trying to make advances in this stuff (I actually have to email someone to talk about a paper somewhat related to this).
Neither of these is true. Both heterogeneous lists and tuples (indeed any data structure) are possible in Haskell; all thanks to algebraic data types.
It could very well be that most programmers just don't like programming that way, and thus Haskell sucks.
Frankly, from my experience with dynamically typed languages such as Clojure few people actually make heterogeneous lists and vectors beyond a handful of elements; a use case which is covered nicely by Haskell's tuples. Long hetereogeneous lists and vectors are not particularly useful because they cannot be reduced/folded -- unless your reducing function accounts for all the different types but then why not use an algebraic data type with pattern matching?
Of course, there is TYPECASE in CL and some similar macro in Clojure, but Haskell's pattern-matching syntax is more concise.