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.
The big win of "managing side effects as a type" is not that effectful code gets an IO type but that everything else does not. Thus the lack of IO in a function's type assures you, reliably, that the function does not cause side effects or depend upon the state of the outside universe. This lets you corral effectful code and keep the bulk of your code "pure" and easy to reuse and reason about.
I would love to use Haskell for the algorithmic part of my code and some other language for the rest of it. I think that is what he is trying to get across. Just get rid of the IO monad and allow only call calls to Haskell from this other language — similar separation but without all the weirdness (IMHO).
Umm, doesn't Haskell sort of provide this with do notation? This is how programming simple things in Haskell feels like to me -- I build a lot of simple pure functions, and then bring them together in do blocks, which feel like a completely different, imperative language.
The fact that you have to use the IO monad, to me, feels like something completely ugly and different from what I get from haskell algorithm wise. IMHO.
I happen to find the IO monad incredibly beautiful and elegant. Haskell lets me define my own control structures for combining IO actions in a far more powerful and elegant manner than other languages.
While that is true, you could have accomplished the same thing through a special syntax that marked a function as "pure", and establishing the constraint that impure functions cannot be called by pure ones. And then only allowed I/O through a set of impure base functions.
As Erik Meijer said, there are many ways to be dirty, but only one way to be pure.
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.
While (map) is pure, (map print) is not. Thus map is a higher-order function that can create both pure and impure functions depending upon the purity of its arguments. How then would your syntactic scheme allow for higher-order functions?
Actually, in Haskell (map print) is pure; it just doesn't do what you expect it to:
>>> 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_:
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.
Actually, every function in Haskell is pure. It's just that some of those pure functions produce values of type (IO a) representing IO actions and, if you sequence those actions into the main action (or a subthread's action), those actions will be performed by the runtime.
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.)
I wasn't contradicting you but trying to reinforce the point that there's an equivalence between "impure" functions and pure functions that produce actions (having impure effects if sequenced). In particular, I wanted to highlight that (mapM_ print) is not somehow more impure than (map print). Many people seem to believe that it is.
(mapM_ print) produces a pure function that produces a single IO action, and (map print) produces a pure function that produces a list of IO actions. Neither has any effect unless called in a context that sequences the resultant actions into the main IO action that the runtime interprets (or a thread's action).
And my point is that the action that mapM_ ultimately produces is not actually executed unless you sequence it into the main action (or a thread's action). Since mapM_'s eventual action is a singleton, you can do this sequencing with any combinator taking a singleton action, for example (>>) or (>>=), but it must be sequenced nonetheless, the same as for the list of actions that (map print) produces, if you want those actions to be executed.
My point was not to argue that something like that would be better solution, but since you're asking: Having a special syntax would make the learning curve a little shallower for newcomers. And it would simplify certain constructs -- instead of having to lift IO values or using mapM_ or whatever, you could actually deal with the results from impure functions directly, no unwrapping or rewrapping needed.
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.
That would certainly be true if purity enforced by monadic I/O were the end of the story, but it isn't. While new users create a lot of hot air about monads and I/O, intermediate-experienced Haskell users just use them for various different purposes and get on with life.
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.
Sure. IO plays a part of a larger system of monads and functional programming, but it's not a prerequisite for impurity to exist -- it is more like a happy confluence of various strands of functional theory. After all, Haskell had I/O before the IO monad existed (although it was apparently not a happy solution).
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.
I guess I just don't see them that way. Most of the time my code is in pure land, and I don't avoid "do" when the result is more readable. I think the ugliest thing in Haskell is probably monad transformer stacks, but that's mainly because I think they're overused by folks who create more abstraction than they need as a matter of habit.
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.
Rust used to have a "pure" annotation, but it's gone, partially because it was a pain to have to write the annotation everywhere, partially because it's not needed for memory safety anymore, partially because nobody can agree on what "pure" means.
I wouldn't have anticipated that, but now that you say it I could see why that would lead to a debate. How would you deal with mutable data structures, for instance? What if it accesses the environment, but in a fashion you could somehow guarantee were safe? In Haskell the programmer can circumvent the system with unsafePerformIO if they know something they can't convince Haskell otherwise, but it almost seems like you'd need a "pure-but-not-really" annotation to do this kind of thing in an imperative language that actually enforced purity.
C++ walked in these very same footsteps. First by not having const, then by having it, then by allowing exceptions to constness, then by introducing const_cast and finally by allowing temporarily mutable const objects.
C++ const is defective because it's a shallow const. You can modify an object through a const pointer.
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").
I like the uniqueness typing approach in Clean very much.
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.
How it so significantly better than using, say, Erlang or CL with a strict self-discipline (separation of impure functions, using appropriate naming conventions, etc.?
OK, let's say that it is better first to learn how to make trees with conses and traverse them using maps and folds with null? as a base case, and then enjoy a compiler which won't allow you to "cons" improper element to it.)
Monads don't really have much to do with IO. It's up in the air whether IO really even is a monad. See Conal Elliot's answer on SO and the link he provides: http://stackoverflow.com/a/16444789/65799
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 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.
I don't agree that managing effects is over the top per se however the use of monads feels over the top for pretty much everything!
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.
I think a separate effect system would be needless specialisation. The IO monad has shown that effects can be reasonably well encoded in the existing type system, the problem is just that composing lots of effects starts to get complicated. I think any solution should involve extending the type system in a general way, or even just providing some kind of syntactic sugar to hide the noise from more complicated types.
For Haskell you're absolutely right however I was responding to the OPs comment that controlling effects in general is over the top.
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.
The progress that has been made in Haskell in the past decade leads me to believe that the issues with the current system can be solved with better abstractions, and possibly new syntax. Or if the solution isn't possible in Haskell, that it will be born of a similar philosophy.
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.
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.
That's an interesting case I hadn't seen before, so thanks for the link.
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.
Using the mechanic employed by the scala plugin I linked to[1], dofib would be annotated with pure(argumentProvider). dofib itself is pure however, at each call site, it has the effect of its argument for that call. This is consistent with your example.
dofib is a pure, higher order function. dofib(getValueFromUser) always returns the same (non-pure) function, but it is always the same. If the argument is provided by another variable, then you need to look up the chain, and would likely have the type/effect system force you not to mix pure/impure functions in the possible parameters, or if you do mix them, call dofib(f) impure.
Well, if you're working in a proper language with an effect system, then dofib is parametrically effect-polymorphic on the effect of its input function.
I'm having fantastic success with a custom IO-like monad that lets me statically verify that my unit test suite is totally side effect free.
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.
I don't have much experience with haskell beyond Learn You A Haskell (a book), but I wonder how beneficial the no null pointer errors are in practice.
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.)
This really depends. The usual rule of thumb is that a runtime exception (at least in pure code) is for bugs in the code and explicit methods like Maybe are for bugs in the input. So if getting a Nothing from your map means your own code is broken, an exception is perfectly fine; if it means your code was called wrong, you want to just return a Maybe.
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.
From this description it sounds like Maybe is similar to checked exceptions in ways I didn't even realize when I wrote my comment. The difference is that Haskell's syntax is pleasant and concise to work with.
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.
Gennerally when you have null pointer erros, it is because the developer did not realize that there was the possibility of having a null value at that point in code. Adding this information to the type system allows the developer to know exactly where the issue might occur (and have the compiler complain if it is not handled). Also, in terms of use, maybe has another major advantage over null. Many times in languages with null you see the pattern
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.
> 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.
Pattern matching is great, you can usually use it instead of head, but you want something like Control.Error.Safe.tryRead to replace other unsafe functions like read.
Working with a Maybe isn't that much better than working with a type that might be null in Java, C#, or C++. You do have to go slightly farther out of your way to be "bad" and ignore the Nothing case, but not much -- you could wrap all your Maybe values with fromJust and turn them into exceptions as you suggest.
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.
I think that in the particular case of an associative map, you are generally correct: there is no gain.
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.
The important bit isn't the Maybe-types, but all types that are _not_ Maybe.
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.
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.
This 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.
That's the theory. In practice, your code is clearer if you use Maybe's all along the composition path, otherwise you spend your time unboxing/lifting values.
A typical Haskell project will have thousands upon thousands of uses of Maybe, and only a handful of calls to the unsafe "unjust". Sure, sometime the maybe wrapper is wrong and you have to force it but it is exceedingly rare and in the vast majority of cases you have maybe you need to actually handle the nothing case. The compiler makes sure you do. It also makes sure you remember not to give nothing to a function that doesn't expect it. It also reminds you to think really hard if you unsafe fromJust.
Without it you can easily hand null to anyone who doesn't expect it, and forget to think about null when receiving it.
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.
> 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?
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.
>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.
Maybe you just put it there or it wasn't Maybe when you checked it?
While I agree that Haskell is a great language and everyone should learn it because it's fun to play with it, I think solution to author's problems (and as a current evolution step) would be something like Kotlin.
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.
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.
Yes. You're free to rename second a to b. 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.
Depending on the monad. If you are in Reader and getA == ask, a and b will have the same value. If you are in IO and getA == getLine, a and b may be different.
The whole point I was talking about is that as soon as you're in monad, referential transparency becomes a bit a "lie", and a <- getA, b <- getA becomes non just analogous to a = getA(); b = getA();, but even worse in "referential transparency" sense.
Referential transparency is not a lie; getA is just a value. It could be substituted for its meaning without altering the program. For example, the following program:
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 ++ "!")
Yes, that's exactly what I was sayint 2 comments upper this thread:
> 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
> It’s also why ruthless testing and 100% test coverage have become so important in mainstream languages. But even with 100% test coverage you can’t be sure that your code will work correctly if a function unexpectedly returns nil unless you’re also mocking things out or using fuzz testing.
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.
I frequently see people talking about an unqualified "100% test coverage". Is it safe to assume in these cases that the author is referring to statement coverage[1] rather than something more stringent like decision coverage or even MC/DC?[2]
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.
Can you list those same features that it provides? My understanding of Scala's philosophy is it provides as many features as it can. A Maybe type, immutable state, separation of IO, etc. isn't very useful if my coworker can choose not to use it.
In theory that's true, and I hope Scala eventually makes non-null, immutability and perhaps even I/O visible at the type level. In practice as long as it's clear when you are bypassing these systems, and you have a culture of not doing so (and the language libraries are part of the same culture), you get most of the benefits.
Even in Haskell you can cast out of your type system, or unsafePerformIO, no?
Scala is like Perl in that there is a non-zero possibility of writing non-native code. In Scala, you're supposed to favor immutability and functional constructs, but there's nothing preventing you from treating Scala exactly like Java. Same with Perl and C.
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)
I assume by "non-native", you mean "non-idiomatic"?
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.
Yeah, we discovered that for my team's projects as well. Having a scala "guru" define and teach idiomatic scala has actually increased our productivity a lot. Any non-idiomatic scala is now purely for interacting with our existing Java libraries
FWIW, the constructs with the initial biggest win for us (in converting procedural thinking) were pattern matching and option types. For pattern matching, the guidelines were
- 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.
Sometimes it is good to drop into non-idiomatic, imperative code for the sake of performance. This is one of the things I like Scala for - it lets you choose your poison.
Scala doesn't really provide much of what is mentioned in the article.
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.
If Scala provided all the things that were mentioned in the article exactly as is, Scala would become essentially yet another Haskell and couldn't do many things that Haskell can't (e.g. seamless usage of Java libraries, real OOP support or performant imperative code). And this would probably restrict it to the academic-niche.
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.
It strikes me that 'puzzle languages' are just languages with non-mainstream semantics. If we lived in an alternate universe where the dominant paradigm was entirely pure, then those odd languages where any bit of code can change any part of the state of the program would be puzzle languages.
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.
Personally I find Python, Ruby, Lua, C to be "puzzle" languages. The puzzle is how to manipulate the primatives like arrays, pointers, dictionaries, etc. to express the natural, higher-level concepts that you really want, like map, fold, etc.
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.
Haskell certainly has a big learning curve, but the experience will be much more rewarding than learning Clojure or F#. Haskell is a (relatively) uncompromising language and Clojure and F# are really just trying to take some of the features of Haskell and bring them into the mainstream. So if you learn F# or Clojure it may help you if you ever try to learn Haskell, but if you learn Haskell, F# and Clojure will be trivial to learn.
> I like things that are straight forward and obvious when I program.
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.
You could just as easily say that Python (one of the languages listed as non-puzzle) is a puzzle language because it lacks goto, so you have to puzzle out ways to model your control flow and looping into the more rigid while-loops, for-loops and if-else-branches. "I want to execute this block at least one time, but not necessarily the second, and I have to check these conditions here, but I have to declare the boolean variable over here... Oh my kingdom for a goto!"
Right, but how does that give me any assurance that the value contained within the optional is itself not null? Haskell gives me that assurance.
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.
An optional<T> has two states: it either holds a T or nothing. There's no third state.
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.
#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);
}
Right. What makes Haskell special is not that it has option types, it's the fact that most types in Haskell cannot be null. This allows us to write pure (and total) functions, knowing that the compiler will enforce it; giving us a high degree of assurance that our function will not fail.
If we choose to write optional<User*>, then we have three possibilities. The point is that we don't have to do that in C++, we can write optional<User> and be certain that the returned value is either empty or a User.
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 :-)
You have an option type in Guava as well, but compared to Haskell, it's annoying to use due to the lack of pattern matching. Also, you can't magically chain them the way you can do with Maybe.
I skimmed so perhaps I missed something, but it looks like this is just the usual argument in favor of functional programming. I don't find it convincing because there's more to programming than getting the right output. Performance and memory usage are also important, and Haskell is extremely opaque in that respect. A strict language with optional laziness makes it much easier to reason about performance.
Have you written any Haskell? As a Ruby dev, I found your statement to be true until I actually tried to write a program (an actual one, not a toy). Fluency came only when I had to write code.
I would say that about Python, and probably anyone can say the same about his own pet computer language.
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.
Haskell zealots are using the same nonsensical arguments that Java used, claiming to be a "safe" language where the compiler and static typing "eliminating common bugs". This is a naive meme, almost everyone telling to each other.
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.
The benefit of Haskell for me is not that it allows "idiots" to write decent code, but that it allows very smart people to write decent code.
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.
Absolutely, which is why the compiler should remove as much thinking burden from the programer as possible. Then he/she has more free brainpower to spend on additional important things.
Exactly the same rhetoric about those evil pointers and safety of static typing and compiler technology was that gave rise to Java.
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.)
This is an such a bone dry straw man it's liable to spontaneously combust in the summer sun.
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.
Attempting to prove any sufficiently complex theorem over an entire program will uncover a certain amount of bugs. Type theory has turned out to be one of our most powerful tools for specifying and proving theorems over whole programs, which is why type systems get touted so much.
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.
Yes, theorem-proving stuff sounds very clever, but no one said that it is at the cost of inability to make a heterogeneous list or even have tuples of different kind in it.) It is, perhaps, possible to prove lots of theorems about very restricted code, I have no idea.
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.)
Yes, theorem-proving stuff sounds very clever, but no one said that it is at the cost of inability to make a heterogeneous list or even have tuples of different kind in 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).
Extensible heterogeneous lists are hard in Haskell, but that's because Haskell chooses to encode extensibility through type-classes rather than data heterogeneity.
It could very well be that most programmers just don't like programming that way, and thus Haskell sucks.
Not really. Just make your types instances of Typeable and then store them in a list of Dynamics. This gives you dynamic typing for those exceptionally rare occasions when you really need it.
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?
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.
[1]: http://www.youtube.com/watch?v=vy5C-mlUQ1w