her.esy.fun/src/posts/0010-Haskell-Now/index.org

4112 lines
110 KiB
Org Mode
Raw Normal View History

2019-12-15 16:05:57 +00:00
#+title: Learn Haskell Now!
#+subtitle: A dense Haskell learning material for the brave
#+date: [2019-12-15 Sun]
#+author: Yann Esposito
#+EMAIL: yann@esposito.host
2020-01-04 14:29:27 +00:00
#+keywords: Haskell, programming, functional, tutorial
2020-05-02 13:30:40 +00:00
#+DESCRIPTION: A short and intense introduction to Haskell.
#+DESCRIPTION: This is an update of my old (2012) article.
#+DESCRIPTION: A lot of things have changed since then.
#+DESCRIPTION: Mostly I changed my approach about the easiest way to install
#+DESCRIPTION: a Haskell playground.
#+DESCRIPTION: I removed the not as important part, and added a short
#+DESCRIPTION: introduction about starting a new project.
#+OPTIONS: auto-id:t toc:t
2019-12-26 11:45:11 +00:00
#+STARTUP: overview
#+begin_quote
*Prelude*
In 2012, I really believed that every developer should learn Haskell.
2019-12-15 16:05:57 +00:00
This is why I wrote my old article.
2020-02-22 17:11:49 +00:00
This is the end of 2019 and I still strongly believe that.
I think you should at least be able to understand enough Haskell to write a
simple tool.
2020-03-09 22:55:55 +00:00
There are some features in Haskell that I really miss in most programming
2020-02-22 17:11:49 +00:00
languages and that would not appear to be difficult to provide.
Typically sum types.
A concept so simple yet so helpful.
2019-12-15 16:05:57 +00:00
2020-03-09 22:55:55 +00:00
Since I wrote my article the Haskell ecosystem has evolved:
2019-12-15 16:05:57 +00:00
2020-03-09 22:55:55 +00:00
1. Project building has different existing solutions.
When I wrote this article I made some web applications that I can no
longer build today.
I mean, if I really want to invest some time, I'm sure I could upgrade those
projects to build again.
But this is not worth the hassle.
Now we have =stack=, =nix=, =cabal new-build= and I'm sure other
2019-12-15 16:05:57 +00:00
solutions.
2020-03-09 22:55:55 +00:00
2. GHC is able to do a lot more magic.
This is beyond the scope of an introduction material in my opinion.
While the learning curve is as steep as before, the highest point of
learning just climbed higher and higher with each successive new GHC release.
3. Still no real consencus about how to work, learn, and use Haskell.
In my opinion there are three different perspective on Haskell that
could definitively change how you make decisions about different aspect
of Haskell programming.
I believe the main groups of ideolgies are application developers, library
developers and the main compiler (GHC) developers.
I find those tensions a proof of a healthy environment.
There are different solutions to the same problems and that is perfectly
fine.
This is different when you compare to other language ecosystems where
decisions are more controlled or enforced.
I feel fine with both approaches.
But you must understand that there is no central mindset within
Haskellers
unlike I can find in some other programming language communities.
2019-12-22 23:01:06 +00:00
4. I think that Haskell is now perceived as a lot more serious programming
language now.
2020-03-09 22:55:55 +00:00
A lot more big projects uses Haskell.
Haskell proved its utility to write succesful complex entreprise
2019-12-22 23:01:06 +00:00
projects.
While the ecosystem evolved I believe that I myself have certainly matured.
2020-03-09 22:55:55 +00:00
Since 2013 I'm paid to develop in Clojure.
I write most of my personal side projects in Haskell or in some
2019-12-22 23:01:06 +00:00
Haskell-inspired language.
2019-12-15 16:05:57 +00:00
As such I can follow two functional programming communities growth and
evolution.
I am kind of confident that my Haskell understanding is a lot better than
before.
But I still think, the ability to learn new Haskell subject is infinite.
2019-12-22 23:01:06 +00:00
One article I would like to write someday is about my current team
philosophy about programming.
Our main rule is to use as few features of a programming language as
2019-12-15 16:05:57 +00:00
possible to achieve your goal.
This is a kind of merge between minimalism and pragmatism that in the end
provide a tremendous amount of benefits.
2020-01-03 08:34:31 +00:00
This is why, even if I like to play with the latest Haskell trendy features,
2019-12-22 23:01:06 +00:00
I generally program without those.
With just a very few amount of Haskell features you will already be in
enviromnent with a *lot* of benefits as compared to many programming
languages.
2019-12-15 16:05:57 +00:00
2020-03-09 22:55:55 +00:00
So enough talk, here is my old article updated with a some changes and
2020-01-03 08:34:31 +00:00
cleanups.
I also added a section about how to create a new project template with nix.
I will try to write other articles about how to write a real program in
Haskell.
I tried to add those to this already long article, but, it occurs to be
more work than expected.
So I preferred stop at this point for now and provide separate articles in
the future related to Haskell application development.
2019-12-15 16:05:57 +00:00
#+end_quote
2019-12-22 23:01:06 +00:00
* Introduction
:PROPERTIES:
:CUSTOM_ID: introduction
:END:
2019-12-15 16:05:57 +00:00
I really believe that every developer should learn Haskell.
I don't think every dev needs to be a super Haskell ninja, but they should
at least discover what Haskell has to offer.
Learning Haskell opens your mind.
Mainstream languages share the same foundations:
- variables
- loops
- pointers[fn:1]
- data structures, objects and classes (for most)
Haskell is very different.
The language uses a lot of concepts I had never heard about before.
Many of those concepts will help you become a better programmer.
2019-12-15 16:05:57 +00:00
But learning Haskell can be (and will certainly be) hard.
It was for me.
In this article I try to provide as much help as possible to accelerate
your learning.
This article will certainly be hard to follow.
This is on purpose.
There is no shortcut to learning Haskell.
It is hard and challenging.
But I believe this is a good thing.
It is because it is hard that Haskell is interesting and rewarding.
Today, I could not really provide a conventional path to learn Haskell.
So I think the best I can do is point you to the [[https://www.haskell.org/documentation/][haskell.org]] documentation
website.
2020-03-09 22:55:55 +00:00
And you will see that most path involve a long learning process.
By that, I mean that you should read a long book and invest a lot of hours
and certainly days before having a good idea about what Haskell is all about.
2020-03-09 22:55:55 +00:00
In contrast, this article is a brief and dense overview of all
major aspects of Haskell.
I also added some information I lacked while I learned Haskell.
The article contains five parts:
2020-02-16 14:32:33 +00:00
- *Essential Haskell*: Haskell syntax, and some essential notions.
- *First Dive*:
- Functional style; a progressive example, from imperative to
functional style
- Types; types and a standard binary tree example
- Infinite Structure; manipulate an infinite binary tree!
2020-02-16 14:32:33 +00:00
- *Dive into the impure*:
2020-03-09 22:55:55 +00:00
- Deal with IO; A minimal example
- IO trick explained; the hidden detail I lacked to understand IO
- Monads; incredible how we can generalize
2020-02-16 14:32:33 +00:00
- *Start swimming*: Start a new project.
2019-12-15 16:05:57 +00:00
** Install
:PROPERTIES:
:CUSTOM_ID: install
:END:
#+CAPTION: Haskell logo
[[./Haskell-logo.png]]
2020-05-02 11:37:25 +00:00
If you are not using either Linux nor macOS, you should look here:
https://www.haskell.org/downloads/.
Otherwise, you can follow my advice to use nix:
1. Install [[https://nixos.org/nix][nix]] (The version I used while writting this article was
nix (Nix) 2.3.1, future 2.X.X versions should work with the
examples in this article)
2020-01-01 22:54:53 +00:00
3. create a new empty directory =hsenv= somewhere
4. Put the following =shell.nix= file inside it
2019-12-23 01:08:11 +00:00
#+begin_src nix :tangle shell.nix
2020-06-23 07:15:03 +00:00
{ nixpkgs ? import (fetchTarball https://github.com/NixOS/nixpkgs/archive/19.09.tar.gz) {} }:
let
inherit (nixpkgs) pkgs;
inherit (pkgs) haskellPackages;
haskellDeps = ps: with ps; [
base
protolude
containers
];
ghc = haskellPackages.ghcWithPackages haskellDeps;
nixPackages = [
ghc
pkgs.gdb
haskellPackages.cabal-install
];
in
pkgs.stdenv.mkDerivation {
name = "env";
buildInputs = nixPackages;
shellHook = ''
export PS1="\n\[[hs:\033[1;32m\]\W\[\033[0m\]]> "
'';
}
2019-12-23 01:08:11 +00:00
#+end_src
2020-01-01 22:54:53 +00:00
5. In the =hsenv= directory, in a terminal, run =nix-shell --pure=.
2019-12-23 01:08:11 +00:00
You should wait a lot of time for everything to download.
And you should be ready.
You will have in your PATH:
- =ghc=, the Haskell compiler
- =ghci= that we can described as a Haskell REPL
- =runghc= that will be able to interpret a Haskell file
- =cabal= which is the main tool to deal with Haskell projects
- the Haskell libraries =protolude= and =containers=.
2020-01-01 22:54:53 +00:00
6. To test your env, rung =ghci= and type =import Protolude= you should see
2019-12-23 09:56:47 +00:00
something like this:
#+begin_src
2020-06-23 07:15:03 +00:00
~/hsenv> nix-shell
[nix-shell:~/hsenv]$ ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> import Protolude
Prelude Protolude>
2019-12-23 09:56:47 +00:00
#+end_src
2019-12-23 01:08:11 +00:00
2019-12-23 09:56:47 +00:00
Congratulations you should be ready to start now.
2019-12-23 01:08:11 +00:00
2019-12-23 09:56:47 +00:00
#+begin_notes
- There are multiple ways to install Haskell and I don't think there is a
2019-12-25 15:35:56 +00:00
full consensus between developer about what is the best method.
If you whish to use another method take a look at [[http://haskell.org][haskell.org]].
- This install method is only suitable for using as a playground and I
think perfectly adapted to run code example from this article.
I do not recommend it for serious development.
- =nix= is a generic package manager and goes beyond Haskell.
One great good point is that it does not only manage Haskell packages but
really a lot of other kind of packages.
2020-03-09 22:55:55 +00:00
This can be helpful if you need to depends on a Haskell package that
2019-12-25 15:35:56 +00:00
itself depends on a system library, for example =ncurses=.
- I use [[http://nixos.org/nix][=nix=]] for other projects unrelated to Haskell.
For example, I use the nix-shell bang pattern for shell script for which
I can assume the executable I want are present.
#+end_notes
#+begin_notes
*BONUS*: use [[https://direnv.net][=direnv=]]
#+begin_src
~ cd hsenv
~ echo "use nix" > .envrc
~ direnv allow
#+end_src
Now each time you'll cd into your hsenv directory you'll get the
environment set for you.
2019-12-23 01:08:11 +00:00
#+end_notes
** Don't be afraid
:PROPERTIES:
:CUSTOM_ID: don't-be-afraid
:END:
#+CAPTION: The Scream
[[./munch_TheScream.jpg]]
Many books/articles about Haskell start by introducing some esoteric
2019-12-24 00:23:05 +00:00
formula (quick sort, Fibonacci, etc...).
I will do the exact opposite.
At first I won't show you any Haskell super power.
I will start with similarities between Haskell and other programming
languages.
Let's jump to the mandatory "Hello World".
2019-12-23 09:56:47 +00:00
#+BEGIN_SRC haskell :tangle hello.hs
2019-12-26 11:45:11 +00:00
main = putStrLn "Hello World!"
#+END_SRC
#+BEGIN_EXAMPLE
2019-12-23 09:56:47 +00:00
~ runghc hello.hs
2019-12-16 12:07:02 +00:00
Hello World!
#+END_EXAMPLE
Now, a program asking your name and replying "Hello" using the name you
entered:
2019-12-23 09:56:47 +00:00
#+BEGIN_SRC haskell :tangle name.hs
main = do
print "What is your name?"
name <- getLine
print ("Hello " ++ name ++ "!")
#+END_SRC
First, let us compare this with similar programs in a few imperative
languages:
#+BEGIN_SRC python
# Python
print "What is your name?"
name = raw_input()
print "Hello %s!" % name
#+END_SRC
#+BEGIN_SRC ruby
# Ruby
puts "What is your name?"
name = gets.chomp
puts "Hello #{name}!"
#+END_SRC
#+BEGIN_SRC C
// In C
#include <stdio.h>
int main (int argc, char **argv) {
char name[666]; // <- An Evil Number!
// What if my name is more than 665 character long?
printf("What is your name?\n");
scanf("%s", name);
printf("Hello %s!\n", name);
return 0;
}
#+END_SRC
2019-12-23 09:56:47 +00:00
The structure is the same, but there are some syntax differences.
The main part of this tutorial will be dedicated to explaining why.
2019-12-24 00:23:05 +00:00
In Haskell there is a =main= function and every object has a type.
The type of =main= is =IO ()=.
2019-12-23 09:56:47 +00:00
This means =main= will cause side effects.
Just remember that Haskell can look a lot like mainstream imperative
languages.
** Very basic Haskell
:PROPERTIES:
:CUSTOM_ID: very-basic-haskell
:END:
#+CAPTION: Picasso minimal owl
[[./picasso_owl.jpg]]
Before continuing you need to be warned about some essential properties
of Haskell.
/Functional/
2019-12-24 00:23:05 +00:00
Haskell is a functional language.
If you have an imperative language background, you'll have to learn a lot
of new things.
Hopefully many of these new concepts will help you to program even in
imperative languages.
2019-12-25 15:35:56 +00:00
/Advanced Static Typing/
2019-12-24 00:23:05 +00:00
Instead of being in your way like in =C=, =C++= or =Java=, the type system
is here to help you.
/Purity/
Generally your functions won't modify anything in the outside world.
2019-12-24 00:23:05 +00:00
This means they can't modify the value of a variable, can't get user input,
can't write on the screen, can't launch a missile.
On the other hand, parallelism will be very easy to achieve.
Haskell makes it clear where effects occur and where your code is pure.
Also, it will be far easier to reason about your program.
Most bugs will be prevented in the pure parts of your program.
Furthermore, pure functions follow a fundamental law in Haskell:
#+BEGIN_QUOTE
Applying a function with the same parameters always returns the same value.
#+END_QUOTE
/Laziness/
2020-03-09 22:55:55 +00:00
Laziness by default is an uncommon language design.
2019-12-24 00:23:05 +00:00
By default, Haskell evaluates something only when it is needed.
2020-03-09 22:55:55 +00:00
In consequence, it provides an elegant way to manipulate infinite
2019-12-24 00:23:05 +00:00
structures, for example.
A last warning about how you should read Haskell code.
For me, it is like reading scientific papers.
2020-03-09 22:55:55 +00:00
Some parts are clear, but when you see a formula, just focus and read
2019-12-24 00:23:05 +00:00
slower.
Also, while learning Haskell, it /really/ doesn't matter much if you don't
understand syntax details.
If you meet a =>>==, =<$>=, =<-= or any other weird symbol, just ignore
them and follows the flow of the code.
*** Function declaration
:PROPERTIES:
:CUSTOM_ID: function-declaration
:END:
You might be used to declaring functions like this:
In =C=:
#+BEGIN_SRC C
2019-12-26 11:45:11 +00:00
int f(int x, int y) {
return x*x + y*y;
}
#+END_SRC
In JavaScript:
#+BEGIN_SRC javascript
2019-12-26 11:45:11 +00:00
function f(x,y) {
return x*x + y*y;
}
#+END_SRC
in Python:
#+BEGIN_SRC python
2019-12-26 11:45:11 +00:00
def f(x,y):
return x*x + y*y
#+END_SRC
in Ruby:
#+BEGIN_SRC ruby
2019-12-26 11:45:11 +00:00
def f(x,y)
x*x + y*y
end
#+END_SRC
In Scheme:
#+BEGIN_SRC scheme
2019-12-26 11:45:11 +00:00
(define (f x y)
(+ (* x x) (* y y)))
#+END_SRC
Finally, the Haskell way is:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f x y = x*x + y*y
#+END_SRC
Very clean. No parenthesis, no =def=.
2019-12-23 09:56:47 +00:00
Don't forget, Haskell uses functions and types a lot.
It is thus very easy to define them.
The syntax was particularly well thought out for these objects.
*** A Type Example
:PROPERTIES:
:CUSTOM_ID: a-type-example
:END:
Although it is not mandatory, type information for functions is usually
2019-12-23 09:56:47 +00:00
made explicit.
It's not mandatory because the compiler is smart enough to infer it for
you.
It's a good idea because it indicates intent and understanding.
2019-12-23 09:56:47 +00:00
Let's play a little.
We declare the type using =::=
2019-12-23 09:56:47 +00:00
#+BEGIN_SRC haskell :tangle basic.hs
2019-12-26 11:45:11 +00:00
f :: Int -> Int -> Int
f x y = x*x + y*y
2019-12-26 11:45:11 +00:00
main = print (f 2 3)
#+END_SRC
#+BEGIN_EXAMPLE
2019-12-25 15:35:56 +00:00
[nix-shell:~/hsenv]$ runghc basic.hs
13
#+END_EXAMPLE
Now try
2019-12-23 09:56:47 +00:00
#+BEGIN_SRC haskell :tangle error_basic.hs
2019-12-26 11:45:11 +00:00
f :: Int -> Int -> Int
f x y = x*x + y*y
2019-12-26 11:45:11 +00:00
main = print (f 2.3 4.2)
#+END_SRC
You should get this error:
#+BEGIN_EXAMPLE
2019-12-25 15:35:56 +00:00
[nix-shell:~/hsenv]$ runghc error_basic.hs
2019-12-23 09:56:47 +00:00
error_basic.hs:4:17: error:
• No instance for (Fractional Int) arising from the literal 2.3
• In the first argument of f, namely 2.3
In the first argument of print, namely (f 2.3 4.2)
In the expression: print (f 2.3 4.2)
|
4 | main = print (f 2.3 4.2)
| ^^^
#+END_EXAMPLE
The problem: =4.2= isn't an Int.
2019-12-24 00:23:05 +00:00
The solution: don't declare a type for =f= for the moment and let Haskell
infer the most general type for us:
2019-12-23 09:56:47 +00:00
#+BEGIN_SRC haskell :tangle float_basic.hs
2019-12-26 11:45:11 +00:00
f x y = x*x + y*y
2019-12-26 11:45:11 +00:00
main = print (f 2.3 4.2)
#+END_SRC
2019-12-23 09:56:47 +00:00
#+begin_example
2019-12-25 15:35:56 +00:00
[nix-shell:~/hsenv]$ runghc float_basic.hs
2019-12-23 09:56:47 +00:00
22.93
#+end_example
2019-12-24 00:23:05 +00:00
It works!
Luckily, we don't have to declare a new function for every single type.
For example, in =C=, you'll have to declare a function for =int=, for
=float=, for =long=, for =double=, etc...
2019-12-24 00:23:05 +00:00
But, what type should we declare?
To discover the type Haskell has found for us, just launch ghci:
2019-12-26 11:45:11 +00:00
#+BEGIN_EXAMPLE
% ghci
GHCi, version 7.0.4: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Prelude> let f x y = x*x + y*y
Prelude> :type f
f :: Num a => a -> a -> a
#+END_EXAMPLE
Uh? What is this strange type?
2019-12-26 11:45:11 +00:00
#+BEGIN_SRC haskell
Num a => a -> a -> a
2019-12-26 11:45:11 +00:00
#+END_SRC
2019-12-24 00:23:05 +00:00
First, let's focus on the right part =a -> a -> a=.
To understand it, just look at a list of progressive examples:
| The written type | Its meaning |
|--------------------+---------------------------------------------------------------------------|
| =Int= | the type =Int= |
| =Int -> Int= | the type function from =Int= to =Int= |
| =Float -> Int= | the type function from =Float= to =Int= |
| =a -> Int= | the type function from any type to =Int= |
| =a -> a= | the type function from any type =a= to the same type =a= |
| =a -> a -> a= | the type function of two arguments of any type =a= to the same type =a= |
2019-12-23 09:56:47 +00:00
In the type =a -> a -> a=, the letter =a= is a /type variable/.
It means =f= is a function with two arguments and both arguments and the
result have the same type.
The type variable =a= could take many different type values.
For example =Int=, =Integer=, =Float=...
So instead of having a forced type like in =C= and having to declare a
function for =int=, =long=, =float=, =double=, etc., we declare only one
function like in a dynamically typed language.
2019-12-23 09:56:47 +00:00
This is sometimes called parametric polymorphism.
It's also called having your cake and eating it too.
2019-12-24 00:23:05 +00:00
Generally =a= can be any type, for example a =String= or an =Int=, but also
more complex types, like =Trees=, other functions, etc...
2019-12-23 09:56:47 +00:00
But here our type is prefixed with =Num a =>=.
2019-12-23 09:56:47 +00:00
=Num= is a /type class/.
A type class can be understood as a set of types.
=Num= contains only types which behave like numbers.
More precisely, =Num= is class containing types which implement a specific
list of functions, and in particular =(+)= and =(*)=.
2019-12-23 09:56:47 +00:00
Type classes are a very powerful language construct.
We can do some incredibly powerful stuff with this.
More on this later.
Finally, =Num a => a -> a -> a= means:
2019-12-24 00:23:05 +00:00
Let =a= be a type belonging to the =Num= type class.
This is a function from type =a= to (=a -> a=).
2019-12-23 09:56:47 +00:00
Yes, strange.
In fact, in Haskell no function really has two arguments.
Instead all functions have only one argument.
But we will note that taking two arguments is equivalent to taking one
argument and returning a function taking the second argument as a
parameter.
2019-12-24 00:23:05 +00:00
More precisely =f 3 4= is equivalent to =(f 3) 4=.
Note =f 3= is a function:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f :: Num a => a -> a -> a
2019-12-26 11:45:11 +00:00
g :: Num a => a -> a
g = f 3
2019-12-26 11:45:11 +00:00
g y ⇔ 3*3 + y*y
#+END_SRC
2019-12-23 09:56:47 +00:00
Another notation exists for functions.
The lambda notation allows us to create functions without assigning them a
name.
We call them anonymous functions.
We could also have written:
#+BEGIN_SRC haskell
g = \y -> 3*3 + y*y
#+END_SRC
The =\= is used because it looks like =λ= and is ASCII.
2019-12-24 00:23:05 +00:00
If you are not used to functional programming your brain should be starting
to heat up.
2019-12-23 09:56:47 +00:00
It is time to make a real application.
But just before that, we should verify the type system works as
expected:
2019-12-23 09:56:47 +00:00
#+BEGIN_SRC haskell :tangle typed_float_basic.hs
2019-12-26 11:45:11 +00:00
f :: Num a => a -> a -> a
f x y = x*x + y*y
2019-12-26 11:45:11 +00:00
main = print (f 3 2.4)
#+END_SRC
It works, because, =3= is a valid representation both for Fractional
2019-12-24 00:23:05 +00:00
numbers like Float and for Integer.
As =2.4= is a Fractional number, =3= is then interpreted as being also a
Fractional number.
If we force our function to work with different types, it will fail:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f :: Num a => a -> a -> a
f x y = x*x + y*y
2019-12-26 11:45:11 +00:00
x :: Int
x = 3
y :: Float
y = 2.4
-- won't work because type x ≠ type y
main = print (f x y)
#+END_SRC
2019-12-24 00:23:05 +00:00
The compiler complains.
The two parameters must have the same type.
2019-12-24 00:23:05 +00:00
If you believe that this is a bad idea, and that the compiler should make
the transformation from one type to another for you, you should really
watch this great (and funny) video: [[https://www.destroyallsoftware.com/talks/wat][WAT]]
* Essential Haskell
:PROPERTIES:
:CUSTOM_ID: essential-haskell
:END:
#+CAPTION: Kandinsky Gugg
[[./kandinsky_gugg.jpg]]
2019-12-24 00:23:05 +00:00
I suggest that you skim this part.
Think of it as a reference.
Haskell has a lot of features.
A lot of information is missing here.
Come back here if the notation feels strange.
2019-12-24 00:23:05 +00:00
I use the =⇔= symbol to state that two expression are equivalent.
It is a meta notation, =⇔= does not exists in Haskell.
I will also use =⇒= to show what the return value of an expression is.
** Notations
:PROPERTIES:
:CUSTOM_ID: notations
:END:
**** Arithmetic
:PROPERTIES:
:CUSTOM_ID: arithmetic
:END:
#+BEGIN_SRC
3 + 2 * 6 / 3 ⇔ 3 + ((2*6)/3)
#+END_SRC
**** Logic
:PROPERTIES:
:CUSTOM_ID: logic
:END:
#+BEGIN_SRC
True || False ⇒ True
True && False ⇒ False
True == False ⇒ False
True /= False ⇒ True (/=) is the operator for different
#+END_SRC
**** Powers
:PROPERTIES:
:CUSTOM_ID: powers
:END:
#+BEGIN_SRC
x^n for n an integral (understand Int or Integer)
x**y for y any kind of number (Float for example)
#+END_SRC
=Integer= has no limit except the capacity of your machine:
#+BEGIN_EXAMPLE
4^103
102844034832575377634685573909834406561420991602098741459288064
#+END_EXAMPLE
Yeah! And also rational numbers FTW! But you need to import the module
=Data.Ratio=:
#+BEGIN_EXAMPLE
$ ghci
....
Prelude> :m Data.Ratio
Data.Ratio> (11 % 15) * (5 % 3)
11 % 9
#+END_EXAMPLE
**** Lists
:PROPERTIES:
:CUSTOM_ID: lists
:END:
#+BEGIN_EXAMPLE
[] ⇔ empty list
[1,2,3] ⇔ List of integral
["foo","bar","baz"] ⇔ List of String
1:[2,3] ⇔ [1,2,3], (:) prepend one element
1:2:[] ⇔ [1,2]
[1,2] ++ [3,4] ⇔ [1,2,3,4], (++) concatenate
[1,2,3] ++ ["foo"] ⇔ ERROR String ≠ Integral
[1..4] ⇔ [1,2,3,4]
[1,3..10] ⇔ [1,3,5,7,9]
[2,3,5,7,11..100] ⇔ ERROR! I am not so smart!
[10,9..1] ⇔ [10,9,8,7,6,5,4,3,2,1]
#+END_EXAMPLE
**** Strings
:PROPERTIES:
:CUSTOM_ID: strings
:END:
In Haskell strings are list of =Char=.
#+BEGIN_EXAMPLE
'a' :: Char
"a" :: [Char]
"" ⇔ []
"ab" ⇔ ['a','b'] ⇔ 'a':"b" ⇔ 'a':['b'] ⇔ 'a':'b':[]
"abc" ⇔ "ab"++"c"
#+END_EXAMPLE
#+BEGIN_QUOTE
2019-12-24 00:23:05 +00:00
/Remark/: In real code you shouldn't use list of char to represent text.
You should mostly use =Data.Text= instead.
If you want to represent a stream of ASCII char, you should use
=Data.ByteString=.
#+END_QUOTE
**** Tuples
:PROPERTIES:
:CUSTOM_ID: tuples
:END:
2019-12-24 00:23:05 +00:00
The type of couple is =(a,b)=.
Elements in a tuple can have different types.
#+BEGIN_EXAMPLE
-- All these tuples are valid
(2,"foo")
(3,'a',[2,3])
((2,"a"),"c",3)
fst (x,y) ⇒ x
snd (x,y) ⇒ y
fst (x,y,z) ⇒ ERROR: fst :: (a,b) -> a
snd (x,y,z) ⇒ ERROR: snd :: (a,b) -> b
#+END_EXAMPLE
**** Deal with parentheses
:PROPERTIES:
:CUSTOM_ID: deal-with-parentheses
:END:
To remove some parentheses you can use two functions: =($)= and =(.)=.
#+BEGIN_EXAMPLE
-- By default:
f g h x ⇔ (((f g) h) x)
-- the $ replace parenthesis from the $
-- to the end of the expression
f g $ h x ⇔ f g (h x) ⇔ (f g) (h x)
f $ g h x ⇔ f (g h x) ⇔ f ((g h) x)
f $ g $ h x ⇔ f (g (h x))
-- (.) the composition function
(f . g) x ⇔ f (g x)
(f . g . h) x ⇔ f (g (h x))
#+END_EXAMPLE
** Useful notations for functions
:PROPERTIES:
:CUSTOM_ID: useful-notations-for-functions
:END:
Just a reminder:
#+BEGIN_EXAMPLE
x :: Int ⇔ x is of type Int
x :: a ⇔ x can be of any type
x :: Num a => a ⇔ x can be any type a
such that a belongs to Num type class
f :: a -> b ⇔ f is a function from a to b
f :: a -> b -> c ⇔ f is a function from a to (b→c)
f :: (a -> b) -> c ⇔ f is a function from (a→b) to c
#+END_EXAMPLE
2019-12-24 00:23:05 +00:00
Remember that defining the type of a function before its declaration isn't
mandatory.
Haskell infers the most general type for you.
But it is considered a good practice to do so.
/Infix notation/
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle functions.hs
2019-12-26 11:45:11 +00:00
square :: Num a => a -> a
square x = x^2
#+END_SRC
2019-12-24 00:23:05 +00:00
Note =^= uses infix notation.
For each infix operator there its associated prefix notation.
You just have to put it inside parenthesis.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle functions.hs
2019-12-26 11:45:11 +00:00
square' x = (^) x 2
2019-12-26 11:45:11 +00:00
square'' x = (^2) x
#+END_SRC
2019-12-24 00:23:05 +00:00
We can remove =x= in the left and right side!
It's called η-reduction.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle functions.hs
2019-12-26 11:45:11 +00:00
square''' = (^2)
#+END_SRC
2019-12-24 00:23:05 +00:00
Note we can declare functions with ='= in their name.
Here:
#+BEGIN_QUOTE
2019-12-26 11:45:11 +00:00
=square==square'==square''==square'''=
#+END_QUOTE
2019-12-25 15:35:56 +00:00
Note for each prefix notation you can transform it to infix notation with
=`= like this:
#+begin_example
2019-12-26 11:45:11 +00:00
foo x y ↔ x `foo` y
2019-12-25 15:35:56 +00:00
#+end_example
/Tests/
An implementation of the absolute function.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle functions.hs
2019-12-26 11:45:11 +00:00
absolute :: (Ord a, Num a) => a -> a
absolute x = if x >= 0 then x else -x
#+END_SRC
Note: the =if .. then .. else= Haskell notation is more like the =¤?¤:¤=
2019-12-24 00:23:05 +00:00
C operator.
You cannot forget the =else=.
Another equivalent version:
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle functions.hs
2019-12-26 11:45:11 +00:00
absolute' x
| x >= 0 = x
| otherwise = -x
#+END_SRC
#+BEGIN_QUOTE
2019-12-24 00:23:05 +00:00
Notation warning: indentation is /important/ in Haskell.
Like in Python, bad indentation can break your code!
#+END_QUOTE
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle functions.hs
2019-12-26 11:45:11 +00:00
main = do
print $ square 10
print $ square' 10
print $ square'' 10
print $ square''' 10
print $ absolute 10
print $ absolute (-10)
print $ absolute' 10
print $ absolute' (-10)
#+END_SRC
2019-12-25 15:35:56 +00:00
#+begin_example
~/t/hsenv> runghc functions.hs
100
100
100
100
10
10
10
10
#+end_example
2019-12-26 16:27:19 +00:00
* First dive
:PROPERTIES:
2019-12-26 16:27:19 +00:00
:CUSTOM_ID: first-dive
:END:
2019-12-26 16:27:19 +00:00
In this part, you will be introduced to functional style, types and
infinite structures manipulation.
** Functional style
:PROPERTIES:
:CUSTOM_ID: functional-style
:END:
#+CAPTION: Biomechanical Landscape by H.R. Giger
[[./hr_giger_biomechanicallandscape_500.jpg]]
2019-12-24 00:23:05 +00:00
In this section, I will give a short example of the impressive refactoring
ability provided by Haskell.
We will select a problem and solve it in a standard imperative way.
Then I will make the code evolve.
The end result will be both more elegant and easier to adapt.
Let's solve the following problem:
#+BEGIN_QUOTE
2019-12-24 00:23:05 +00:00
Given a list of integers, return the sum of the even numbers in the list.
example: =[1,2,3,4,5] ⇒ 2 + 4 ⇒ 6=
#+END_QUOTE
To show differences between functional and imperative approaches, I'll
2019-12-25 15:35:56 +00:00
start by providing an imperative solution (in javascript):
#+BEGIN_SRC javascript
2019-12-26 11:45:11 +00:00
function evenSum(list) {
var result = 0;
for (var i=0; i< list.length ; i++) {
if (list[i] % 2 ==0) {
result += list[i];
}
}
2019-12-26 11:45:11 +00:00
return result;
}
#+END_SRC
2019-12-24 00:23:05 +00:00
In Haskell, by contrast, we don't have variables or a for loop.
One solution to achieve the same result without loops is to use recursion.
#+BEGIN_QUOTE
2019-12-24 00:23:05 +00:00
/Remark/: Recursion is generally perceived as slow in imperative languages.
But this is generally not the case in functional programming.
Most of the time Haskell will handle recursive functions efficiently.
#+END_QUOTE
2019-12-24 00:23:05 +00:00
Here is a =C= version of the recursive function.
Note that for simplicity I assume the int list ends with the first =0=
value.
#+BEGIN_SRC C
2019-12-26 11:45:11 +00:00
int evenSum(int *list) {
return accumSum(0,list);
}
int accumSum(int n, int *list) {
int x;
int *xs;
if (*list == 0) { // if the list is empty
return n;
} else {
x = list[0]; // let x be the first element of the list
xs = list+1; // let xs be the list without x
if ( 0 == (x%2) ) { // if x is even
return accumSum(n+x, xs);
} else {
return accumSum(n, xs);
}
}
}
#+END_SRC
2019-12-24 00:23:05 +00:00
Keep this code in mind.
We will translate it into Haskell.
First, however, I need to introduce three simple but useful functions we
will use:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
even :: Integral a => a -> Bool
head :: [a] -> a
tail :: [a] -> [a]
#+END_SRC
=even= verifies if a number is even.
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
even :: Integral a => a -> Bool
even 3 ⇒ False
even 2 ⇒ True
#+END_SRC
=head= returns the first element of a list:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
head :: [a] -> a
head [1,2,3] ⇒ 1
head [] ⇒ ERROR
#+END_SRC
=tail= returns all elements of a list, except the first:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
tail :: [a] -> [a]
tail [1,2,3] ⇒ [2,3]
tail [3] ⇒ []
tail [] ⇒ ERROR
#+END_SRC
Note that for any non empty list =l=, =l ⇔ (head l):(tail l)=
2019-12-24 00:23:05 +00:00
The first Haskell solution.
The function =evenSum= returns the sum of all even numbers in a list:
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v1.hs
2019-12-26 11:45:11 +00:00
-- Version 1
evenSum :: [Integer] -> Integer
evenSum l = accumSum 0 l
accumSum n l = if l == []
then n
else let x = head l
xs = tail l
in if even x
then accumSum (n+x) xs
else accumSum n xs
#+END_SRC
To test a function you can use =ghci=:
2019-12-25 15:35:56 +00:00
#+begin_example
~/t/hsenv> ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> :l evenSum_v1.hs
[1 of 1] Compiling Main ( evenSum_v1.hs, interpreted )
Ok, one module loaded.
*Main> evenSum [1..5]
6
#+end_example
Here is an example of execution[fn:2]:
2019-12-25 15:35:56 +00:00
#+begin_example
*Main> evenSum [1..5]
accumSum 0 [1,2,3,4,5]
1 is odd
accumSum 0 [2,3,4,5]
2 is even
accumSum (0+2) [3,4,5]
3 is odd
accumSum (0+2) [4,5]
2 is even
accumSum (0+2+4) [5]
5 is odd
accumSum (0+2+4) []
l == []
0+2+4
0+6
6
#+end_example
2019-12-24 00:23:05 +00:00
Coming from an imperative language all should seem right.
In fact, many things can be improved here.
First, we can generalize the type.
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
evenSum :: Integral a => [a] -> a
#+END_SRC
Next, we can use sub functions using =where= or =let=.
2019-12-25 15:35:56 +00:00
This way our =accumSum= function will not pollute the namespace of our
module.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v2.hs
2019-12-26 11:45:11 +00:00
-- Version 2
evenSum :: Integral a => [a] -> a
evenSum l = accumSum 0 l
where accumSum n l =
if l == []
then n
else let x = head l
xs = tail l
in if even x
then accumSum (n+x) xs
else accumSum n xs
#+END_SRC
Next, we can use pattern matching.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v3.hs
2019-12-26 11:45:11 +00:00
-- Version 3
evenSum l = accumSum 0 l
where
accumSum n [] = n
accumSum n (x:xs) =
if even x
then accumSum (n+x) xs
else accumSum n xs
#+END_SRC
2019-12-24 00:23:05 +00:00
What is pattern matching?
Use values instead of general parameter names[fn:3].
2019-12-24 00:23:05 +00:00
Instead of saying: =foo l = if l == [] then <x> else <y>= you simply state:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
foo [] = <x>
foo l = <y>
#+END_SRC
2019-12-24 00:23:05 +00:00
But pattern matching goes even further.
It is also able to inspect the inner data of a complex value.
We can replace
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
foo l = let x = head l
xs = tail l
in if even x
then foo (n+x) xs
else foo n xs
#+END_SRC
with
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
foo (x:xs) = if even x
then foo (n+x) xs
else foo n xs
#+END_SRC
This is a very useful feature.
It makes our code both terser and easier to read.
In Haskell you can simplify function definitions by η-reducing them.
For example, instead of writing:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f x = (some expresion) x
#+END_SRC
you can simply write
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f = (some expression)
#+END_SRC
We use this method to remove the =l=:
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v4.hs
2019-12-26 11:45:11 +00:00
-- Version 4
evenSum :: Integral a => [a] -> a
evenSum = accumSum 0
where
accumSum n [] = n
accumSum n (x:xs) =
if even x
then accumSum (n+x) xs
else accumSum n xs
#+END_SRC
*** Higher Order Functions
:PROPERTIES:
:CUSTOM_ID: higher-order-functions
:END:
#+CAPTION: Escher
[[./escher_polygon.png]]
2019-12-24 00:23:05 +00:00
To make things even better we should use higher order functions.
What are these beasts?
Higher order functions are functions taking functions as parameters.
Here are some examples:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
filter :: (a -> Bool) -> [a] -> [a]
map :: (a -> b) -> [a] -> [b]
foldl :: (a -> b -> a) -> a -> [b] -> a
#+END_SRC
Let's proceed by small steps.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v5.hs
2019-12-26 11:45:11 +00:00
-- Version 5
evenSum l = mysum 0 (filter even l)
where
mysum n [] = n
mysum n (x:xs) = mysum (n+x) xs
#+END_SRC
where
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
filter even [1..10] ⇔ [2,4,6,8,10]
#+END_SRC
The function =filter= takes a function of type (=a -> Bool=) and a list of
type =[a]=.
It returns a list containing only elements for which the function returned
2019-12-25 15:35:56 +00:00
=True=.
Our next step is to use another technique to accomplish the same thing as a
loop.
We will use the =foldl= function to accumulate a value as we pass through
the list.
The function =foldl= captures a general coding pattern:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
myfunc list = foo initialValue list
foo accumulated [] = accumulated
foo tmpValue (x:xs) = foo (bar tmpValue x) xs
#+END_SRC
Which can be replaced by:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
myfunc list = foldl bar initialValue list
#+END_SRC
If you really want to know how the magic works, here is the definition of
=foldl=:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
#+END_SRC
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
foldl f z [x1,...xn]
⇔ f (... (f (f z x1) x2) ...) xn
#+END_SRC
But as Haskell is lazy, it doesn't evaluate =(f z x)= and simply pushes it
onto the stack.
This is why we generally use =foldl'= instead of =foldl=; =foldl'= is a
/strict/ version of =foldl=.
If you don't understand what lazy and strict means, don't worry, just
follow the code as if =foldl= and =foldl'= were identical.
Now our new version of =evenSum= becomes:
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v6.hs
2019-12-26 11:45:11 +00:00
-- Version 6
-- foldl' isn't accessible by default
-- we need to import it from the module Data.List
import Data.List
evenSum l = foldl' mysum 0 (filter even l)
where mysum acc value = acc + value
#+END_SRC
We can also simplify this by using directly a lambda notation.
This way we don't have to create the temporary name =mysum=.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v7.hs
2019-12-26 11:45:11 +00:00
-- Version 7
-- Generally it is considered a good practice
-- to import only the necessary function(s)
import Data.List (foldl')
evenSum l = foldl' (\x y -> x+y) 0 (filter even l)
#+END_SRC
And of course, we note that
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
(\x y -> x+y) ⇔ (+)
#+END_SRC
Finally
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v8.hs
2019-12-26 11:45:11 +00:00
-- Version 8
import Data.List (foldl')
evenSum :: Integral a => [a] -> a
evenSum l = foldl' (+) 0 (filter even l)
#+END_SRC
=foldl'= isn't the easiest function to grasp.
If you are not used to it, you should study it a bit.
To help you understand what's going on here, let's look at a step by step
evaluation:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
evenSum [1,2,3,4]
⇒ foldl' (+) 0 (filter even [1,2,3,4])
⇒ foldl' (+) 0 [2,4]
⇒ foldl' (+) (0+2) [4]
⇒ foldl' (+) 2 [4]
⇒ foldl' (+) (2+4) []
⇒ foldl' (+) 6 []
⇒ 6
#+END_SRC
Another useful higher order function is =(.)=.
The =(.)= function corresponds to mathematical composition.
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
(f . g . h) x ⇔ f ( g (h x))
#+END_SRC
We can take advantage of this operator to η-reduce our function:
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v9.hs
2019-12-26 11:45:11 +00:00
-- Version 9
import Data.List (foldl')
evenSum :: Integral a => [a] -> a
evenSum = (foldl' (+) 0) . (filter even)
#+END_SRC
Also, we could rename some parts to make it clearer:
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle evenSum_v10.hs
2019-12-26 11:45:11 +00:00
-- Version 10
import Data.List (foldl')
sum' :: (Num a) => [a] -> a
sum' = foldl' (+) 0
evenSum :: Integral a => [a] -> a
evenSum = sum' . (filter even)
#+END_SRC
It is time to discuss the direction our code has moved as we introduced
more functional idioms.
What did we gain by using higher order functions?
At first, you might think the main difference is terseness.
2019-12-24 00:23:05 +00:00
But in fact, it has more to do with better thinking.
Suppose we want to modify our function slightly, for example, to get the
sum of all even squares of elements of the list.
#+BEGIN_EXAMPLE
[1,2,3,4] ▷ [1,4,9,16] ▷ [4,16] ▷ 20
#+END_EXAMPLE
Updating version 10 is extremely easy:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
squareEvenSum = sum' . (filter even) . (map (^2))
squareEvenSum' = evenSum . (map (^2))
#+END_SRC
2019-12-25 15:35:56 +00:00
We just had to add another "transformation function".
#+BEGIN_EXAMPLE
map (^2) [1,2,3,4] ⇔ [1,4,9,16]
#+END_EXAMPLE
2019-12-24 00:23:05 +00:00
The =map= function simply applies a function to all the elements of a list.
We didn't have to modify anything /inside/ the function definition.
This makes the code more modular.
2019-12-25 15:35:56 +00:00
But in addition you can think more mathematically about your functions.
You can also use your functions interchangeably with others, as needed.
That is, you can /compose/, map, fold, filter using your new function.
Modifying version 1 is left as an exercise to the reader ☺.
If you believe we have reached the end of generalization, then know you are
very wrong.
For example, there is a way to not only use this function on lists but on
any recursive type.
If you want to know how, I suggest you to read this quite fun article:
2019-12-24 00:23:05 +00:00
[[http://eprints.eemcs.utwente.nl/7281/01/db-utwente-40501F46.pdf][Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire by
Meijer, Fokkinga and Paterson]].
This example should show you how great pure functional programming is.
2019-12-24 00:23:05 +00:00
Unfortunately, using pure functional programming isn't well suited to all
usages.
Or at least such a language hasn't been found yet.
2019-12-25 15:35:56 +00:00
One of the great powers of Haskell is the ability to create DSL (Domain
Specific Language) making it easy to change the programming paradigm.
In fact, Haskell is also great when you want to write imperative style
2019-12-24 00:23:05 +00:00
programming.
Understanding this was really hard for me to grasp when first learning
Haskell.
A lot of effort tends to go into explaining the superiority of the
functional approach.
Then when you start using an imperative style with Haskell, it can be hard
to understand when and how to use it.
But before talking about this Haskell super-power, we must talk about
another essential aspect of Haskell: /Types/.
** Types
:PROPERTIES:
:CUSTOM_ID: types
:END:
#+CAPTION: Dali, the madonna of port Lligat
[[./salvador-dali-the-madonna-of-port-lligat.jpg]]
2019-12-25 15:35:56 +00:00
#+MACRO: tldr @@html:<abbr title="too long; didn't read">tl;dr:</abbr> @@
#+BEGIN_QUOTE
2019-12-25 15:35:56 +00:00
{{{tldr}}}
- =type Name = AnotherType= is just an alias and the compiler doesn't
mark any difference between =Name= and =AnotherType=.
- =data Name = NameConstructor AnotherType= does mark a difference.
- =data= can construct structures which can be recursives.
- =deriving= is magic and creates functions for you.
#+END_QUOTE
In Haskell, types are strong and static.
2019-12-24 00:23:05 +00:00
Why is this important?
It will help you /greatly/ to avoid mistakes.
In Haskell, most bugs are caught during the compilation of your program.
2019-12-25 15:35:56 +00:00
And the main reason is because of the type checking during compilation.
Type checking makes it easy to detect where you used the wrong parameter
2019-12-24 00:23:05 +00:00
at the wrong place, for example.
*** Type inference
:PROPERTIES:
:CUSTOM_ID: type-inference
:END:
2019-12-24 00:23:05 +00:00
Static typing is generally essential for fast execution.
But most statically typed languages are bad at generalizing concepts.
Haskell's saving grace is that it can /infer/ types.
Here is a simple example, the =square= function in Haskell:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
square x = x * x
#+END_SRC
2019-12-24 00:23:05 +00:00
This function can =square= any Numeral type.
You can provide =square= with an =Int=, an =Integer=, a =Float= a
=Fractional= and even =Complex=.
Proof by example:
#+BEGIN_EXAMPLE
2019-12-25 15:35:56 +00:00
~/t/hsenv> ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> let square x = x * x
Prelude> square 2
4
Prelude> square 2.1
4.41
Prelude> :m Data.Complex
Prelude Data.Complex> square (2 :+ 1)
3.0 :+ 4.0
#+END_EXAMPLE
=x :+ y= is the notation for the complex (x + iy).
Now compare with the amount of code necessary in C:
#+BEGIN_SRC C
2019-12-26 11:45:11 +00:00
int int_square(int x) { return x*x; }
float float_square(float x) {return x*x; }
complex complex_square (complex z) {
complex tmp;
tmp.real = z.real * z.real - z.img * z.img;
tmp.img = 2 * z.img * z.real;
}
complex x,y;
y = complex_square(x);
#+END_SRC
2019-12-24 00:23:05 +00:00
For each type, you need to write a new function.
The only way to work around this problem is to use some meta-programming
trick, for example using the pre-processor.
In C++ there is a better way, C++ templates:
#+BEGIN_SRC c++
#include <iostream>
#include <complex>
using namespace std;
template<typename T>
T square(T x)
{
return x*x;
}
int main() {
// int
int sqr_of_five = square(5);
cout << sqr_of_five << endl;
// double
cout << (double)square(5.3) << endl;
// complex
cout << square( complex<double>(5,3) )
<< endl;
return 0;
}
#+END_SRC
2019-12-24 00:23:05 +00:00
C++ does a far better job than C in this regard.
2019-12-25 15:35:56 +00:00
But for more complex functions the syntax can be hard to follow: see
[[http://bartoszmilewski.com/2009/10/21/what-does-haskell-have-to-do-with-c/][this article]] for example.
In C++ you must declare that a function can work with different types.
2019-12-24 00:23:05 +00:00
In Haskell, the opposite is the case.
The function will be as general as possible by default.
2019-12-24 00:23:05 +00:00
Type inference gives Haskell the feeling of freedom that dynamically typed
languages provide.
But unlike dynamically typed languages, most errors are caught before run
time.
Generally, in Haskell:
#+BEGIN_QUOTE
"if it compiles it certainly does what you intended"
#+END_QUOTE
*** Type construction
:PROPERTIES:
:CUSTOM_ID: type-construction
:END:
2019-12-24 00:23:05 +00:00
You can construct your own types.
First, you can use aliases or type synonyms.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle type_constr_1.hs
2019-12-26 11:45:11 +00:00
type Name = String
type Color = String
2019-12-26 11:45:11 +00:00
showInfos :: Name -> Color -> String
showInfos name color = "Name: " ++ name
++ ", Color: " ++ color
name :: Name
name = "Robin"
color :: Color
color = "Blue"
main = putStrLn $ showInfos name color
#+END_SRC
2019-12-24 00:23:05 +00:00
But it doesn't protect you much.
Try to swap the two parameter of =showInfos= and run the program:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
putStrLn $ showInfos color name
#+END_SRC
It will compile and execute.
2019-12-24 00:23:05 +00:00
In fact you can replace Name, Color and String everywhere.
The compiler will treat them as completely identical.
Another method is to create your own types using the keyword =data=.
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data Name = NameConstr String
data Color = ColorConstr String
2019-12-26 11:45:11 +00:00
showInfos :: Name -> Color -> String
showInfos (NameConstr name) (ColorConstr color) =
"Name: " ++ name ++ ", Color: " ++ color
2019-12-26 11:45:11 +00:00
name = NameConstr "Robin"
color = ColorConstr "Blue"
main = putStrLn $ showInfos name color
#+END_SRC
2019-12-24 00:23:05 +00:00
Now if you switch parameters of =showInfos=, the compiler complains!
So this is a potential mistake you will never make again and the only price
2019-12-25 15:35:56 +00:00
is to be a bit more verbose.
Also notice that constructors are functions:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
NameConstr :: String -> Name
ColorConstr :: String -> Color
#+END_SRC
The syntax of =data= is mainly:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data TypeName = ConstructorName [types]
| ConstructorName2 [types]
| ...
#+END_SRC
Generally the usage is to use the same name for the DataTypeName and
DataTypeConstructor.
Example:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data Complex a = Num a => Complex a a
#+END_SRC
Also you can use the record syntax:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data DataTypeName = DataConstructor {
field1 :: [type of field1]
, field2 :: [type of field2]
...
, fieldn :: [type of fieldn] }
#+END_SRC
2019-12-24 00:23:05 +00:00
And many accessors are made for you.
Furthermore you can use another order when setting values.
Example:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data Complex a = Num a => Complex { real :: a, img :: a}
c = Complex 1.0 2.0
z = Complex { real = 3, img = 4 }
real c ⇒ 1.0
img z ⇒ 4
#+END_SRC
*** Recursive type
:PROPERTIES:
:CUSTOM_ID: recursive-type
:END:
2019-12-24 00:23:05 +00:00
You already encountered a recursive type: lists.
You can re-create lists, but with a more verbose syntax:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data List a = Empty | Cons a (List a)
#+END_SRC
If you really want to use an easier syntax you can use an infix name for
constructors.
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
infixr 5 :::
data List a = Nil | a ::: (List a)
#+END_SRC
The number after =infixr= gives the precedence.
If you want to be able to print (=Show=), read (=Read=), test equality
2019-12-24 00:23:05 +00:00
(=Eq=) and compare (=Ord=) your new data structure you can tell Haskell to
derive the appropriate functions for you.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle list.hs
2019-12-26 11:45:11 +00:00
infixr 5 :::
data List a = Nil | a ::: (List a)
deriving (Show,Read,Eq,Ord)
#+END_SRC
2019-12-24 00:23:05 +00:00
When you add =deriving (Show)= to your data declaration, Haskell creates a
=show= function for you.
We'll see soon how you can use your own =show= function.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle list.hs
2019-12-26 11:45:11 +00:00
convertList [] = Nil
convertList (x:xs) = x ::: convertList xs
#+END_SRC
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle list.hs
2019-12-26 11:45:11 +00:00
main = do
print (0 ::: 1 ::: Nil)
print (convertList [0,1])
#+END_SRC
This prints:
#+BEGIN_EXAMPLE
0 ::: (1 ::: Nil)
0 ::: (1 ::: Nil)
#+END_EXAMPLE
*** Trees
:PROPERTIES:
:CUSTOM_ID: trees
:END:
#+CAPTION: Magritte, l'Arbre
[[./magritte-l-arbre.jpg]]
We'll just give another standard example: binary trees.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle tree.hs
2019-12-26 11:45:11 +00:00
data BinTree a = Empty
| Node a (BinTree a) (BinTree a)
deriving (Show)
#+END_SRC
We will also create a function which turns a list into an ordered binary
tree.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle tree.hs
2019-12-26 11:45:11 +00:00
treeFromList :: (Ord a) => [a] -> BinTree a
treeFromList [] = Empty
treeFromList (x:xs) = Node x (treeFromList (filter (<x) xs))
(treeFromList (filter (>x) xs))
#+END_SRC
Look at how elegant this function is. In plain English:
- an empty list will be converted to an empty tree.
- a list =(x:xs)= will be converted to a tree where:
- The root is =x=
- Its left subtree is the tree created from members of the list =xs=
which are strictly inferior to =x= and
- the right subtree is the tree created from members of the list =xs=
which are strictly superior to =x=.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle tree.hs
2019-12-26 11:45:11 +00:00
main = print $ treeFromList [7,2,4,8]
#+END_SRC
You should obtain the following:
#+BEGIN_EXAMPLE
Node 7 (Node 2 Empty (Node 4 Empty Empty)) (Node 8 Empty Empty)
#+END_EXAMPLE
This is an informative but quite unpleasant representation of our tree.
2019-12-25 15:35:56 +00:00
I've added the =containers= package in the =shell.nix= file, it is time to
2019-12-25 21:17:22 +00:00
use this library which contain functions to show trees and list of trees
(forest) named =drawTree= and =drawForest=.
2019-12-25 15:35:56 +00:00
#+BEGIN_SRC haskell :tangle pretty_tree.hs
import Data.Tree (Tree,Forest(..))
import qualified Data.Tree as Tree
2019-12-25 15:35:56 +00:00
data BinTree a = Empty
| Node a (BinTree a) (BinTree a)
deriving (Eq,Ord,Show)
2019-12-25 15:35:56 +00:00
treeFromList :: (Ord a) => [a] -> BinTree a
treeFromList [] = Empty
treeFromList (x:xs) = Node x (treeFromList (filter (<x) xs))
(treeFromList (filter (>x) xs))
2019-12-25 15:35:56 +00:00
-- | Function to transform our internal BinTree type to the
-- type of Tree declared in Data.Tree (from containers package)
-- so that the function Tree.drawForest can use
binTreeToForestString :: (Show a) => BinTree a -> Forest String
binTreeToForestString Empty = []
binTreeToForestString (Node x left right) =
[Tree.Node (show x) ((binTreeToForestString left) ++ (binTreeToForestString right))]
2019-12-25 15:35:56 +00:00
-- | Function that given a BinTree print a representation of it in the console
prettyPrintTree :: (Show a) => BinTree a -> IO ()
prettyPrintTree = putStrLn . Tree.drawForest . binTreeToForestString
2019-12-25 15:35:56 +00:00
main = do
putStrLn "Int binary tree:"
prettyPrintTree $ treeFromList [7,2,4,8,1,3,6,21,12,23]
putStrLn "\nNote we could also use another type\n"
putStrLn "String binary tree:"
prettyPrintTree $
treeFromList ["foo","bar","baz","gor","yog"]
putStrLn "\nAs we can test equality and order trees, we can make tree of trees!\n"
putStrLn "\nBinary tree of Char binary trees:"
prettyPrintTree (treeFromList
(map treeFromList ["foo","bar","zara","baz","foo"]))
#+END_SRC
2019-12-25 15:35:56 +00:00
#+begin_example
~/t/hsenv> runghc pretty_tree.hs
Int binary tree:
2019-12-25 15:35:56 +00:00
7
|
+- 2
| |
| +- 1
| |
| `- 4
| |
| +- 3
| |
| `- 6
|
`- 8
|
`- 21
|
+- 12
|
`- 23
Note we could also use another type
2019-12-25 15:35:56 +00:00
String binary tree:
"foo"
|
+- "bar"
| |
| `- "baz"
|
`- "gor"
|
`- "yog"
As we can test equality and order trees, we can make tree of trees!
Binary tree of Char binary trees:
2019-12-25 15:35:56 +00:00
Node 'f' Empty (Node 'o' Empty Empty)
|
+- Node 'b' (Node 'a' Empty Empty) (Node 'r' Empty Empty)
| |
| `- Node 'b' (Node 'a' Empty Empty) (Node 'z' Empty Empty)
|
`- Node 'z' (Node 'a' Empty (Node 'r' Empty Empty)) Empty
#+end_example
2019-12-25 15:35:56 +00:00
Notice how duplicate elements aren't inserted in trees.
For exemple the Char BinTree constructed from the list =foo= is
just =f -> o=.
2019-12-25 15:35:56 +00:00
When =o= is inserted another time the second =o= is not duplicated.
But more importantly it works also for our own =BinTree= notice how the
tree for =foo= is inserted only once.
2019-12-24 00:23:05 +00:00
We have this for (almost) free, because we have declared Tree to be an
instance of =Eq=.
2019-12-24 00:23:05 +00:00
See how awesome this structure is: we can make trees containing not only
integers, strings and chars, but also other trees.
And we can even make a tree containing a tree of trees!
*** More Advanced Types
:PROPERTIES:
:CUSTOM_ID: more-advanced-types
:END:
So far we have presented types that are close to types we can see in most
typed programming languages.
But the real strength of Haskell is its type system.
So I will try to give you an idea about what makes the Haskell type system
more advanced than in most languages.
So as comparison, classical types/schemas, etc... are about products of
different sub-types:
#+begin_src haskell
data ProductType = P Int String
data PersonRecord = Person { age :: Int, name :: String }
#+end_src
Haskell has also a notion of =sum types= that I often lack a lot in other
programming languages I use.
You can define your type as a sum:
#+begin_src haskell
data Point = D1 Int | D2 Int Int | D3 Int Int Int
#+end_src
So far so good.
Sum types are already a nice thing to have, in particular within Haskell
because now the compiler can warn you if you miss a case.
For example if you write:
#+begin_src haskell
case point of
D1 x -> ...
D2 x y -> ...
#+end_src
If you compile with the =-Wall= flag (as you should always do for serious
development) then the compiler will warn you that you are forgetting some
possible value.
Those are still not really advanced types.
Advanced type are higher order types.
Those are the one that help with making your code more polymorphic.
We will start with example I alreday provided, lists:
#+begin_src haskell
data MyList a = Cons a (MyList a) | Nil
#+end_src
As you can see =MyList= takes a type parameter.
So =MyList= is a higher order type.
Generally, the intuition behind type is that a type is a data structure or
a container.
But in fact, Haskell types can be or can contain functions.
This is for example the case for =IO=.
And this is why it can be confusing to read the type of some functions.
I will take as example =sequenceA=:
#+begin_src haskell
sequenceA :: Applicative f => t (f a) -> f (t a)
#+end_src
So if you read this, it can be quite difficult to grasp what is the
intended use of this function.
A simple technique for example, is to try to replace the higher order types
(here =t= and =f=) by a type you can have some intuition about.
For example consider =t= to be the higher order type =Tree= and =f= to be
the higher order type =[]= (list).
Now you can see that =sequenceA= sill take a Tree of lists and will return
a list of trees.
For it to work =[]= need to be part of the =Applicative= class type (which
is the case).
I will not enter into the details about what =Applicative= type class is
here.
But just with this, you should start to have a better intuition about what
=sequenceA= is about.
** Infinite Structures
:PROPERTIES:
:CUSTOM_ID: infinite-structures
:END:
#+CAPTION: Escher
[[./escher_infinite_lizards.jpg]]
It is often said that Haskell is /lazy/.
2019-12-24 00:23:05 +00:00
In fact, if you are a bit pedantic, you should say that [[http://www.haskell.org/haskellwiki/Lazy_vs._non-strict][Haskell is
/non-strict/]].
Laziness is just a common implementation for non-strict languages.
Then what does "not-strict" mean? From the Haskell wiki:
#+BEGIN_QUOTE
Reduction (the mathematical term for evaluation) proceeds from the
outside in.
so if you have =(a+(b*c))= then you first reduce =+= first, then you
reduce the inner =(b*c)=
#+END_QUOTE
For example in Haskell you can do:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
-- numbers = [1,2,..]
numbers :: [Integer]
numbers = 0:map (1+) numbers
2019-12-26 11:45:11 +00:00
take' n [] = []
take' 0 l = []
take' n (x:xs) = x:take' (n-1) xs
2019-12-26 11:45:11 +00:00
main = print $ take' 10 numbers
#+END_SRC
And it stops.
How?
Instead of trying to evaluate =numbers= entirely, it evaluates elements
only when needed.
Also, note in Haskell there is a notation for infinite lists
#+BEGIN_EXAMPLE
[1..] ⇔ [1,2,3,4...]
[1,3..] ⇔ [1,3,5,7,9,11...]
#+END_EXAMPLE
and most functions will work with them. Also, there is a built-in
function =take= which is equivalent to our =take'=.
2019-12-26 11:45:11 +00:00
*** Infinite Trees
:PROPERTIES:
:CUSTOM_ID: infinite-trees
:END:
2019-12-25 21:17:22 +00:00
#+begin_src haskell :tangle infinite_tree.hs :exports none
import Data.Tree (Tree,Forest(..))
import qualified Data.Tree as Tree
data BinTree a = Empty
2019-12-25 21:17:22 +00:00
| Node a (BinTree a) (BinTree a)
deriving (Eq,Ord,Show)
-- | Function to transform our internal BinTree type to the
-- type of Tree declared in Data.Tree (from containers package)
-- so that the function Tree.drawForest can use
binTreeToForestString :: (Show a) => BinTree a -> Forest String
binTreeToForestString Empty = []
binTreeToForestString (Node x left right) =
[Tree.Node (show x) ((binTreeToForestString left) ++ (binTreeToForestString right))]
-- | Function that given a BinTree print a representation of it in the console
prettyPrintTree :: (Show a) => BinTree a -> IO ()
prettyPrintTree = putStrLn . Tree.drawForest . binTreeToForestString
#+end_src
2019-12-24 00:23:05 +00:00
Suppose we don't mind having an ordered binary tree.
Here is an infinite binary tree:
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle infinite_tree.hs
2019-12-26 11:45:11 +00:00
nullTree = Node 0 nullTree nullTree
#+END_SRC
2019-12-24 00:23:05 +00:00
A complete binary tree where each node is equal to 0.
Now I will prove you can manipulate this object using the following
function:
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle infinite_tree.hs
2019-12-26 11:45:11 +00:00
-- take all element of a BinTree
-- up to some depth
treeTakeDepth _ Empty = Empty
treeTakeDepth 0 _ = Empty
treeTakeDepth n (Node x left right) = let
nl = treeTakeDepth (n-1) left
nr = treeTakeDepth (n-1) right
in
Node x nl nr
#+END_SRC
See what occurs for this program:
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle infinite_tree.hs
2019-12-26 11:45:11 +00:00
main = prettyPrintTree (treeTakeDepth 4 nullTree)
#+END_SRC
This code compiles, runs and stops giving the following result:
#+BEGIN_EXAMPLE
2019-12-25 21:17:22 +00:00
[hs:hsenv]> runghc infinite_tree.hs
0
|
+- 0
| |
| +- 0
| | |
| | +- 0
| | |
| | `- 0
| |
| `- 0
| |
| +- 0
| |
| `- 0
|
`- 0
|
+- 0
| |
| +- 0
| |
| `- 0
|
`- 0
|
+- 0
|
`- 0
#+END_EXAMPLE
Just to heat up your neurones a bit more, let's make a slightly more
interesting tree:
2019-12-25 21:17:22 +00:00
#+begin_src haskell :tangle infinite_tree_2.hs :exports none
import Data.Tree (Tree,Forest(..))
import qualified Data.Tree as Tree
data BinTree a = Empty
| Node a (BinTree a) (BinTree a)
deriving (Eq,Ord,Show)
-- | Function to transform our internal BinTree type to the
-- type of Tree declared in Data.Tree (from containers package)
-- so that the function Tree.drawForest can use
binTreeToForestString :: (Show a) => BinTree a -> Forest String
binTreeToForestString Empty = []
binTreeToForestString (Node x left right) =
[Tree.Node (show x) ((binTreeToForestString left) ++ (binTreeToForestString right))]
-- | Function that given a BinTree print a representation of it in the console
prettyPrintTree :: (Show a) => BinTree a -> IO ()
prettyPrintTree = putStrLn . Tree.drawForest . binTreeToForestString
-- | take all element of a BinTree up to some depth
treeTakeDepth _ Empty = Empty
treeTakeDepth 0 _ = Empty
treeTakeDepth n (Node x left right) = let
nl = treeTakeDepth (n-1) left
nr = treeTakeDepth (n-1) right
in
Node x nl nr
#+end_src
#+BEGIN_SRC haskell :tangle infinite_tree_2.hs
2019-12-26 11:45:11 +00:00
iTree = Node 0 (dec iTree) (inc iTree)
where
dec (Node x l r) = Node (x-1) (dec l) (dec r)
inc (Node x l r) = Node (x+1) (inc l) (inc r)
#+END_SRC
2019-12-24 00:23:05 +00:00
Another way to create this tree is to use a higher order function.
This function should be similar to =map=, but should work on =BinTree=
instead of list.
Here is such a function:
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle infinite_tree_2.hs
2019-12-26 11:45:11 +00:00
-- apply a function to each node of Tree
treeMap :: (a -> b) -> BinTree a -> BinTree b
treeMap f Empty = Empty
treeMap f (Node x left right) = Node (f x)
(treeMap f left)
(treeMap f right)
#+END_SRC
2019-12-24 00:23:05 +00:00
/Hint/: I won't talk more about this here.
If you are interested in the generalization of =map= to other data
structures, search for functor and =fmap=.
Our definition is now:
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle infinite_tree_2.hs
2019-12-26 11:45:11 +00:00
infTreeTwo :: BinTree Int
infTreeTwo = Node 0 (treeMap (\x -> x-1) infTreeTwo)
(treeMap (\x -> x+1) infTreeTwo)
#+END_SRC
Look at the result for
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle infinite_tree_2.hs
2019-12-26 11:45:11 +00:00
main = prettyPrintTree $ treeTakeDepth 4 infTreeTwo
#+END_SRC
#+BEGIN_EXAMPLE
2019-12-25 21:17:22 +00:00
[hs:hsenv]> runghc infinite_tree_2.hs
0
|
+- -1
| |
| +- -2
| | |
| | +- -3
| | |
| | `- -1
| |
| `- 0
| |
| +- -1
| |
| `- 1
|
`- 1
|
+- 0
| |
| +- -1
| |
| `- 1
|
`- 2
|
+- 1
|
`- 3
#+END_EXAMPLE
2019-12-26 11:45:11 +00:00
*** Fibonnacci infinite list
:PROPERTIES:
:CUSTOM_ID: fibonnacci-infinite-list
:END:
2019-12-25 21:17:22 +00:00
The important things to remember.
Haskell handle infinite structures naturally mostly because it is not strict.
So you can write, infinite tree, but also, you can generate infinite list
like this common example:
#+begin_src haskell :tangle fib_lazy.hs
fib :: [Integer]
fib = 1:1:zipWith (+) fib (tail fib)
2019-12-26 11:45:11 +00:00
main = traverse print (take 20 (drop 200 fib))
2019-12-25 21:17:22 +00:00
#+end_src
Many new details in this small code. Don't worry if you do not get all details:
- =fib= is a list of Integer, not a function
- =drop n= remove n element of a list
- =take n= keep the first n elements of a list
- =zipWith op [a1,a2,a3,...] [b1,b2,b3,...]= will generate the list
=[op a1 b1,op a2 b2,op a3 b3, .... ]=
2019-12-26 11:45:11 +00:00
- =traverse= is like map but for performing effects (in this case print)
2019-12-25 21:17:22 +00:00
This progam print all fibonnacci numbers from 201 to 221 instantaneously.
Because, =fib= is a list that will be used as "cache" to compute each
number even considering the code looks a bit like a double recursion.
#+begin_example
[hs:0010-Haskell-Now]> time runghc fib_lazy.hs
453973694165307953197296969697410619233826
734544867157818093234908902110449296423351
1188518561323126046432205871807859915657177
1923063428480944139667114773918309212080528
3111581989804070186099320645726169127737705
5034645418285014325766435419644478339818233
8146227408089084511865756065370647467555938
13180872826374098837632191485015125807374171
21327100234463183349497947550385773274930109
34507973060837282187130139035400899082304280
55835073295300465536628086585786672357234389
90343046356137747723758225621187571439538669
146178119651438213260386312206974243796773058
236521166007575960984144537828161815236311727
382699285659014174244530850035136059033084785
619220451666590135228675387863297874269396512
1001919737325604309473206237898433933302481297
1621140188992194444701881625761731807571877809
2623059926317798754175087863660165740874359106
4244200115309993198876969489421897548446236915
real 0m1.000s
user 0m0.192s
sys 0m0.058s
#+end_example
2019-12-26 11:45:11 +00:00
Let's see how this work using =Debug.Trace=:
#+begin_src haskell :tangle fib_lazy_trace.hs
import Debug.Trace
-- like + but each time this is evaluated print a trace
tracedPlus x y = trace ("> " ++ show x ++ " + " ++ show y) (x + y)
fib :: [Integer]
fib = 1:1:zipWith tracedPlus fib (tail fib)
main = do
print (fib !! 10)
print (fib !! 12)
#+end_src
#+begin_example
[hs:hsenv]> runghc fib_lazy_trace.hs
> 1 + 1
> 1 + 2
> 2 + 3
> 3 + 5
> 5 + 8
> 8 + 13
> 13 + 21
> 21 + 34
> 34 + 55
89
> 55 + 89
> 89 + 144
233
#+end_example
Notice how, once computed, the list is kept in memory.
This is why when the second time we ask for the 12th element of fib we only
perform two more additions.
This is both a blessing and a curse.
A blessing if you know when to use this as in this example.
And a curse as if do not take care about lazyness it will come back at you
with memory leaks.
After a bit of experience, most Haskellers can avoid memory leaks naturally.
2019-12-26 16:27:19 +00:00
* Dive into the impure
:PROPERTIES:
2020-01-03 08:34:31 +00:00
:CUSTOM_ID: dive-into-the-impure
:END:
2019-12-24 00:23:05 +00:00
Congratulations for getting so far!
2019-12-26 16:27:19 +00:00
You have been introduced to the functional style and how to deal with
/pure/ code.
Understand code that is only evaluated without changing the state of the
external world.
2019-12-24 00:23:05 +00:00
If you are like me, you should get the functional style.
You should also understand a bit more the advantages of laziness by
default.
But you also don't really understand where to start in order to make a real
program.
And in particular:
- How do you deal with effects?
- Why is there a strange imperative-like notation for dealing with IO?
2019-12-24 00:23:05 +00:00
Be prepared, the answers might be complex.
But they are all very rewarding.
2019-12-26 16:27:19 +00:00
In this section you will first introduced about how to /use/ IO.
That should not be that hard.
Then, a harder section should explain how IO works.
And the last part will talk about how we can generalize why we learned so
far with IO to many different types.
** Deal With IO
:PROPERTIES:
:CUSTOM_ID: deal-with-io
:END:
#+CAPTION: Magritte, Carte blanche
[[./magritte_carte_blanche.jpg]]
#+BEGIN_QUOTE
2019-12-25 21:17:22 +00:00
{{{tldr}}}
2019-12-25 21:17:22 +00:00
A typical function doing =IO= looks a lot like an imperative program:
2019-12-24 00:23:05 +00:00
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f :: IO a
f = do
x <- action1
action2 x
y <- action3
action4 x y
#+END_SRC
- To set a value to an object we use =<-= .
- The type of each line is =IO *=; in this example:
2019-12-26 11:45:11 +00:00
#+begin_src haskell
- action1 :: IO b
- x :: b
- action2 x :: IO ()
- action3 :: IO c
- y :: c
- action4 x y :: IO a
#+end_src
- Few objects have the type =IO a=, this should help you choose. In
particular you cannot use pure functions directly here. To use pure
functions you could do =action2 (purefunction x)= for example.
#+END_QUOTE
2019-12-24 00:23:05 +00:00
In this section, I will explain how to use IO, not how it works.
You'll see how Haskell separates the pure from the impure parts of the
program.
2019-12-24 00:23:05 +00:00
Don't stop because you're trying to understand the details of the syntax.
Answers will come in the next section.
What to achieve?
#+BEGIN_QUOTE
2019-12-24 00:23:05 +00:00
Ask a user to enter a list of numbers.
Print the sum of the numbers.
#+END_QUOTE
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle io_sum.hs
2019-12-26 11:45:11 +00:00
toList :: String -> [Integer]
toList input = read ("[" ++ input ++ "]")
2019-12-26 11:45:11 +00:00
main = do
putStrLn "Enter a list of numbers (separated by comma):"
input <- getLine
print $ sum (toList input)
#+END_SRC
It should be straightforward to understand the behavior of this program.
Let's analyze the types in more detail.
#+BEGIN_SRC haskell
putStrLn :: String -> IO ()
getLine :: IO String
print :: Show a => a -> IO ()
#+END_SRC
2019-12-24 00:23:05 +00:00
Or more interestingly, we note that each expression in the =do= block has a
type of =IO a=.
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
main = do
putStrLn "Enter ... " :: IO ()
getLine :: IO String
print Something :: IO ()
#+END_SRC
We should also pay attention to the effect of the =<-= symbol.
#+BEGIN_SRC haskell
do
x <- something
#+END_SRC
If =something :: IO a= then =x :: a=.
2019-12-24 00:23:05 +00:00
Another important note about using =IO=: all lines in a do block must be of
one of the two forms:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
action1 :: IO a
-- in this case, generally a = ()
#+END_SRC
or
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
value <- action2 -- where
-- action2 :: IO b
-- value :: b
#+END_SRC
2019-12-24 00:23:05 +00:00
These two kinds of line will correspond to two different ways of sequencing
actions.
The meaning of this sentence should be clearer by the end of the next
section.
2019-12-24 00:23:05 +00:00
Now let's see how this program behaves.
For example, what happens if the user enters something strange?
Let's try:
#+BEGIN_EXAMPLE
2019-12-25 21:17:22 +00:00
[hs:hsenv]> runghc io_sum.hs
Enter a list of numbers (separated by comma):
foo
Prelude.read: no parse
#+END_EXAMPLE
2019-12-24 00:23:05 +00:00
Argh!
An evil error message and a crash!
Our first improvement will simply be to answer with a more friendly
message.
2019-12-24 00:23:05 +00:00
In order to do this, we must detect that something went wrong.
Here is one way to do this: use the type =Maybe=.
This is a very common type in Haskell.
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle io_sum_safe.hs
2019-12-26 11:45:11 +00:00
import Data.Maybe
import Text.Read (readMaybe)
#+END_SRC
2019-12-24 00:23:05 +00:00
What is this thing?
=Maybe= is a type which takes one parameter.
Its definition is:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
data Maybe a = Nothing | Just a
#+END_SRC
This is a nice way to tell there was an error while trying to
2019-12-24 00:23:05 +00:00
create/compute a value.
2019-12-26 11:45:11 +00:00
The =readMaybe= function is a great example of this.
2019-12-24 00:23:05 +00:00
This is a function similar to the function =read=[fn:4], but if something
goes wrong the returned value is =Nothing=.
If the value is right, it returns =Just <the value>=.
2019-12-24 00:23:05 +00:00
Now to be a bit more readable, we define a function which goes like this:
If the string has the wrong format, it will return =Nothing=.
Otherwise, for example for "1,2,3", it will return =Just [1,2,3]=.
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle io_sum_safe.hs
2019-12-26 11:45:11 +00:00
getListFromString :: String -> Maybe [Integer]
getListFromString str = readMaybe $ "[" ++ str ++ "]"
#+END_SRC
We simply have to test the value in our main function.
2019-12-25 21:17:22 +00:00
#+BEGIN_SRC haskell :tangle io_sum_safe.hs
2019-12-26 11:45:11 +00:00
main :: IO ()
main = do
putStrLn "Enter a list of numbers (separated by comma):"
input <- getLine
let maybeList = getListFromString input
case maybeList of
Just l -> print (sum l)
Nothing -> putStrLn "Bad format. Good Bye."
#+END_SRC
In case of error, we display a nice error message.
2019-12-24 00:23:05 +00:00
Note that the type of each expression in the main's =do= block remains of
the form =IO a=.
2019-12-24 00:23:05 +00:00
One very important thing to note is the type of all the functions defined
so far.
There is only one function which contains =IO= in its type: =main=.
This means main is impure.
But main uses =getListFromString= which is pure.
So it's clear just by looking at declared types which functions are pure
and which are impure.
Why does purity matter? Among the many advantages, here are three:
- It is far easier to think about pure code than impure code.
- Purity protects you from all the hard-to-reproduce bugs that are due
to side effects.
- You can evaluate pure functions in any order or in parallel without
risk.
2019-12-24 00:23:05 +00:00
This is why you should generally put as most code as possible inside pure
functions.
Our next iteration will be to prompt the user again and again until she
enters a valid answer.
We keep the first part:
2019-12-26 11:45:11 +00:00
#+BEGIN_SRC haskell :tangle io_sum_ask.hs
import Data.Maybe
import Text.Read (readMaybe)
2019-12-26 11:45:11 +00:00
getListFromString :: String -> Maybe [Integer]
getListFromString str = readMaybe $ "[" ++ str ++ "]"
#+END_SRC
Now we create a function which will ask the user for an list of integers
until the input is right.
2019-12-26 11:45:11 +00:00
#+BEGIN_SRC haskell :tangle io_sum_ask.hs
askUser :: IO [Integer]
askUser = do
putStrLn "Enter a list of numbers (separated by comma):"
input <- getLine
let maybeList = getListFromString input
case maybeList of
Just l -> return l
Nothing -> askUser
#+END_SRC
2019-12-24 00:23:05 +00:00
This function is of type =IO [Integer]=.
Such a type means that we retrieved a value of type =[Integer]= through
some IO actions.
Some people might explain while waving their hands:
#+BEGIN_QUOTE
2019-12-26 11:45:11 +00:00
«This is an =[Integer]= inside an =IO=
#+END_QUOTE
If you want to understand the details behind all of this, you'll have to
2019-12-24 00:23:05 +00:00
read the next section.
But really, if you just want to /use/ IO just practice a little and
remember to think about the type.
Finally our main function is much simpler:
2019-12-26 11:45:11 +00:00
#+BEGIN_SRC haskell :tangle io_sum_ask.hs
main :: IO ()
main = do
list <- askUser
print $ sum list
#+END_SRC
2019-12-24 00:23:05 +00:00
We have finished with our introduction to =IO=.
This was quite fast.
Here are the main things to remember:
- in the =do= block, each expression must have the type =IO a=. You are
then limited with regard to the range of expressions available. For
example, =getLine=, =print=, =putStrLn=, etc...
- Try to externalize the pure functions as much as possible.
- the =IO a= type means: an IO /action/ which returns an element of type
=a=. =IO= represents actions; under the hood, =IO a= is the type of a
function. Read the next section if you are curious.
If you practice a bit, you should be able to /use/ =IO=.
#+BEGIN_QUOTE
2019-12-26 11:45:11 +00:00
/Exercises/:
2019-12-26 11:45:11 +00:00
- Make a program that sums all of its arguments. Hint: use the
function =getArgs=.
#+END_QUOTE
** IO trick explained
:PROPERTIES:
:CUSTOM_ID: io-trick-explained
:END:
#+CAPTION: Magritte, ceci n'est pas une pipe
[[./magritte_pipe.jpg]]
#+BEGIN_QUOTE
2019-12-26 11:45:11 +00:00
{{{tldr}}}
To separate pure and impure parts, =main= is defined as a function which
modifies the state of the world.
#+BEGIN_EXAMPLE
main :: World -> World
#+END_EXAMPLE
A function is guaranteed to have side effects only if it has this type.
But look at a typical main function:
#+BEGIN_SRC haskell
main w0 =
let (v1,w1) = action1 w0 in
let (v2,w2) = action2 v1 w1 in
let (v3,w3) = action3 v2 w2 in
action4 v3 w3
#+END_SRC
We have a lot of temporary elements (here =w1=, =w2= and =w3=) which must
be passed on to the next action.
We create a function =bind= or ~(>>=)~.
With =bind= we don't need temporary names anymore.
#+BEGIN_SRC haskell
main =
action1 >>= action2 >>= action3 >>= action4
#+END_SRC
Bonus: Haskell has syntactical sugar for us:
#+BEGIN_SRC haskell
main = do
v1 <- action1
v2 <- action2 v1
v3 <- action3 v2
action4 v3
#+END_SRC
#+END_QUOTE
Why did we use this strange syntax, and what exactly is this =IO= type?
It looks a bit like magic.
For now let's just forget all about the pure parts of our program, and
focus on the impure parts:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
askUser :: IO [Integer]
askUser = do
putStrLn "Enter a list of numbers (separated by commas):"
input <- getLine
let maybeList = getListFromString input
case maybeList of
Just l -> return l
Nothing -> askUser
2019-12-26 11:45:11 +00:00
main :: IO ()
main = do
list <- askUser
print $ sum list
#+END_SRC
2019-12-24 00:23:05 +00:00
First remark: this looks imperative.
Haskell is powerful enough to make impure code look imperative.
For example, if you wish you could create a =while= in Haskell.
In fact, for dealing with =IO=, an imperative style is generally more
appropriate.
2019-12-24 00:23:05 +00:00
But you should have noticed that the notation is a bit unusual.
Here is why, in detail.
2019-12-24 00:23:05 +00:00
In an impure language, the state of the world can be seen as a huge hidden
global variable.
This hidden variable is accessible by all functions of your language.
For example, you can read and write a file in any function.
Whether a file exists or not is a difference in the possible states that
the world can take.
2019-12-24 00:23:05 +00:00
In Haskell the current state of the world is not hidden.
Rather, it is /explicitly/ said that =main= is a function that
/potentially/ changes the state of the world.
Its type is then something like:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
main :: World -> World
#+END_SRC
2019-12-24 00:23:05 +00:00
Not all functions may access this variable.
Those which have access to this variable are impure.
Functions to which the world variable isn't provided are pure[fn:5].
Haskell considers the state of the world as an input variable to =main=.
But the real type of main is closer to this one[fn:6]:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
main :: World -> ((),World)
#+END_SRC
The =()= type is the unit type. Nothing to see here.
Now let's rewrite our main function with this in mind:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
main w0 =
let (list,w1) = askUser w0 in
let (x,w2) = print (sum list,w1) in
x
#+END_SRC
First, we note that all functions which have side effects must have the
type:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
World -> (a,World)
#+END_SRC
2019-12-24 00:23:05 +00:00
where =a= is the type of the result.
For example, a =getChar= function should have the type =World -> (Char,
World)=.
2019-12-24 00:23:05 +00:00
Another thing to note is the trick to fix the order of evaluation.
In Haskell, in order to evaluate =f a b=, you have many choices:
- first eval =a= then =b= then =f a b=
- first eval =b= then =a= then =f a b=.
- eval =a= and =b= in parallel then =f a b=
This is true because we're working in a pure part of the language.
2019-12-24 00:23:05 +00:00
Now, if you look at the main function, it is clear you must eval the first
line before the second one since to evaluate the second line you have to
get a parameter given by the evaluation of the first line.
2019-12-24 00:23:05 +00:00
This trick works like a charm.
The compiler will at each step provide a pointer to a new real world id.
Under the hood, =print= will evaluate as:
- print something on the screen
- modify the id of the world
- evaluate as =((),new world id)=.
2019-12-24 00:23:05 +00:00
Now, if you look at the style of the main function, it is clearly awkward.
Let's try to do the same to the =askUser= function:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
askUser :: World -> ([Integer],World)
#+END_SRC
Before:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
askUser :: IO [Integer]
askUser = do
putStrLn "Enter a list of numbers:"
input <- getLine
let maybeList = getListFromString input in
case maybeList of
Just l -> return l
Nothing -> askUser
#+END_SRC
After:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
askUser w0 =
let (_,w1) = putStrLn "Enter a list of numbers:" in
let (input,w2) = getLine w1 in
let (l,w3) = case getListFromString input of
Just l -> (l,w2)
Nothing -> askUser w2
in
(l,w3)
#+END_SRC
This is similar, but awkward. Look at all these temporary =w?= names.
The lesson is: naive IO implementation in Pure functional languages is
awkward!
2019-12-24 00:23:05 +00:00
Fortunately, there is a better way to handle this problem.
We see a pattern.
Each line is of the form:
#+BEGIN_SRC haskell
let (y,w') = action x w in
#+END_SRC
2019-12-24 00:23:05 +00:00
Even if for some lines the first =x= argument isn't needed.
The output type is a couple, =(answer, newWorldValue)=.
Each function =f= must have a type similar to:
#+BEGIN_SRC haskell
2019-12-26 11:45:11 +00:00
f :: World -> (a,World)
#+END_SRC
Not only this, but we can also note that we always follow the same usage
pattern:
#+BEGIN_SRC haskell
let (y,w1) = action1 w0 in
let (z,w2) = action2 w1 in
let (t,w3) = action3 w2 in
...
#+END_SRC
2019-12-24 00:23:05 +00:00
Each action can take from 0 to n parameters.
And in particular, each action can take a parameter from the result of a
line above.
For example, we could also have:
#+BEGIN_SRC haskell
let (_,w1) = action1 x w0 in
let (z,w2) = action2 w1 in
let (_,w3) = action3 z w2 in
...
#+END_SRC
With, of course: =actionN w :: (World) -> (a,World)=.
#+BEGIN_QUOTE
2019-12-26 11:45:11 +00:00
*IMPORTANT*: there are only two important patterns to consider:
2019-12-26 11:45:11 +00:00
#+BEGIN_SRC haskell
let (x,w1) = action1 w0 in
let (y,w2) = action2 x w1 in
#+END_SRC
2019-12-26 11:45:11 +00:00
and
2019-12-26 11:45:11 +00:00
#+BEGIN_SRC haskell
let (_,w1) = action1 w0 in
let (y,w2) = action2 w1 in
#+END_SRC
#+END_QUOTE
2019-12-26 11:45:11 +00:00
#+CAPTION: Slave Market with the disappearing bust of Voltaire
[[./slave-market-with-the-disappearing-bust-of-voltaire.jpg]]
2019-12-24 00:23:05 +00:00
Now, we will do a magic trick.
2019-12-26 16:27:19 +00:00
We will make the temporary world symbols /disappear/.
2019-12-24 00:23:05 +00:00
We will =bind= the two lines.
Let's define the =bind= function.
Its type is quite intimidating at first:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
bind :: (World -> (a,World))
-> (a -> (World -> (b,World)))
-> (World -> (b,World))
#+END_SRC
But remember that =(World -> (a,World))= is the type for an IO action.
Now let's rename it for clarity:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
type IO a = World -> (a, World)
#+END_SRC
Some examples of functions:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
getLine :: IO String
print :: Show a => a -> IO ()
#+END_SRC
=getLine= is an IO action which takes world as a parameter and returns a
2019-12-24 00:23:05 +00:00
couple =(String, World)=.
This can be summarized as: =getLine= is of type =IO String=, which we also
see as an IO action which will return a String "embeded inside an IO".
The function =print= is also interesting.
It takes one argument which can be shown.
In fact it takes two arguments.
The first is the value to print and the other is the state of world.
It then returns a couple of type =((), World)=.
This means that it changes the state of the world, but doesn't yield any
more data.
This new =IO a= type helps us simplify the type of =bind=:
#+BEGIN_SRC haskell
bind :: IO a
-> (a -> IO b)
-> IO b
#+END_SRC
2019-12-24 00:23:05 +00:00
It says that =bind= takes two IO actions as parameters and returns another
IO action.
2019-12-26 16:27:19 +00:00
Now, remember the /important/ patterns.
The first was:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
pattern1 w0 =
let (x,w1) = action1 w0 in
let (y,w2) = action2 x w1 in
(y,w2)
#+END_SRC
Look at the types:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
action1 :: IO a
action2 :: a -> IO b
pattern1 :: IO b
#+END_SRC
Doesn't it seem familiar?
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
(bind action1 action2) w0 =
let (x, w1) = action1 w0
(y, w2) = action2 x w1
in (y, w2)
#+END_SRC
2019-12-24 00:23:05 +00:00
The idea is to hide the World argument with this function.
2019-12-26 16:27:19 +00:00
As an example imagine if we wanted to simulate:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
let (line1, w1) = getLine w0 in
let ((), w2) = print line1 in
((), w2)
#+END_SRC
Now, using the =bind= function:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
(res, w2) = (bind getLine print) w0
#+END_SRC
2019-12-26 16:27:19 +00:00
As print is of type ~Show a => a -> (World -> ((), World))~, we know
~res = ()~ (=unit= type).
2019-12-24 00:23:05 +00:00
If you didn't see what was magic here, let's try with three lines this
time.
#+BEGIN_SRC haskell
let (line1,w1) = getLine w0 in
let (line2,w2) = getLine w1 in
let ((),w3) = print (line1 ++ line2) in
((),w3)
#+END_SRC
Which is equivalent to:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
(res,w3) = (bind getLine (\line1 ->
(bind getLine (\line2 ->
print (line1 ++ line2))))) w0
#+END_SRC
2019-12-24 00:23:05 +00:00
Didn't you notice something?
Yes, no temporary World variables are used anywhere!
2019-12-26 16:27:19 +00:00
This is /MA/. /GIC/.
2019-12-24 00:23:05 +00:00
We can use a better notation.
2019-12-26 16:27:19 +00:00
Let's use ~(>>=)~ instead of =bind=.
~(>>=)~ is an infix function like ~(+)~; reminder ~3 + 4 ⇔ (+) 3 4~
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
(res,w3) = (getLine >>=
(\line1 -> getLine >>=
(\line2 -> print (line1 ++ line2)))) w0
#+END_SRC
2019-12-24 00:23:05 +00:00
Merry Christmas Everyone!
Haskell has made syntactical sugar for us:
#+BEGIN_SRC haskell
do
x <- action1
y <- action2
z <- action3
...
#+END_SRC
Is replaced by:
#+BEGIN_SRC haskell
action1 >>= (\x ->
action2 >>= (\y ->
action3 >>= (\z ->
...
)))
#+END_SRC
Note that you can use =x= in =action2= and =x= and =y= in =action3=.
2019-12-26 16:27:19 +00:00
But what about the lines not using the ~<-~?
2019-12-24 00:23:05 +00:00
Easy, another function =blindBind=:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
blindBind :: IO a -> IO b -> IO b
blindBind action1 action2 w0 =
bind action (\_ -> action2) w0
#+END_SRC
2019-12-24 00:23:05 +00:00
I didn't simplify this definition for the purposes of clarity.
Of course, we can use a better notation: we'll use the =(>>)= operator.
And
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
do
action1
action2
action3
#+END_SRC
Is transformed into
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
action1 >>
action2 >>
action3
#+END_SRC
Also, another function is quite useful.
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
putInIO :: a -> IO a
putInIO x = IO (\w -> (x,w))
#+END_SRC
2019-12-24 00:23:05 +00:00
This is the general way to put pure values inside the "IO context".
2019-12-26 16:27:19 +00:00
The general name for =putInIO= is =pure= but you also see very often =return=.
Historically =pure= was called =return=.
2019-12-24 00:23:05 +00:00
This is quite a bad name when you learn Haskell.
=return= is very different from what you might be used to.
To finish, let's translate our example:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
askUser :: IO [Integer]
askUser = do
putStrLn "Enter a list of numbers (separated by commas):"
input <- getLine
let maybeList = getListFromString input in
case maybeList of
Just l -> return l
Nothing -> askUser
2019-12-26 16:27:19 +00:00
main :: IO ()
main = do
list <- askUser
print $ sum list
#+END_SRC
Is translated into:
2019-12-26 16:27:19 +00:00
#+BEGIN_SRC haskell :tangle io_bind.hs
import Data.Maybe
import Text.Read (readMaybe)
2019-12-26 16:27:19 +00:00
getListFromString :: String -> Maybe [Integer]
getListFromString str = readMaybe $ "[" ++ str ++ "]"
askUser :: IO [Integer]
askUser =
putStrLn "Enter a list of numbers (sep. by commas):" >>
getLine >>= \input ->
let maybeList = getListFromString input in
case maybeList of
Just l -> return l
Nothing -> askUser
main :: IO ()
main = askUser >>=
\list -> print $ sum list
#+END_SRC
You can compile this code to verify that it works.
Imagine what it would look like without the =(>>)= and =(>>=)=.
** Monads
:PROPERTIES:
:CUSTOM_ID: monads
:END:
#+begin_comment
#+CAPTION: Dali, reve. It represents a weapon out of the
mouth of a tiger, itself out of the mouth of another tiger, itself out
2019-12-24 00:23:05 +00:00
of the mouth of a fish itself out of a grenade.
[[./dali_reve.jpg]]
#+end_comment
2019-12-24 00:23:05 +00:00
Now the secret can be revealed: =IO= is a /monad/.
Being a monad means you have access to some syntactical sugar with the =do=
notation.
But mainly, you have access to a coding pattern which will ease the flow of
your code.
#+BEGIN_QUOTE
2019-12-26 16:27:19 +00:00
*Important remarks*:
2019-12-26 16:27:19 +00:00
- Monad are not necessarily about effects! There are a lot of /pure/
monads.
- Monad are more about sequencing
#+END_QUOTE
2019-12-24 00:23:05 +00:00
In Haskell, =Monad= is a type class.
To be an instance of this type class, you must provide the functions
2019-12-26 16:27:19 +00:00
~(>>=)~ and ~return~.
The function ~(>>)~ is derived from ~(>>=)~.
Here is how the type class =Monad= is declared (from [[https://hackage.haskell.org/package/base-4.12.0.0/docs/src/GHC.Base.html#Monad][hackage GHC.Base]]):
#+BEGIN_SRC haskell
class Applicative m => Monad m where
-- | Sequentially compose two actions, passing any value produced
-- by the first as an argument to the second.
(>>=) :: forall a b. m a -> (a -> m b) -> m b
-- | Sequentially compose two actions, discarding any value produced
-- by the first, like sequencing operators (such as the semicolon)
-- in imperative languages.
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \_ -> k -- See Note [Recursive bindings for Applicative/Monad]
{-# INLINE (>>) #-}
-- | Inject a value into the monadic type.
return :: a -> m a
return = pure
-- | Fail with a message. This operation is not part of the
-- mathematical definition of a monad, but is invoked on pattern-match
-- failure in a @do@ expression.
--
-- As part of the MonadFail proposal (MFP), this function is moved
-- to its own class 'MonadFail' (see "Control.Monad.Fail" for more
-- details). The definition here will be removed in a future
-- release.
fail :: String -> m a
fail s = errorWithoutStackTrace s
#+END_SRC
#+BEGIN_QUOTE
2019-12-26 16:27:19 +00:00
Remarks:
- the keyword =class= is not your friend. A Haskell class is /not/ a
class of the kind you will find in object-oriented programming.
A Haskell class has a lot of similarities with Java interfaces.
A better word would have been =typeclass=, since that means a set of types.
For a type to belong to a class, all functions of the class must be
provided for this type.
- In this particular example of type class, the type =m= must be a type
that takes an argument.
For example =IO a=, but also =Maybe a=, =[a]=, etc...
- To be a useful monad, your function must obey some rules.
If your construction does not obey these rules strange things might happens:
#+BEGIN_SRC haskell
return a >>= k == k a
m >>= return == m
m >>= (\x -> k x >>= h) == (m >>= k) >>= h
2019-12-26 16:27:19 +00:00
#+END_SRC
- Furthermore the =Monad= and =Applicative= operations should relate as follow:
#+BEGIN_SRC haskell
pure = return
(<*>) = ap
#+END_SRC
The above laws imply:
#+begin_src haskell
fmap f xs = xs >>= return . f
(>>) = (*>)
#+end_src
#+END_QUOTE
*** Monad Intuition
:PROPERTIES:
:CUSTOM_ID: monad-intuition
:END:
I explained how to use the IO Monad.
In the previous chapter I explained how it works behind the scene.
Notice there is a huge difference between be a client of the Monad API and
be an architect of the Monad API but also have an intuition about what is
really a Monad.
So to try to give you an intuition, just remember a Monad is a construction
that has to do with /composition/ into higher order type constructors
(types with a parameter).
So if we consider ~(<=<)~ and ~(>=>)~ (Kleisli arrow composition) which are
defined (simplified for the purpose of this article) as
#+begin_src haskell
f >=> g = \x -> f x >>= g
g <=< f = f >=> g
#+end_src
Those operation constructed with the bind operator ~(>>=)~ are a
generalisation of ~(.)~ and ~(>>>)~ where ~f >>> g = g . f~.
2019-12-26 16:27:19 +00:00
If you can look at the type this become visible, simply compare:
#+begin_src haskell
f :: a -> b
g :: b -> c
g . f :: a -> c
f >>> g :: a -> c
2019-12-26 16:27:19 +00:00
#+end_src
with
#+begin_src haskell
f :: a -> m b
g :: b -> m c
g <=< f :: a -> m c
f >=> g :: a -> m c
#+end_src
As I said, this is a generalisation of the composition operation to
functions that returns types within a higher order type constructor.
To give you better example, consider:
- ~m = []~; ~[]~ is a higher order type constructor as it takes a type
parameter, the /kind/ of this type is ~* -> *~.
So if values have types, types have /kinds/.
You can see them in =ghci=:
#+begin_example
[hs:hsenv]> ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> :kind Int
Int :: *
Prelude> :kind []
[] :: * -> *
#+end_example
We see that the kind of =Int= is =*= so, it is a monotype, but the kind of
=[]= is =* -> *= so it takes one type parameter.
- ~a~, ~b~ to be ~Int~ and ~c~ to be ~String~
- ~f n = [n, n+1]~
- ~g n = [show n,">"++show (n+1)]~
So
#+begin_src haskell
f 2 = [2,3]
g 2 = ["2",">3"]
g 3 = ["3",">4"]
#+end_src
One would expect to /combine/ ~f~ and ~g~ such that
~(combine f g) 0 ⇒ ["2",">3","3",">4"]~.
Unfortunately ~(.)~ will not work directly and this would be cumbersome to
write.
But thanks to the Monad abstraction we can write:
#+begin_src haskell
(f >=> g) 2 ⇒ ["2",">3","3",">4"]
#+end_src
#+begin_src haskell :tangle monad_composition.hs
import Control.Monad ((>=>))
f :: Int -> [Int]
f n = [n, n+1]
g :: Int -> [String]
g n = [show n,">"++show (n+1)]
main = print $ (f >=> g) 2
#+end_src
The next chapters are simply about providing some examples of useful Monads.
*** Maybe is a monad
:PROPERTIES:
:CUSTOM_ID: maybe-is-a-monad
:END:
2019-12-24 00:23:05 +00:00
There are a lot of different types that are instances of =Monad=.
One of the easiest to describe is =Maybe=.
If you have a sequence of =Maybe= values, you can use monads to manipulate
them.
It is particularly useful to remove very deep =if..then..else..=
constructions.
2019-12-24 00:23:05 +00:00
Imagine a complex bank operation.
You are eligible to gain about 700€ only if you can afford to follow a list
of operations without your balance dipping below zero.
2019-12-26 16:27:19 +00:00
#+BEGIN_SRC haskell :tangle maybe_monad_1.hs
deposit value account = account + value
withdraw value account = account - value
eligible :: (Num a,Ord a) => a -> Bool
eligible account =
let account1 = deposit 100 account in
if (account1 < 0)
then False
else
let account2 = withdraw 200 account1 in
if (account2 < 0)
then False
else
2019-12-26 16:27:19 +00:00
let account3 = deposit 100 account2 in
if (account3 < 0)
then False
else
2019-12-26 16:27:19 +00:00
let account4 = withdraw 300 account3 in
if (account4 < 0)
then False
else
2019-12-26 16:27:19 +00:00
let account5 = deposit 1000 account4 in
if (account5 < 0)
then False
else
2019-12-26 16:27:19 +00:00
True
2019-12-26 16:27:19 +00:00
main = do
print $ eligible 300 -- True
print $ eligible 299 -- False
#+END_SRC
2019-12-24 00:23:05 +00:00
Now, let's make it better using Maybe and the fact that it is a Monad.
2019-12-26 16:27:19 +00:00
#+BEGIN_SRC haskell :tangle maybe_monad_2.hs
deposit :: (Num a) => a -> a -> Maybe a
deposit value account = Just (account + value)
2019-12-26 16:27:19 +00:00
withdraw :: (Num a,Ord a) => a -> a -> Maybe a
withdraw value account = if (account < value)
then Nothing
else Just (account - value)
2019-12-26 16:27:19 +00:00
eligible :: (Num a, Ord a) => a -> Maybe Bool
eligible account = do
account1 <- deposit 100 account
account2 <- withdraw 200 account1
account3 <- deposit 100 account2
account4 <- withdraw 300 account3
account5 <- deposit 1000 account4
Just True
2019-12-26 16:27:19 +00:00
main = do
print $ eligible 300 -- Just True
print $ eligible 299 -- Nothing
#+END_SRC
Not bad, but we can make it even better:
2019-12-26 16:27:19 +00:00
#+BEGIN_SRC haskell :tangle maybe_monad_3.hs
deposit :: (Num a) => a -> a -> Maybe a
deposit value account = Just (account + value)
2019-12-26 16:27:19 +00:00
withdraw :: (Num a,Ord a) => a -> a -> Maybe a
withdraw value account = if (account < value)
then Nothing
else Just (account - value)
2019-12-26 16:27:19 +00:00
eligible :: (Num a, Ord a) => a -> Maybe Bool
eligible account =
deposit 100 account >>=
withdraw 200 >>=
deposit 100 >>=
withdraw 300 >>=
deposit 1000 >>
return True
2019-12-26 16:27:19 +00:00
main = do
print $ eligible 300 -- Just True
print $ eligible 299 -- Nothing
#+END_SRC
We have proven that Monads are a good way to make our code more elegant.
2019-12-24 00:23:05 +00:00
Note this idea of code organization, in particular for =Maybe= can be used
in most imperative languages.
In fact, this is the kind of construction we make naturally.
#+BEGIN_QUOTE
2019-12-26 16:27:19 +00:00
An important remark:
2019-12-26 16:27:19 +00:00
The first element in the sequence being evaluated to =Nothing= will
stop the complete evaluation. This means you don't execute all lines.
You get this for free, thanks to laziness.
#+END_QUOTE
2019-12-26 16:27:19 +00:00
You could also replay these example with the definition of ~(>>=)~ for
=Maybe= in mind:
#+BEGIN_SRC haskell
2019-12-26 16:27:19 +00:00
instance Monad Maybe where
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= _ = Nothing
(Just x) >>= f = f x
2019-12-26 16:27:19 +00:00
return x = Just x
#+END_SRC
The =Maybe= monad proved to be useful while being a very simple example.
2019-12-24 00:23:05 +00:00
We saw the utility of the =IO= monad.
But now for a cooler example, lists.
*** The list monad
:PROPERTIES:
:CUSTOM_ID: the-list-monad
:END:
#+CAPTION: Golconde de Magritte
[[./golconde.jpg]]
2019-12-24 00:23:05 +00:00
The list monad helps us to simulate non-deterministic computations.
Here we go:
2019-12-26 16:27:19 +00:00
#+BEGIN_SRC haskell :tangle list_monad.hs
import Control.Monad (guard)
2019-12-26 16:27:19 +00:00
allCases = [1..10]
2019-12-26 16:27:19 +00:00
resolve :: [(Int,Int,Int)]
resolve = do
x <- allCases
y <- allCases
z <- allCases
guard $ 4*x + 2*y < z
return (x,y,z)
2019-12-26 16:27:19 +00:00
main = do
print resolve
#+END_SRC
MA. GIC. :
#+BEGIN_EXAMPLE
[(1,1,7),(1,1,8),(1,1,9),(1,1,10),(1,2,9),(1,2,10)]
#+END_EXAMPLE
2019-12-26 16:27:19 +00:00
For the list monad, there is also this syntactic sugar (à la Python):
#+BEGIN_SRC haskell
print $ [ (x,y,z) | x <- allCases,
y <- allCases,
z <- allCases,
4*x + 2*y < z ]
#+END_SRC
2019-12-24 00:23:05 +00:00
I won't list all the monads, since there are many of them.
Using monads simplifies the manipulation of several notions in pure
languages.
In particular, monads are very useful for:
- IO,
- non-deterministic computation,
- generating pseudo random numbers,
- keeping configuration state,
- writing state,
- ...
If you have followed me until here, then you've done it! You know
monads[fn:7]!
2019-12-26 16:27:19 +00:00
* Start swimming
2019-12-15 16:05:57 +00:00
:PROPERTIES:
2020-01-03 08:34:31 +00:00
:CUSTOM_ID: start-swimming
2019-12-15 16:05:57 +00:00
:END:
If you come this far, you can really congratulate yourself.
This is already what I would personnaly call a tremendous achievement.
2019-12-31 00:00:04 +00:00
This chapter will focus on how to build applications with Haskell.
How to use libraries inside your project.
Note application development is easier to introduce than library development.
Mostly because dependency management will be a lot easier.
2019-12-15 16:05:57 +00:00
2020-01-03 08:34:31 +00:00
I first intended to provide a lot more informations about how to create a
project and provide a few project examples.
But it occurs this is harder than I first expected.
So I will just provide the introduction about how to create a starting
point with many pointers for other possible options.
2019-12-15 16:05:57 +00:00
2019-12-26 16:27:19 +00:00
** Start a new project
2019-12-25 15:35:56 +00:00
:PROPERTIES:
2019-12-26 16:27:19 +00:00
:CUSTOM_ID: start-a-new-project
2019-12-25 15:35:56 +00:00
:END:
2020-01-01 22:54:53 +00:00
There are multiple starting options to create a new project.
2019-12-31 00:00:04 +00:00
The most common one is certainly to use =cabal-install=.
Another popular option is to use =stack=.
2020-01-01 22:54:53 +00:00
=stack= adds a layer on top of =cabal-install= and uses fixed set of
libraries known to compile together.
Another method is to =nix= to handle the dependencies and use
=cabal-install= for the rest.
That final choice is often considered as the most complex and difficult for
beginners.
Still this is the one I find the most elegant.
This is the method I will use in this article.
2019-12-31 00:00:04 +00:00
Still, you shall not be intimidated. Look:
- To create a new project the steps will be:
1. run =nix-shell= (to have =cabal= executable in your PATH)
2. run =cabal install -i= and answer a few questions
3. copy a few =.nix= files in your project directory
4. run another =nix-shell= in your new directory this time to enter in the
local dev env of your new project.
- To add a new library:
1. Just add it in the =.cabal= file, and enter again in your =nix-shell=.
I will just walk you through all the steps in detail.
And mostly I will tell you not to take care about most warning messages.
For our end-goal, those are mostly noise.
I am aware of the level of complexity that it looks like at first.
But really most of the apparent complexity is due to poor naming convention
and not to any fundenmental core difficulty.
*** Bootstrap a project template files
:PROPERTIES:
2019-12-31 00:00:04 +00:00
:CUSTOM_ID: bootstrap-a-project-template-files
:END:
2019-12-31 00:00:04 +00:00
1. put the [[file:shell.nix][shell.nix]] file in some directory
2. start =nix-shell --pure=
3. in the nix shell create a new directory and then
4. =cabal init -i=
5. You should use the default value for most questions except:
1. Should I generate a simple project with sensible defaults? [default: y] n
2. the package should build "Library AND Executable" (choice 3)
3. Cabal specification 2.4 (choice 4)
4. Application directory choose =app= (choice 3)
5. Library directory choose =lib= (choice 3)
6. Add informative comments, choose yes.
2019-12-31 00:00:04 +00:00
Here is a full interaction:
2019-12-26 16:27:19 +00:00
2019-12-31 00:00:04 +00:00
#+begin_src
~/dev/hsenv> nix-shell
[hs:hsenv]> mkdir my-app
[hs:hsenv]> cd my-app/
[hs:my-app]> cabal init -i
Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal
update' to download it.
Should I generate a simple project with sensible defaults? [default: y] n
What does the package build:
1) Executable
2) Library
3) Library and Executable
Your choice? 3
What is the main module of the executable:
* 1) Main.hs (does not yet exist, but will be created)
2) Main.lhs (does not yet exist, but will be created)
3) Other (specify)
Your choice? [default: Main.hs (does not yet exist, but will be created)]
Please choose version of the Cabal specification to use:
* 1) 1.10 (legacy)
2) 2.0 (+ support for Backpack, internal sub-libs, '^>=' operator)
3) 2.2 (+ support for 'common', 'elif', redundant commas, SPDX)
4) 2.4 (+ support for '**' globbing)
Your choice? [default: 1.10 (legacy)] 4
Package name? [default: my-app]
Package version? [default: 0.1.0.0]
Please choose a license:
1) GPL-2.0-only
2) GPL-3.0-only
3) LGPL-2.1-only
4) LGPL-3.0-only
5) AGPL-3.0-only
6) BSD-2-Clause
* 7) BSD-3-Clause
8) MIT
9) ISC
10) MPL-2.0
11) Apache-2.0
12) LicenseRef-PublicDomain
13) NONE
14) Other (specify)
Your choice? [default: BSD-3-Clause]
Author name? [default: Yann Esposito (Yogsototh)]
Maintainer email? [default: yann.esposito@gmail.com]
Project homepage URL?
Project synopsis?
Project category:
* 1) (none)
2) Codec
3) Concurrency
4) Control
5) Data
6) Database
7) Development
8) Distribution
9) Game
10) Graphics
11) Language
12) Math
13) Network
14) Sound
15) System
16) Testing
17) Text
18) Web
19) Other (specify)
Your choice? [default: (none)]
Application (Main.hs) directory:
* 1) (none)
2) src-exe
3) app
4) Other (specify)
Your choice? [default: (none)] 3
Library source directory:
* 1) (none)
2) src
3) lib
4) src-lib
5) Other (specify)
Your choice? [default: (none)] 2
Should I generate a test suite for the library? [default: y]
Test directory:
* 1) test
2) Other (specify)
Your choice? [default: test]
What base language is the package written in:
* 1) Haskell2010
2) Haskell98
3) Other (specify)
Your choice? [default: Haskell2010]
Add informative comments to each field in the cabal file (y/n)? [default: n] y
Guessing dependencies...
Generating LICENSE...
Generating Setup.hs...
Generating CHANGELOG.md...
Generating src/MyLib.hs...
Generating app/Main.hs...
Generating test/MyLibTest.hs...
Generating my-app.cabal...
Warning: no synopsis given. You should edit the .cabal file and add one.
You may want to edit the .cabal file and add a Description field.
[hs:my-app]>
#+end_src
#+begin_notes
Please ignore the following warning:
#+begin_example
Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal
update' to download it.
#+end_example
Nix should take care of handling Haskell libraries not =cabal-install=.
No need to run =cabal update=.
#+end_notes
After this step you should end up with the following set of files:
#+begin_example
[hs:my-app]> tree
.
├── CHANGELOG.md
├── LICENSE
├── Setup.hs
├── app
│   └── Main.hs
├── src
│   └── MyLib.hs
├── my-app.cabal
└── test
└── MyLibTest.hs
3 directories, 7 files
#+end_example
*** Create a few nix files
:PROPERTIES:
2019-12-31 00:00:04 +00:00
:CUSTOM_ID: create-a-few-nix-files
:END:
2019-12-31 00:00:04 +00:00
#+begin_notes
The goal of this tutorial is not to make you learn =nix= because it is a
bit complex, but to explain you a bit, =nix= use a a /configuration language/
and not just a /configuration format/.
So to configure your =nix= environment you endup writing a /nix expression/
in this /nix language/.
And thus you can call the content of one nix-file in another one for
example, or use variables.
#+end_notes
The first file to create is the one that will pin the versions of all your
packages and libraries:
#+begin_src nix :tangle my-app/nixpkgs.nix :mkdirp t
import (fetchTarball https://github.com/NixOS/nixpkgs/archive/19.09.tar.gz) {}
#+end_src
The second file is the =default.nix= file:
#+begin_src nix :tangle my-app/default.nix :mkdirp t
{ nixpkgs ? import ./nixpkgs.nix
, compiler ? "default"
, doBenchmark ? false }:
let
inherit (nixpkgs) pkgs;
name = "my-app";
haskellPackages = pkgs.haskellPackages;
variant = if doBenchmark
then pkgs.haskell.lib.doBenchmark
else pkgs.lib.id;
drv = haskellPackages.callCabal2nix name ./. {};
in
{
my_project = drv;
shell = haskellPackages.shellFor {
# generate hoogle doc
withHoogle = true;
packages = p: [drv];
# packages dependencies (by default haskellPackages)
buildInputs = with haskellPackages;
[ hlint
ghcid
cabal-install
cabal2nix
hindent
# # if you want to add some system lib like ncurses
# # you could by writing it like:
# pkgs.ncurses
];
# nice prompt for the nix-shell
shellHook = ''
export PS1="\n\[[${name}:\033[1;32m\]\W\[\033[0m\]]> "
2019-12-31 00:00:04 +00:00
'';
};
}
#+end_src
It uses the =nixpkgs.nix= file.
But also you can configure it to enable/disable benchmarks while building
your application.
I do not expect you to understand what is really going on here, but a short
explanation is this file take cares of:
1. use the pinned version of nixpkgs and should provide a working set of
haskell libraries.
2. read you =.cabal= file and find the set of libraries you depends on so
=nix= will be able to download them.
3. download a few useful packages for Haskell development, in particular
=hlint=, =ghcid=, =cabal-install=, =cabal2nix= and =hindent=.
I will talk about those tools later.
4. take care of handling the =nix-shell= prompt so you should see the name
of your project.
The only things you should manipulate for a new fresh project should be the
=name= and perhaps the =buildInputs= list to add a few more libraries that
could be either Haskell libraries or any library =nix= know about (for
example =ncurses=, in that case you should write it =pkgs.ncurses=).
The two last file simply use the =default.nix= file:
The =shell.nix= file:
#+begin_src nix :tangle my-app/shell.nix :mkdirp t
(import ./. {}).shell
#+end_src
And =release.nix=:
#+begin_src nix :tangle my-app/release.nix :mkdirp t
let
def = import ./. {};
in
{ my_project = def.my_project; }
#+end_src
So download those files as well as this =.gitignore= file:
#+begin_src gitignore :tangle my-app/.gitignore :mkdirp t
dist-newstyle/
result
#+end_src
*** Checking your environment
:PROPERTIES:
:CUSTOM_ID: checking-your-environment
:END:
Now you should see those files in your project:
#+begin_example
[hs:my-app]> tree
.
├── CHANGELOG.md
├── LICENSE
├── Setup.hs
├── app
│   └── Main.hs
├── default.nix
├── src
│   └── MyLib.hs
├── my-app.cabal
├── nixpkgs.nix
├── release.nix
├── shell.nix
└── test
└── MyLibTest.hs
3 directories, 11 files
#+end_example
You shall now enter =nix-shell= again, but in your =my-app= directory this time.
#+begin_example
[hs:my-app]> nix-shell
warning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring
building '/nix/store/j3hi4wm9996wfga61arc2917klfgspwr-cabal2nix-my-app.drv'...
installing
warning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels/nixpkgs' does not exist, ignoring
warning: file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I), at (string):1:9; will use bash from your environment
[my-app:my-app]> which ghcid
/nix/store/ckps9wgbmpckxdvs42p6sqz64dfqiv35-ghcid-0.7.5-bin/bin/ghcid
[my-app:my-app]> cabal run my-app
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
- my-app-0.1.0.0 (src) (first run)
- my-app-0.1.0.0 (exe:my-app) (first run)
Configuring library for my-app-0.1.0.0..
Preprocessing library for my-app-0.1.0.0..
Building library for my-app-0.1.0.0..
[1 of 1] Compiling MyLib ( src/MyLib.hs, /Users/y/hsenv/my-app/dist-newstyle/build/x86_64-osx/ghc-8.6.5/my-app-0.1.0.0/build/MyLib.o )
Configuring executable 'my-app' for my-app-0.1.0.0..
Preprocessing executable 'my-app' for my-app-0.1.0.0..
Building executable 'my-app' for my-app-0.1.0.0..
[1 of 1] Compiling Main ( app/Main.hs, /Users/y/hsenv/my-app/dist-newstyle/build/x86_64-osx/ghc-8.6.5/my-app-0.1.0.0/x/my-app/build/my-app/my-app-tmp/Main.o )
Linking /Users/y/hs-env/my-app/dist-newstyle/build/x86_64-osx/ghc-8.6.5/my-app-0.1.0.0/x/my-app/build/my-app/my-app ...
Hello, Haskell!
someFunc
#+end_example
Great! It works!
Try to run it again:
#+begin_example
[my-app:my-app]> cabal run my-app
Up to date
Hello, Haskell!
someFunc
#+end_example
This time, the compilation is not done again.
=cabal= is smart enough not to repeat the compilation again.
#+begin_notes
You could also use =nix-build= to compile your app.
I think this is nice to do for releases.
But for development, you should use =cabal=.
#+end_notes
*** Add a library
:PROPERTIES:
:CUSTOM_ID: add-a-library
:END:
2019-12-31 00:00:04 +00:00
#+begin_notes
{{{tldr}}} do not be afraid by the lenght of this section in fact, this is
straightforward.
I just take a lot of time to go through all intermediate steps.
1. add the library in the =build-depends= inside your =.cabal= file.
2. restart =nix-shell= to download the new dependencies.
#+end_notes
If you open the =my-app.cabal= file in an editor you should see a =library=
section and and =executable my-app= section.
In particular for each section you can see a =build-depends= sub-section as
this one:
#+begin_src cabal
...
library
...
build-depends: base ^>=4.12.0.0
...
executable my-app
...
build-depends: base ^>=4.12.0.0, my-app
...
#+end_src
#+begin_notes
The =^>=4.12.0.0= means that it should use the latest non breaking version
of the haskell package =base=. The author of the =base= package are
responsible not to break the API for minor releases.
Haskell libs uses a 4 number versionning quite similar to the semantic
versionning scheme with just another minor number for non visible changes.
I will not argue much, but mainly, semantic versionning and Haskell
versionning are just a "right to break things to your users".
I don't want to talk a lot more about this, but, it would be nice if more
people would watch this talk[fn:9] related to versionning.
[fn:9]: [[https://www.youtube.com/watch?v=oyLBGkS5ICk][Spec-ulation Keynote - Rich Hickey]]
If you want to know more about Haskell versionning convention:
https://pvp.haskell.org
#+end_notes
Add the =protolude= lib in the library build-depends like this:
#+begin_src cabal
...
library
...
build-depends: base ^>=4.12.0.0,
protolude
...
executable my-app
...
build-depends: base ^>=4.12.0.0, my-app
...
#+end_src
#+begin_notes
2020-01-01 22:54:53 +00:00
I did not include a version constraint here.
2019-12-31 00:00:04 +00:00
This is ok if you do not deploy your library publicly.
This would be absolutely awful if you deploy your library publicly.
So while developing a private app nobody can see except you, nothing is
wrong with this.
But I would encourage you to write those version bounds.
It is sane to do that, but be warned that your lib might rot if you want it
to be part of a working set of libs.
So you might be pinged time to time to update some bounds or to adap your
code to the breaking change of a lib you are using.
Do not think too much about this.
This is generally quite trivial work to do to maintain your lib into a
working lib set.
#+end_notes
Now that you have added =protolude= modify slightly the code of your app to
use it.
Change the code inside =src/MyLib.hs=:
#+begin_src haskell :tangle my-app/src/MyLib.hs :mkdirp t
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module MyLib (someFunc) where
import Protolude
someFunc :: IO ()
someFunc = putText "someFunc"
#+end_src
Please do not try to search right now about what this change is doing.
It should work mostly as before.
The goal here is just to check that you can use another library easily.
So now you should get out of the =nix-shell= because =nix= dependencies
changed.
Generally just type =^D= (=Ctrl-d=) then launch =nix-shell --pure=.
#+begin_example
[my-app:my-app]> cabal build
Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal
update' to download it.
Resolving dependencies...
cabal: Could not resolve dependencies:
[__0] trying: my-app-0.1.0.0 (user goal)
[__1] unknown package: protolude (dependency of my-app)
[__1] fail (backjumping, conflict set: my-app, protolude)
After searching the rest of the dependency tree exhaustively, these were the
goals I've had most trouble fulfilling: my-app, protolude
[my-app:my-app]> exit
[hs:my-app]> nix-shell
warning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring
building '/nix/store/sr4838rnmzn30j3qc5ray4i2n6n0p8pq-cabal2nix-my-app.drv'...
installing
[my-app:my-app]> cabal build
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
- my-app-0.1.0.0 (lib) (file src/MyLib.hs changed)
- my-app-0.1.0.0 (exe:my-app) (configuration changed)
Preprocessing library for my-app-0.1.0.0..
Building library for my-app-0.1.0.0..
[1 of 1] Compiling MyLib ( src/MyLib.hs, .../my-app/dist-newstyle/build/x86_64-osx/ghc-8.6.5/my-app-0.1.0.0/build/MyLib.o )
Configuring executable 'my-app' for my-app-0.1.0.0..
Preprocessing executable 'my-app' for my-app-0.1.0.0..
Building executable 'my-app' for my-app-0.1.0.0..
[1 of 1] Compiling Main ( app/Main.hs, .../my-app/dist-newstyle/build/x86_64-osx/ghc-8.6.5/my-app-0.1.0.0/x/my-app/build/my-app/my-app-tmp/Main.o ) [MyLib changed]
Linking .../my-app/dist-newstyle/build/x86_64-osx/ghc-8.6.5/my-app-0.1.0.0/x/my-app/build/my-app/my-app ...
[my-app:my-app]> cabal run my-app
Up to date
Hello, Haskell!
someFunc
#+end_example
Yes!
*** Better defaults
:PROPERTIES:
:CUSTOM_ID: better-defaults
:END:
Some of the default values in the cabal file are not the best for a
professional and serious application development unfortunately.
First, let create a new block called =common professional-properties=
that will help us not repeat ourselve much and show more warning during compilation.
#+begin_src cabal
common professional-properties
default-language: Haskell2010
build-depends:
base ^>=4.12.0.0
ghc-options:
-Wall
-Wcompat
-Wincomplete-uni-patterns
-Wredundant-constraints
-Wnoncanonical-monad-instances
-- -Werror
-- -O2
#+end_src
This should then be used with import in all other sections (=library=,
=executable= and =test=).
Also add the =ghc-options= to enable the use of all core by default.
This might not always be a good idea.
But I think this is generally a better default for most modern application.
#+begin_src cabal
library
import: professional-properties
build-depends: protolude
...
executable my-app
import: professional-properties
ghc-options:
-- enable parallelism
-threaded
"-with-rtsopts=-N"
...
test-suite my-app-test
import: professional-properties
...
#+end_src
You can download the final cabal file: [[file:my-app/my-app.cabal][my-app.cabal]]
2020-01-03 08:34:31 +00:00
* Conclusion
2019-12-31 00:00:04 +00:00
:PROPERTIES:
2020-01-03 08:34:31 +00:00
:CUSTOM_ID: conclusion
2019-12-31 00:00:04 +00:00
:END:
2020-01-03 08:34:31 +00:00
This was a re-written fast Haskell tutorial.
I will certainly complete this with more advanced tutorial explaining how
to write a few Haskell projects.
2020-01-01 22:54:53 +00:00
2020-01-03 08:34:31 +00:00
Thanks for reading it.
2020-01-03 08:34:31 +00:00
* Thanks
2019-12-26 16:27:19 +00:00
:PROPERTIES:
2020-01-03 08:34:31 +00:00
:CUSTOM_ID: thanks
2019-12-26 16:27:19 +00:00
:END:
2019-12-24 00:23:05 +00:00
Thanks to [[http://reddit.com/r/haskell][=/r/haskell=]] and [[http://reddit.com/r/programming][=/r/programming=]].
Your comment were most than welcome.
2019-12-24 00:23:05 +00:00
Particularly, I want to thank [[https://github.com/Emm][Emm]] a thousand times for the time he spent on
correcting my English.
Thank you man.
[fn:1] Even if most recent languages try to hide them, they are present.
[fn:2] I know I'm cheating. But I will talk about non-strictness later.
[fn:3] For the brave, a more complete explanation of pattern matching
can be found
[[http://www.cs.auckland.ac.nz/references/haskell/haskell-intro-html/patterns.html][here]].
[fn:4] Which is itself very similar to the javascript =eval= function,
that is applied to a string containing JSON.
[fn:5] There are some /unsafe/ exceptions to this rule. But you
shouldn't see such use in a real application except maybe for
debugging purposes.
2019-12-26 11:45:11 +00:00
[fn:6] For the curious ones, the real type looks like
=data IO a = IO {unIO :: State# RealWorld -> (# State# RealWorld, a #)}=.
2019-12-26 11:45:11 +00:00
All the =#= has to do with optimisation.
I swapped the fields in my example.
But this is the basic idea.
As of today, the definition of =IO= is no more visible into =base=.
We have the following explanation in [[http://hackage.haskell.org/package/base-4.12.0.0/docs/src/GHC.IO.html][=GHC.IO.hs=]]:
#+begin_quote
#+begin_src
The IO Monad is just an instance of the ST monad, where the state is
the real world. We use the exception mechanism (in GHC.Exception) to
implement IO exceptions.
NOTE: The IO representation is deeply wired in to various parts of the
system. The following list may or may not be exhaustive:
Compiler - types of various primitives in PrimOp.hs
RTS - forceIO (StgStartup.cmm)
- catchzh_fast, (un)?blockAsyncExceptionszh_fast, raisezh_fast
(Exception.cmm)
- raiseAsync (RaiseAsync.c)
Prelude - GHC.IO.hs, and several other places including
GHC.Exception.hs.
Libraries - parts of hslibs/lang.
--SDM
#+end_src
2019-12-26 11:45:11 +00:00
#+end_quote
[fn:7] Well, you'll certainly need to practice a bit to get used to them
and to understand when you can use them and create your own. But
you already made a big step in this direction.