Why use Haskell?
Do you hate:
- Losing hours or weeks debugging obscure bugs deep in your stack because it’s hard to test or observe what’s going on?
- Worse, having to debug in production because something didn’t get caught during release?
- Getting woken up at 3AM by random alerts because your application keeps crashing/throwing runtime errors for your users?
- Being afraid of changing the core parts of your codebase, even if they’re buggy and poorly designed, because it would be too difficult to make changes without breaking everything?
- Coming back to an old codebase in 6 months or a year and having no idea how anything works and not knowing how to make working changes?
The Haskell programs in the book are very concise and directly connected to the math they demonstrate. The high-level math constructs which are applied enhance the reader’s ability to write more compact and conceptually elegant programs. Most Haskell tutorials on the web use a style of teaching akin to language reference manuals. They show you the syntax of the language, a few language constructs, then tell you to create a few simple functions at the interactive prompt. The 'hard stuff' of how to write a functioning, useful program is left to the end, or omitted entirely.
What if you could:
- Release to production fearlessly, with total peace of mind that nothing will break?
- Never have a crash or runtime error in prod ever again?
- Refactor one of your core files to expose a better API…
- …and change your entire codebase to use the new interface
- …without introducing any regressions
- …in a day?1
- Change old code with complete confidence that it will still work?
Haskell can do that.
I know, I know, it sounds impossibly far-fetched. Just trust me for a second that these are all benefits that Haskell developers take advantage of each and every day. (Okay, maybe not refactor the whole codebase every day. That would be a little excessive.)
You don’t have to take just my word for it, either.
We took the typechecker for [a compiler] and were about half way through writing it in Haskell. [.] We had to stop and work on something else for 6 months mid-authorship. We came back, dusted it off for a week, updating dependencies. Slotted in and got to work…and it worked perfectly the first time. Then we found we could shed 30% of it. Did so, and it still worked perfectly.
How To Write A Haskell Program
I can make sweeping changes to my API, get all the bugs caught by the type system, and still have minimal code impact.
That freedom to refactor is probably my greatest love for Haskell.
— “Meditations on learning Haskell”
Refactoring is cheap in Haskell so you don’t need to get things right the first time.
— Gabriel Gonzalez, “Advice for Haskell beginners”
A crucial aspect of difficult problem solving is that you’re frequently wrong. You pursue an idea for a long time, only to discover that you had something backwards in an important way, and need to make some pervasive changes throughout your work. Note that the maintenance programmer’s “surgical modification” style is a horrible idea here; the last thing you want, when you’re already working at the limits of your ability, is to wade through code whose structure arose out of your past major mistakes. Rather, what you need is a way to make deep structural changes to your code, and still end up with a fair amount of confidence that the result is at least reasonable, that you haven’t forgotten something big.
— Chris Smith, “Haskell’s Niche: Hard Problems”
I’ve talked a little bit about the nitty-gritty of how to write code to accomplish these things before, but let’s instead take a 10,000-foot overview of how Haskell’s design makes this level of bug-freeness and maintainability not just possible, but easy. These benefits aren’t cherry-picked cases; it’s not a case of “if you expend a lot of effort, encode very strict engineering discipline, and ruthlessly enforce code hygiene, maybe you’ll end up having these benefits.” No, they’re a core part of the experience of using Haskell to write real programs.
The secret of Haskell: Extreme maintainability through types
Because Haskell’s rich type system lets your developers model the domain precisely and in a complete way, it’s easier for the same developers to return months or a year from now, or new developers to arrive, and gain a good grasp of what’s happening in the system.
— Chris Done, “Software project maintenance is where Haskell shines”
If you’ve spent time reading about Haskell, you’ll likely have noticed that a lot of the literature is about types. That’s not a fake-out: a large amount of the benefit of Haskell derives solely from its unusually strict type system.
We’re only going to mention the specific attributes of the type system in passing, since they’ve been talked about to death elsewhere. In brief: no nulls; errors and the possibility of returning nothing are represented explicitly. Algebraic data types model alternatives easily. Well-implemented generics (polymorphism) and typeclasses means more reusable code. Type inference means that syntactic noise from having to specify types is minimal. Side effecting code is clearly separated from pure code at the type level.
A key consequence of all this is that it’s possible to write functions that need no validation done on their inputs.
What does that mean? In other languages, if you pass an empty array to a function that only works on nonempty structures, it’s a toss up as to what will happen. Maybe it’ll return some junk data that you have to check for. Maybe it’ll silently give you a wrong answer. Maybe it will crash your program. All of these are annoying, but more importantly, they degrade the quality of your program. It’s up to you to manually make sure they don’t happen, and if you make a mistake (as is human), your program starts misbehaving, likely in ways that cause you to lose time, customers, money, and/or sanity.
Try the same thing in Haskell, and your compiler will catch the potential error for you. You can write a function that is impossible to call with, say, an empty array; your code will simply refuse to compile. Nulls are the most talked about example when it comes to the benefits of strongly-typed functional programming, and indeed being able to use your function parameters immediately without having to do any null checking at all is very nice. But this idea extends to basically any piece of data. You can write functions that only take:
- nonempty lists or arrays
- nonempty strings that also aren’t all whitespace
- nonnegative integers
- only users that have paid
- only validated emails
- only sanitized strings
- …and so on
and have your compiler check that you got it all right.
Your compiler checks for you whether all your function calls will complete successfully. And since a program is basically a big tree of function calls, if every call will complete successfully, your whole program will complete successfully.2 Haskell is much more precise about exactly what inputs your code needs, including permissions for things like DB or filesystem access.
Consequence 1: Bugs become harder to create
While you still have to get your business logic right, the type system catches a lot of stupid errors for you.
How To Write A Program In Haskell Word
Put another way, the only thing you have to worry about is getting the business logic right; the “plumbing” of making sure that you’re not accidentally passing wrong data or failing to handle an edge case is handled by the compiler for you. Coincidentally, these kinds of issues are exactly the most likely to trip you up when doing refactors and maintenance, since you’re not changing what the program is doing, just how it’s structured. While your compiler doesn’t guarantee that your code will give you the right answer, it does guarantee that you’ll get an answer, which is in itself eliminating a whole class of bugs. And with less headspace used handling the stupid errors, you should be better equipped to handle the hard bits, right?
Consequence 2: Maintenance requires less brainpower
If the compiler is making sure that all your function usage is safe, that holds just as true when you add new code, and when you’re refactoring old code.
So you have the ability to:
- Change core types
- Delete huge swaths of unnecessary code
- Add new features
- Refactor the codebase to completely change the fundamental design
all while having complete confidence that your code will still work afterwards. Things that would require deep understanding of the what the code is doing in other languages instead become mechanical exercises in Haskell because the type checker will catch your mistakes while making changes.
In other languages, maintenance is a process of figuring out which code to change to implement your feature (easy), and then figuring out which other code needs to change as a consequence of that (hard). In Haskell, the compiler handles the second part for you.
Consequence 3: Taking shortcuts becomes harder
Developers using [a language that incentivizes taking shortcuts] might compensate by creating a culture that shames worst practices and glorifies best practices in order to realign incentives, but this does not scale well to large software projects and organizations. You can certainly discipline yourself. You can even police your immediate team to ensure that they program responsibly. But what about other teams within your company? What about third-party libraries that you depend on? You can’t police an entire language ecosystem to do the right thing.
— Gabriel Gonzalez, “Worst practices should be hard”
An objection you might have is that Haskell being easier to maintain is only true if everyone touching the code is on the same page and expends the same amount of effort keeping things kosher. You’ve seen the same thing happen to countless projects. Everyone starts off trying to write good code. Deadlines need to be met, targets need to be hit. People start cutting corners to get things done. You poke a hole in an interface to access some internal functionality. You pull a private attribute out because you really need to use it now. The codebase slowly, surely, deteriorates.
Fundamentally, this happens because most modern languages are permissive; if you want to use global state, or send an API request deep within some business logic to fetch something you don’t have, there’s nothing stopping you. It’s a one line change to do it the “bad” way, and potentially a lot of refactoring to do it the “good” way. If the effort/reward curve is skewed that way, is it a surprise that people resort to dirty hacks?
Haskell takes the opposite approach: by default, it’s extremely restrictive. If you want to access something, or make a network request, or access the database, you have to explicitly ask for it. Because of this, breaking abstractions open and using their stinking guts directly is much less appealing; you might have to make changes in potentially your entire codebase to do so. Doing things the “good” way becomes less effort than doing things the “bad” way. If your core types are set up well, any code using them will naturally have to adhere to the constraints that said types impose. That only has to be done once, potentially only by one developer! You get maintainability throughout the lifetime of your code, simply because the easiest thing to do is to do it right.
What’s the catch?
Getting all of these maintainability benefits probably still sounds too good to be true.
- The library situation is adequate, but only just. While you’ll find good libraries for a lot of your backend needs, other areas are oddly anemic. For instance, you might end up having to roll your own authentication stack.
- A big category here is access to third-party APIs. Since Haskell isn’t a first-class citizen when it comes to SaaS services providing library bindings, expect a Haskell binding to not exist for any third party you use, and budget in time to write a client, even just a thin binding over HTTP requests.
- Documentation sucks. Even for popular libraries, expect less and lower-quality documentation than you’d get for, say, any popular Python library picked using
rand(). Sometimes you’ll get lucky and a library will have both solid reference documentation and lots of examples to help you get started. Sometimes you’ll get sparse reference docs that are accurate in the small but don’t help you get the big picture at all. Sometimes you get the lens library.
- Tooling is just okay. If you’re okay with developing on the command line, the experience isn’t that bad. But expect to lose time paging back and forth between your editor and documentation. Other things that help cut down on the development loop, like immediate highlighting of type errors and autocompletion based on type, are rather patchy and require finicky setup.
- Testing is great, but you’ll need to budget time for it. Through a combination of Haskell code both being fundamentally easier to test, having best-in-class tools and libraries for writing tests, and Haskell having a relatively weak story for runtime debugging, the main way of checking your code before you deploy it is… by writing tests. As a biased party, I think this is definitely a good thing overall, since tests are much better for ensuring quality in the long run than stepping through breakpoints. But it does mean you’ll incur some overhead writing them.
In summary, the biggest downside to writing Haskell is that you’ll have to spend more upfront time, whether that’s on the level of a project or an individual feature. In return, you’ll spend a lot less time on the tail end of already “completed” tasks hunting down bugs, constantly patching unreliable code, and chasing alerts to the end of time. Pick Haskell as an investment into your time and sanity, whether that’s 3 months, 6 months, or years down the line. Pick Haskell for the long term.
Found this useful? Still have questions? Talk to me!
You may also like
|Simple input and output (Solutions)|
Getting set up
Back to the real world
Beyond internally calculating values, we want our programs to interact with the world. The most common beginners' program in any language simply displays a 'hello world' greeting on the screen. Here's a Haskell version:
putStrLn is one of the standard Prelude tools. As the 'putStr' part of the name suggests, it takes a
String as an argument and prints it to the screen. We could use
putStr on its own, but we usually include the 'Ln' part so to also print a line break. Thus, whatever else is printed next will appear on a new line.
So now you should be thinking, 'what is the type of the putStrLn function?' It takes a
String and gives… um… what? What do we call that? The program doesn't get something back that it can use in another function. Instead, the result involves having the computer change the screen. In other words, it does something in the world outside of the program. What type could that have? Let's see what GHCi tells us:
'IO' stands for 'input and output'. Wherever there is
IO in a type, interaction with the world outside the program is involved. We'll call these
IO values actions. The other part of the
IO type, in this case
(), is the type of the return value of the action; that is, the type of what it gives back to the program (as opposed to what it does outside the program).
() (pronounced as 'unit') is a type that only contains one value also called
() (effectively a tuple with zero elements). Since
putStrLn sends output to the world but doesn't return anything to the program,
() is used as a placeholder. We might read
IO () as 'action which returns
A few more examples of when we use IO:
- print a string to the screen
- read a string from a keyboard
- write data to a file
- read data from a file
What makes IO actually work? Lots of things happen behind the scenes to take us from
putStrLn to pixels in the screen, but we don't need to understand any of the details to write our programs. A complete Haskell program is actually a big IO action. In a compiled program, this action is called
main and has type
IO (). From this point of view, to write a Haskell program is to combine actions and functions to form the overall action
main that will be executed when the program is run. The compiler takes care of instructing the computer on how to do this.
|Back in the Type Basics chapter, we mentioned that the type of the |
Sequencing actions with do
do notation provides a convenient means of putting actions together (which is essential in doing useful things with Haskell). Consider the following program:
Example: What is your name?
do notation looks very different from the Haskell code we have seen so far, it is just syntactic sugar for a handful of functions, the most important of them being the
(>>=) operator. We could explain how those functions work and then introduce
do notation. However, there are several topics we would need to cover before we can give a convincing explanation. Jumping in with
do right now is a pragmatic short cut that will allow you to start writing complete programs with IO right away. We will see how
do works later in the book, beginning with the Understanding monads chapter.
Before we get into how do works, take a look at
getLine. It goes to the outside world (to the terminal in this case) and brings back a
String. What is its type?
getLine is an IO action that, when run, will return a
String. But what about the input? While functions have types like
a -> b which reflect that they take arguments and give back results,
getLine doesn't actually take an argument. It takes as input whatever is in the line in the terminal. However, that line in the outside world isn't a defined value with a type until we bring it into the Haskell program.
The program doesn't know the state of the outside world until runtime, so it cannot predict the exact results of IO actions. To manage the relationship of these IO actions to other aspects of a program, the actions must be executed in a predictable sequence defined in advance in the code. With regular functions that do not perform IO, the exact sequencing of execution is less of an issue — as long as the results eventually go to the right places.
In our name program, we're sequencing three actions: a
putStrLn with a greeting, a
getLine, and another
putStrLn. With the
getLine, we use
<- notation which assigns a variable name to stand for the returned value. We cannot know what the value will be in advance, but we know it will use the specified variable name, so we can then use the variable elsewhere (in this case, to prepare the final message being printed). The final action defines the type of the whole
do block. Here, the final action is the result of a
putStrLn, and so our whole program has type
Write a program which asks the user for the base and height of a right angled triangle, calculates its area, and prints it to the screen.The interaction should look something like:You will need to use the function
Left arrow clarifications
While actions like
getLine are almost always used to get values, we are not obliged to actually get them. For example, we could write something like this:
getLine directly Lego city undercover ps3 game.
In this case, we don't use the input at all, but we still give the user the experience of entering their name. By omitting the
<-, the action will happen, but the data won't be stored or accessible to the program.
<- can be used with any action except the last
There are very few restrictions on which actions can have valuesobtained from them. Consider the following example where we put theresults of each action into a variable (except the last.. more on thatlater):
Example: putting all results into a variable
x gets the value outof its action, but that isn't useful in this case becausethe action returns the unit value
(). So while we could technically get the value outof any action, it isn't always worth it.
So, what about the final action? Why can't we get a value out of that? Let's see what happenswhen we try:
Example: getting the value out of the last action
Making sense of this requires a somewhat deeper understanding of Haskell than we currently have. Suffice it tosay, after any line where you use
<- to get the value of an action, Haskell expects another action, so the final action cannot have any
Normal Haskell constructions like if/then/elsecan be used within the do notation, but you need to take somecare here. For instance, in a simple 'guess the number' program, we have:
Remember that the if/then/else construction takes three arguments:the condition, the 'then' branch, and the 'else' branch. The condition needs to have type
Bool,and the two branches can have any type, provided that they have the same type. The type of the entire if/then/elseconstruction is then the type of the two branches.
In the outermost comparison, we have
(read guess) < num as the condition.That has the correct type. Let's now consider the 'then' branch. The code here is:
Here, we are sequencing two actions:
doGuessing. The first has type
IO (), which is fine. Thesecond also has type
IO (), which is fine. The type result of theentire computation is precisely the type of the final computation.Thus, the type of the 'then' branch is also
IO (). A similarargument shows that the type of the 'else' branch is also
IO ().This means the type of the entire if/then/elseconstruction is
IO (), which is what we want.
Note: be careful if you find yourself thinking, 'Well, I already starteda do block; I don't need another one.' We can't have code like:
Here, since we didn't repeat the do, the compiler doesn't knowthat the
doGuessing calls are supposed to besequenced, and the compiler will think you're trying to call
putStrLn with three arguments: the string, the function
doGuessing and the integer
num, and thus reject the program.
Write a program that asks the user for his or her name. If the name isone of Simon, John or Phil, tell the user that you think Haskell is agreat programming language. If the name is Koen, tell them that youthink debugging Haskell is fun (Koen Classen is one of the people whoworks on Haskell debugging); otherwise, tell the user that you don't know who he or she is.(As far as syntax goes there are a few different ways to do it; write at least a version using
Actions under the microscope
Actions may look easy up to now, but they are a common stumbling block for new Haskellers. If you have run into trouble working with actions, see if any of your problems or questions match any of the cases below. We suggest skimming this section now, then come back here when you actually experience trouble.
Mind your action types
One temptation might be to simplify our program for getting a name and printing it back out. Here is one unsuccessful attempt:
Example: Why doesn't this work?
Let us boil the example above down to its simplest form. Would you expect this program to compile?
Example: This still does not work
For the most part, this is the same (attempted) program, except that we've stripped off the superfluous 'What is your name' prompt as well as the polite 'Hello'. One trick to understanding this is to reason about it in terms of types. Let us compare:
We can use the same mental machinery we learned in Type basics to figure how this went wrong.
putStrLn is expecting a
String as input. We do not have a
String; we have something tantalisingly close: an
IO String. This represents an action that will give us a
String when it's run. To obtain the
putStrLn wants, we need to run the action, and we do that with the ever-handy left arrow,
Example: This time it works
Working our way back up to the fancy example:
Now the name is the String we are looking for and everything is rolling again.
Mind your expression types too
So, we've made a big deal out of the idea that you can't use actions in situations that don't call for them. The converse of this is that you can't use non-actions in situations that expect actions. Say we want to greet the user, but this time we're so excited to meet them, we just have to SHOUT their name out:
Example: Exciting but incorrect. Why?
How To Write A Program In Haskell Ct
This goes wrong..
This is similar to the problem we ran into above: we've got a mismatch between something expecting an IO type and something which does not produce IO. This time, the trouble is the left arrow
<-; we're trying to left-arrow a value of
makeLoud name, which really isn't left arrow material. It's basically the same mismatch we saw in the previous section, except now we're trying to use regular old String (the loud name) as an IO String. The latter is an action, something to be run, whereas the former is just an expression minding its own business. We cannot simply use
loudName = makeLoud name because a
do sequences actions, and
loudName = makeLoud name is not an action.
So how do we extricate ourselves from this mess? We have a number of options:
- We could find a way to turn
makeLoudinto an action, to make it return
IO String. However, we don't want to make actions go out into the world for no reason. Within our program, we can reliably verify how everything is working. When actions engage the outside world, our results are much less predictable. An IO
makeLoudwould be misguided. Consider another issue too: what if we wanted to use makeLoud from some other, non-IO, function? We really don't want to engage IO actions except when absolutely necessary.
- We could use a special code called
returnto promote the loud name into an action, writing something like
loudName <- return (makeLoud name). This is slightly better. We at least leave the
makeLoudfunction itself nice and IO-free whilst using it in an IO-compatible fashion. That's still moderately clunky because, by virtue of left arrow, we're implying that there's action to be had -- how exciting! -- only to let our reader down with a somewhat anticlimactic
return(note: we will learn more about appropriate uses for
returnin later chapters).
- Or we could use a let binding..
How To Write A Program In Haskell Ok
It turns out that Haskell has a special extra-convenient syntax for let bindings in actions. It looks a little like this:
let bindings in
If you're paying attention, you might notice that the let binding above is missing an
in. This is because
let bindings inside
do blocks do not require the
in keyword. You could very well use it, but then you'd have messy extra do blocks. For what it's worth, the following two blocks of code are equivalent.
How To Write A Program In Haskell Nj
At this point, you have the fundamentals needed to do some fancier input/output. Here are some IO-related topics you may want to check in parallel with the main track of this course.
- You could continue the sequential track, learning more about types and eventually monads.
- Alternately: you could start learning about building graphical user interfaces in the GUI chapter
- For more IO-related functionality, you could also consider learning more about the System.IO library
|Simple input and output|
|Solutions to exercises|
Getting set up >> Variables and functions >> Truth values >> Type basics >> Lists and tuples >> Type basics II >> Next steps >> Building vocabulary >> Simple input and output
Haskell Basics>> Elementary Haskell>> Intermediate Haskell>> Monads
Libraries Reference>> General Practices>> Specialised Tasks