You already know how to get a working installation of the Haskell Platform. The next step toward your Time Machine Store is to create the initial set of values and functions that will represent the data in the system: clients, machines, and orders.
This chapter will give you the basic ingredients for creating these values and functions. In a first approximation, you will create functions operating on basic types. You already know numbers, and you will add lists and tuples to the mix. Afterward, you will see how to create your own algebraic data types (ADTs) to better represent the kind of values you are interested in here. As part of this, you will learn about pattern matching, a powerful idiom to write concise code that follows closely the shape of the types.
Sometimes ADTs and pattern matching lead to code that’s not clear enough. Records introduce some syntactic forms that make values easier to create and modify, and they are a well-known tool of Haskell programmers. In addition, you will look at two design patterns that are common in Haskell libraries, namely, smart constructors and default values.
This chapter will also introduce how to manage projects using Cabal and Stack. In particular, you will see how to create a new project using both systems, along with the usual structure in folders, and how to load the code into the GHC interpreter to interactively test it.
Characters, Numbers, and Lists
Characters and numbers are universally accepted as the most basic kind of values that a language should provide to programmers. Haskell follows this tradition and offers dedicated character and number types that will be introduced in this section. Afterward, you will see how to put together several of these values to create strings or lists of numbers, as well as the basic operations you can perform on any kind of list.
Characters
Writing the character itself between single quotes, like 'a'.
Writing the code point, that is, the numeric value which represents the character as defined in the Unicode standard, in decimal between ' and ' or in hexadecimal between 'x and '. For example, the same 'a' character can be written as '97' or 'x61'.
Numbers
Int is the bounded integer type. It supports values between at least ±536870911, which corresponds to 229-1 (even though GHC uses a much wider range). Usually, values of the Int type have the native width of the architecture, which makes them the fastest.
Integer is an unbounded integral type. It can represent any value without a decimal part without underflow or overflow. This property makes it useful for writing code without caring about bounds, but it comes at the price of speed.
The Haskell base library also bundles exact rational numbers using the Ratio type. Rational values are created using n % m.
Float and Double are floating-point types of single and double precision, respectively.
Instead of making a numeric constant of a specific type, Haskell has a clever solution for supporting constants for different types: they are called polymorphic. For example, 5 is a constant that can be used for creating values of every type supporting the Num type class (which includes all types introduced before). On the other hand, 3.4 can be used for creating values of any type that is Fractional (which includes Float and Double but not Int or Integer). You will read in detail about type classes in Chapter 4, but right now you can think of a type class as a way to group sets of types that support the same operations. They share many commonalities with interfaces commonly found in object-oriented languages, and are close relatives of Scala’s traits and Swift’s protocols.
Caution
Since Haskell doesn’t use parentheses in function invocations, that is, you write f a b instead of f(a,b), you must be a bit more careful than usual when using negative numbers. For example, if you write atan -4 in GHCi, you will get an error indicating
Non type-variable argument in the constraint (Num (a -> a))
This means it has interpreted that you are trying to compute the subtraction of atan and 4. To get the arctangent of -4, you should instead write atan (-4).
Strings
Instead of some new type, like String, you see your old friend Char but wrapped in square brackets. Those brackets indicate that "Hello world!" is not a character but a list of characters. In general, given a type T, the notation [T] refers to the type of all lists whose elements are of that type T. Lists are the most used data structure in functional programming. The fact that a type like a list depends on other types is known as parametric polymorphism, and you will delve into the details of it in the next chapter. Right now, let’s focus on the practical side.
Lists
Notice from this example that there are functions, such as reverse and (++), that can operate on any kind of list. This means once you know them, you can apply your knowledge of them to any list (including strings of characters). To tell this fact, these functions show in its type a type variable. It is a variable because it can be replaced by any type because regular variables can take different values. Type variables must be written in code starting with lowercase letters, and they consist usually of one or two letters. Here, the type variable is shown as a.
Note
Functions whose names are built entirely by symbols, like ++, must be called using the so-called infix syntax. That is, they should be written between the arguments instead of in front of them. So, you write a ++ b, not ++ a b. In the case where you want to use the function in the normal fashion, you must use parentheses around its name. So, you can write (++) a b, meaning the same as a ++ b.
Note how GHCi writes back the lists using the most common representation using brackets. In the case of lists of characters, it uses string notation.
If you try to get the head or the tail of an empty list, you get an error, as you may expect. Be aware that exceptions are not the preferred way to handle errors in Haskell (you will see why in more detail in subsequent chapters) and by default make the entire program crash when found. To prevent errors from operations on empty lists, just be sure to check for nonemptiness before applying functions such as head and tail (or even better, use pattern matching, which will be introduced shortly).
Caution
The usual warnings about comparing floating-point values apply here. Computers are not able to represent with exact precision all the values, so you may find that equalities that you expect not to hold actually do. For example, in my system the expression (4.00000000000000003 - 4) == 0 evaluates to True.
Both then and else branches must be present along with the if. If this were not the case, then the expression wouldn’t be evaluable for some of the values of b. Other languages opt to return a default value for the nonexistent else, but Haskell makes no commitment.
The entire expression must have a defined type. The way Haskell manages to ensure that is by forcing both t and f expressions to have the same type. Thus, an expression such as if True then 1 else "hello" won’t be accepted by either the compiler or the interpreter.
For sure you have become bored while typing more than once the same constant list in the interpreter. To overcome this, you will learn about the essential ways to reuse functionality across all programming languages: defining functions that work on different input values and creating temporal bindings. But before that, Exercise 2-1includes some tasks to see whether you have understood the concepts up to this point.
Exercise 2-1. Lists Of Lists
Rewrite the previous list literals using only (:) and the empty list constructor, [].
Write an expression that checks whether a list is empty, [], or its first element is empty, like [[],['a','b']].
Write an expression that checks whether a list has only one element. It should return True for ['a'] and False for [] or ['a','b'].
Write an expression that concatenates two lists given inside another list. For example, it should return "abcde" for ["abc","de"].
Use GHCi to check that those expressions work as required.
Creating a New Project
You can create a new project through Cabal and Stack, the main tools for packaging and building systems for Haskell projects. The advantage of using those tools is that they have been especially tailored for Haskell and its package repository, Hackage. In addition, the Cabal description file saves interesting metadata about the project, such as its name, maintainer, and license. In this section you will see how to use both Cabal and Stack from the command line. Feel free to change between them because the project structures are fully compatible.
Creating a Project with Cabal
Note
You might receive a warning about cabal update. Don’t worry, we will download a list of packages shortly, after I introduce how to add dependencies to a Cabal project.
The most important answers to give are the package name and whether you want to create a library or an executable, because what you create affects the name and structure of the project file. The essential difference between a library and an executable project is whether a final program will be produced (in the latter case) or the code is just for consuming other libraries or executables. Right now, it does not matter which one you choose because you will be testing the code using the GHC interpreter. Furthermore, you can refine the project later to add more library or executable descriptions.
Because having all the files in the root of the project makes them difficult to manage, it’s customary to create a folder to hold all the source files of a project, as it is done in other build tools such as Maven for Java. I strongly recommend placing your files in a src folder, as shown in the project initialization above.
Creating a Project with Stack
Stack asks much fewer questions. It is your further responsibility to change the author name, maintainer e-mail, and subsequent fields to the correct value.
There is another possibility to initialize a project using Stack. If you already have a Cabal file, maybe because you have created it previously, you can accommodate it for using Stack by running the command stack init. The only visible difference is the creation of a stack.yaml file in the root of the project.
Exercise 2-2. Your First Project
Create a new library project called chapter2 using either of the methods explained so far.
When doing Exercise 2-2, a pair of files named Setup.hs and chapter2.cabal will be created in the folder. The file Setup.hs is not useful, so you will focus on the .cabal file you have just created. The name of this file always coincides with the name of the package you are developing.
Each property is given a value in the form name: value. The name is case-insensitive (it doesn’t matter whether you write name, Name, or nAmE), and the value is written without any kind of quotes or marks. If the value is a list, the elements are separated by commas.
Stanzas begin with a header, usually library or executable, followed by an application name. Be aware that there is no colon (:) after the header. All properties within the stanza must be indented an equal number of spaces or tabs.
Understanding Modules
You build Haskell projects by writing what are termed modules. Each module contains a set of definitions, such as functions and data types, and groups them under a common umbrella. The names of modules are nested in a hierarchical fashion. For example, inside Data there are a bunch of different modules, like Data.Bool, Data.Ratio, and so forth. This nesting makes modules similar to packages in Java or to namespaces in C#.
You define each module in its own file. The file name should be equal to the last component of the module name (the part after the last dot) and must be nested in folders named like the rest of the components. For example, you would create a module named Chapter2.Section2.Example in the path Chapter2/Section2/Example.hs. At the source directory of your project (which is src is you have followed the instructions above), create a folder named Chapter2. Inside it, create another folder named Section2. Finally, inside Section2 create the Example.hs file.
Changing The Source Directory
to each of the stanzas in the Cabal file. In fact, you can use different source folder for each stanza, which helps us keeping files from libraries, executables, and tests apart.
Then, you can start writing the definitions for that module.
If you are using the command line, you can now compile the project by running cabal new-configure and then cabal new-build, or stack setup and then stack build, depending on your choice of tool. At this point you shouldn’t encounter any compiling errors.
New- Commands in Cabal
At the moment of writing, Cabal is undergoing an internal reorganization. For that reason, it keeps two sets of commands: those starting with the new- prefix (like new-build), and the older ones which do not start like that (e.g., build). Whenever possible, use the former set of commands, because it provides several benefits such as automatic sandboxing of projects.
- 1.
Choose a name for the module, for example A.B.C.
- 2.
Create a folder for each component of its name but the last one, in this case a folder A inside a folder B.
- 3.
Create a file with the same name of the last component ending in .hs (here C.hs) and write the module declaration you saw earlier.
- 4.
Tell Cabal to include the file in your project.
Note From now on, create a new project for each chapter in the book. Create a new module or set of modules for each section. This convention will help keep your work organized.
Cabal and Stack
The Haskell ecosystem has not one but two tools for building projects and managing their dependencies. A fair question to ask is what the differences between them are. In general, Stack is focused on having reproducible builds, whereas Cabal encompasses many more usage scenarios.
The first point of divergence between the two tools is that Stack manages your Haskell installation (including the compiler), whereas Cabal does not. Each Stack project comes with a stack.yaml file in addition to the .cabal one which declares which version of the compiler is targeted. If that specific version is not present in the system, Stack would download and install it in a local directory.
The other main difference is the source of the dependencies declared by each project. Cabal by default uses Hackage, the community-maintained repository of packages. This provides access to every single package in the Haskell ecosystem, but there is no guarantee that a specific combination of packages will work (or even compile) together.
Stack, on the other hand, targets Stackage by default. In Stackage, packages are grouped as resolvers, which specify not only an available set of packages, but also their specific versions. Each of those sets is known to compile together in a specific version of the compiler. Thus, by declaring that your project uses a certain resolver, you are fixing the version of every tool and package, leading to reproducible builds. The downside is that Stackage provides a smaller set of packages than Hackage, although there are ways to declare that some dependency ought to be obtained from the bigger brother.
If you are in doubt of which tool to use, don’t worry and start with any. As I discussed above, both share the same package description format, so changing from one to the other is fairly easy.
Defining Simple Functions
A name, which in Haskell always starts with a lowercase letter
The list of parameters, each of which must also begin with a lowercase letter, separated from the rest by spaces (not by commas, like in most languages) and not surrounded by parentheses
An = sign and the body of the function
Creating a Simple Function
You surely have noticed that loading the file has resulted in a warning. This warning tells you that you have given no type signature, that is, that you haven’t specified the type of the function.
Specifying the Function’s Type
I emphasized in Chapter 1 that Haskell is a strong, statically typed language, and now you are writing functions without any kind of type annotation. How is this possible? The answer is in the same warning message: you didn’t tell anything to Haskell, and it inferred the correct type for the function. Type inference ( i.e., the automatic determination of the type of each expression based on the functions and syntax construct being used) is a key point that makes a strong type system such as Haskell’s still manageable to developers. This is a big contrast with other programming languages, such as Java and C#, which until their last revisions asked developers to write the types of all variables in the code.
Developing a Robust Example
What to do when the list is empty
What to do when the list has some initial element and some tail
When concatenating an empty list with any other list, just return the second list because the first one adds no elements.
When having a nonempty list and appending it to a second list, you have to think about what to do with the head and tail of the first list. Using recursion, you can call (+++) to append the tail of the first list and the second one. The return value from this call will be the list you need, but without the first element. To solve this problem, you can just plug the head of the first list using the (:) operator.
This example also showcases for the first time the use of comments in Haskell code. Comments can include any kind of text and are completely ignored by both the interpreter and the compiler (although some tools like Haddock get information from the comments). As in many programming languages, there are two kinds of comments in Haskell. The first one is a multiline comment, which spans from {- to the nearest -}. Multiline comments are not affected by carriage returns like single-line comments are. Single-line comments span from -- to the first newline symbol found in the source code.
The initial expression comes in as [1,2] +++ [3,4].
It evaluates recursively to 1:([2] +++ [3,4]).
That evaluates recursively to 1:(2:([] +++ [3,4])).
The first list is now empty, so the recursion ends by returning lst2 with 1:(2:[3,4]).
The colon operators simply append list items. Thus, 2:[3,4] evaluates to [2,3,4], and so forth.
The final result is [1,2,3,4].
- 1.
Reverse the tail of the list.
- 2.
Concatenate the head of the list to the end of the reversed tail.
The recursion occurs in step 1. Reversing the tail of a list means to reverse a list that is shorter by one element than the original input list. That shorter-by-one list is passed to the reversal function, creating yet another list, shorter by one more element. This process continues until the tail becomes empty.
I mentioned in Chapter 1 that a useful feature of the Haskell ecosystem is the ability to interactively test functions. Exercise 2-3 describes the steps you should follow for the functions in this section.
Exercise 2-3. Testing Functions
Load the file where you defined the functions into GHCi and call them with different arguments to test them. Based on the warnings that appear, add type signatures to your code.
Returning More Than One Value
Warning
Tuple types of different lengths are completely different types. For example, a function working on tuples in the form (a,b) cannot be applied to tuples such as (a,b,c) that have some other number of values.
In an if block, the lines for then and else must be indented the same way.
In a let or a where block, all local bindings must start in the same position.
Note When reading Haskell code, you will notice that Haskellers also tend to align other symbols, like the = signs in a local bindings block. The layout rule applies only to the beginning of expressions, so alignment is not enforced. However, it’s a common convention that you should follow or at least get used to.
Be aware that this kind of syntax is highly discouraged when writing new code (it is typically used in cases where Haskell code is produced automatically by some other program).
Working with Data Types
A name for the type that will be used to represent its values.
A set of constructors that will be used to create new values. These constructors may have arguments that hold values of the specified types.
In many languages, different constructors can be defined for a data type (or a class, if you are working on an object-oriented language). However, these constructors are somehow linked and tend to be more like shortcuts for default values. In most functional languages, such as Haskell, different constructors are used to represent completely different alternatives to construct values.
- 1.
Government organizations, which are known by their name
- 2.
Companies, for which you need to record a name, an identification number, a contact person, and that person’s position within the company hierarchy
- 3.
Individual clients, known by their name, surname, and whether they want to receive further information about offers and discounts
As you can see, the syntax for declaring data types starts with the data keyword, followed by the type name. After that, constructors are listed, separated by |. Each of them starts with a constructor name and then the types of the arguments to that constructor.
Capitalization in Haskell
Functions, parameters, and bindings must start with a lowercase letter. In the case of an operator name, it must not start with :.
Types, constructors, type classes, and kinds must start with an uppercase letter. If using an operator name, it must start with the : symbol.
These rules make it easier to determine the kind of element you are looking at.
If you tried to create a completely new ADT, for example, named Client2, but you used the same constructor names, you would get a build error. This is because inside a module all constructors must have different names. If you think about it, it’s sensible to ask for that condition because otherwise the compiler wouldn’t be able to distinguish which type you are trying to create.
Data types and constructor names live in different worlds. That means it is possible to create a constructor with the same name as a data type. Indeed, it’s a common convention for one-alternative types, such as Person, to have two names that coincide.
To be able to use the default deriving functionality, all types used inside another one must be showable. For example, if you didn’t include deriving Show in Person, a compilation error would be signaled.
Exercise 2-4 provides a step-by-step recipe on how to integrate this new Gender data type in the existing code base and how to modify the existing functionality for covering it. In the following sections I assume that the Gender type has been defined.
Exercise 2-4. More Types of Values
Add a Gender argument to Person and make it Showable.
Create new values of the new Client data type with the enhanced definition you worked with throughout this section.
You have learned how to define new data types, so it’s time to look at other types that could be useful for the Time Machine Store. Time machines are defined by their manufacturer, their model (which is an integer), their name, whether they can travel to the past and to the future, and a price (which can be represented as a floating-point number). Define a TimeMachine data type holding that information. Try to use more than one ADT to structure the values.
Pattern Matching
Now it’s time to define functions over your shiny new data types. The bad news is that I haven’t taught you how to extract the information from the constructors because you have been taught to use head and tail for lists and to use fst and snd for tuples. The general solution for this task is pattern matching . Matching a value against a pattern allows you to discern the structure of the value, including the constructor that was used to create the value, and to create bindings to the values encoded inside it. When entering the body of the match, the pattern variables will hold the actual inner values, and you can work with them.
Simple Patterns
Let’s see how the execution of a call to clientName (Individual [Person "Jack" "Smith" Male]) False proceeds . First the system finds a case expression. So, it tries to match with the first and second patterns, but in both cases the constructor is not the same as the value. In the third case, the system finds the same constructor, and it binds the values: person now holds Person "Jack" "Smith" Male, and ads holds the value False. In the body of the match, there’s again a case expression, from which a match is done to the Person constructor, binding fNm to "Jack", lNm to "Smith", and gender to Male. Finally, the system proceeds into the innermost body and executes the concatenation, giving “Jack Smith” as the result.
Note
When loading this definition into the interpreter, you will receive a collection of warnings that look like:
Defined but not used: `id'
This tells you that you created a binding that was not used in the body of the match. The solution for this warning is telling the compiler that you won’t use that binding in your code, and this is done by replacing its binding variable by a single underscore, _. For example, the nonwarning pattern for Company name id person resp would have been Company name _ _ _ because you are using only the first pattern variable in the subsequent matching code.
In this case, you have implicitly used the fact that patterns are checked in the same order they appear in the code. This order-dependent behavior can lead to subtle bugs and sometimes even to programs that don’t terminate or run out of resources. As an exercise, rewrite the fibonacci function putting the last pattern in the first position. Now try to test the function in the interpreter. You will see that it never terminates.
When the value is given to f, the first pattern does not match because "Director" is not equal to "Boss". So, the system goes into the second black-hole match and sees that there is no boss. However, on g it first matches into being a Company, which the value satisfies, and in this point it enters the body of the match and forgets about other alternatives. Then, the inner match fails, raising the exception.
Note
I strongly emphasize the fact that pattern matching does not backtrack when something goes wrong in the body of a match. This is important to remember, especially if you are coming from a logic programming background in which unification with backtracking is the norm.
Try to use this new syntax when writing the solution for Exercise 2-5, which provides a set of tasks to practice pattern matching on different kind of values, both clients and time machines.
Exercise 2-5. The Perfect Match For Your Time Machines
These exercises focus on pattern matching on data types defined by you. For working with lists, follow the pattern of having different branches for the empty and general case. Also, think carefully about the order of the patterns. Afterward, test the functions in the interpreter.
For statistical purposes, write a function that returns the number of clients of each gender. You may need to define an auxiliary data type to hold the results of this function.
Every year a time comes when time machines are sold with a big discount to encourage potential buyers. Write a function that, given a list of time machines, decreases their price by some percentage. Use the TimeMachine data type you defined in Exercise 2-4.
Lists and Tuples
Note
It’s customary in Haskell to write pattern matching on lists using a letter or a small word followed by the same identifier in plural, like x:xs.
The Prelude function’s null, head, and tail have no special magic inside them; they can be defined easily using pattern matching. Are you able to do so?
One last remark about matching on lists: In many cases you have a function that at first sight makes sense only on a nonempty list, such as when computing the sum of all elements in a list. In most cases, this function can be extended in a sensible way to empty lists. For example, you can assign the value 0 to the sum of an empty list because if you add that value to any number, it does not change. Values such as 0, which can be safely applied with respect to an operation, such as sum, are called the neutral elements of that operation. I will cover such neutral elements in more detail in Chapter 3 when discussing folds and again in Chapter 4 when discussing monoids.
Guards
A guard is part of the pattern-matching syntax that allows you to refine a pattern using Boolean conditions that must be fulfilled by the bound values after a successful match. Guards are useful for writing clearer code and avoiding certain problems, helping you to obtain the full power of pattern matching.
At this time, your developing sense of clear code signals you that the initial check for negativeness hides part of the algorithm, which is mostly expressed in a pattern match. And that is true. Apart from that, notice that the case statement has used a binding n'. You could have reused n, but the interpreter would complain about shadowing a previous definition. Even though the interpreter knows completely well which n the code refers to, the fact you have used the same name twice may create confusion for another developer. It’s customary in Haskell code to use the same identifier, but with ' (pronounced prime) afterward, to refer to a highly related binding.
But sadly this approach doesn’t make the interpreter happy, which shows the error Conflicting definitions for `x'. This error is because of the restriction imposed on patterns in which a variable can appear only once in each of them. A possible solution is to change the entire shape of the function. Once again, it seems that pattern matching is not giving you all the power you are asking from it.
Apart from the use of guards, you should notice another tidbit in the previous code. The use of otherwise in the last pattern when using guards is a common convention in Haskell. While using otherwise doesn’t add anything to the code (using no guard is equivalent), it signals clearly that the remaining pattern takes care of all the cases not handled by other cases.
Up to this point, I have introduced matching on user defined data types, on lists, and on tuples and guards. The tasks in Exercise 2-6 will help ensure that you understand these concepts.
Exercise 2-6. More Matches and Guards
Up to this point I have introduced matching on lists and tuples and guards. The following tasks will help you ensure that you understand these concepts:
Define the famous Ackermann function. Try using guards:
Define the unzip function, which takes a list of tuples and returns two lists, one with all the first components and other one with the seconds. Here’s an example: unzip [(1,2),(3,4)] = ([1,3],[2,4]).
View Patterns
In the rest of the book, I shall remark that an extension needs to be enabled for a specific piece of code by including the corresponding pragma as you would do in the beginning of a source file.
Note
GHC includes a great many extensions (more than 30 at the moment of writing). They range from simple extensions to the syntax (like the view patterns discussed earlier) for complete overhauls of the type system. Being so different in power, some of them are accepted by the community while others are controversial. All the GHC extensions that will be introduced in this book belong to the first set: they are seen as beneficial because they make code more elegant and easier to understand, without running into any problems. The main con of extensions, even with not-controversial ones, is that they are not part of the Haskell 2010 Report, so in theory they could make your code less interoperable between different Haskell compilers. However, this interoperability is almost never a problem.
Records
In most programming languages you can find the idea of a field as something that holds a value in a larger structure. Furthermore, fields can be accessed or changed easily (e.g., in C or Java using structure.field). From what you have learned so far, you can see that pattern matching on big structures may get unwieldy quickly, because it forces to write long matches to retrieve just a single value and to re-create entire data structures merely to change just a single field.
Creation and Use
The concept of a data structure with fields that can be accessed by name does exist in Haskell. Records make accessing or updating part of a structure much easier than otherwise. Records are defined using data declarations, but instead of just using a type for each parameter, you write parameter name :: parameter type. These declarations are the only exception to the layout rule. You always need to write the set of fields between { and } and to separate them by commas.
They must not clash with any other field or function name.
As I mentioned earlier, you are allowed to use the same field name in more than one alternative of your data type. However, if you do so, all those fields must have the same type. If such is not the case, no correct type can be given to the corresponding function.
Note
Remember that to use these extensions, you need to include the {-# LANGUAGE Extension #-} declaration at the beginning of your source code.
Take the time to understand this last example because it shows a lot of features from this chapter. Record syntax is used to pattern match on PersonR. Inside it, x:xs is used to match a list. As you later want to refer to the entire value to update it, an as pattern is used to bind it to p. Finally, the new name is computed inside a let expression, which is used to update p using record-updating syntax.
As you have done for clients, you can also benefit from record syntax when describing time machines. That is the purpose of Exercise 2-7.
Exercise 2-7. Time Machine Records
Rewrite the TimeMachine data type defined earlier using records. You should find that updating the prices of time machines is now much more concise.
The Default Values Idiom
URL to connect to
Connection type: TCP or UDP
Connection speed
Whether to use a proxy
Whether to use caching
Whether to use keep-alive
Time-out lapse
- 1.
Maintainability is harmed. If at some point you need to add a new connection parameter, all users of the function need to change their calls to connect. Or if the default value changes, all the uses must be reconsidered and rewritten.
- 2.
Using the library is easy only for the simplest case. If you want to connect to a URL using a proxy, you need to step back and use the full connect function, passing all the parameters. In some cases, knowing which the sensible defaults are may be difficult.
There is only one problem left. If you add a new option and the developer has made direct use of the constructor for the record type, that use must be changed. The solution is to forbid calling the constructor directly, forcing the use of connDefault in some way or another. This can be done by not exporting the constructor. You will see how to do this in the next chapter, where you will also learn about smart constructors.
Summary
Basic data types were introduced: characters, Booleans, lists, and tuples.
You learned how to define new functions and how to use let and where to create temporary bindings that allow reusing expressions and thus writing better code. Afterward, you learned how to define a function by cases.
You defined your first data types, learned about ADTs and constructors, and played with creating new values in the interpreter.
Pattern matching is a fundamental tool for Haskell programming, which I touched upon in this chapter. You saw how to match both primitive and user-defined types and how guards, as patterns and view patterns, make matching more concise.
Records were introduced as a better syntax for building, accessing, and updating fields in a Haskell value. You saw the default values design pattern, which uses records at its core.