1
1
mirror of https://github.com/thma/WhyHaskellMatters.git synced 2024-10-26 14:32:24 +03:00

Implementing Num & Eq

This commit is contained in:
Mahler, Thomas 2020-04-07 20:51:55 +02:00
parent 97d3f36361
commit e97bd35c51
3 changed files with 162 additions and 37 deletions

160
README.md
View File

@ -501,6 +501,8 @@ data constructor `P` takes a value of type `a` and a value of type `b` and retur
(the data constructor `P` has the signature `P :: a -> b -> Pair a b`).
The type `Pair` can now be used to create many different concrete data types it is thus
called a *polymorphic* data type.
As the Polymorphism is defined by type variables, i.e. parameters to the type declarations, this mechanism is
called *parametric polymorphism*.
As pairs and n-tuples are so frequently used, the Haskell language designers have added some syntactic sugar to
work effortlessly with them.
@ -982,6 +984,149 @@ sign x = cond [(x > 0 , 1 )
-1
```
## Type Classes
Now we come to one of the most distinguishing features of Haskell: *type classes*.
In the section [Polymorphic Data Types](#polymorphic-data-types) we have seen that type variables (or parameters) allow
type declarations to be polymorphic like in:
```haskell
data [a] = [] | a : [a]
```
This approach is called *parametric polymorphism* and is used in several programming languages.
Type classes on the other hand address *ad hoc polymorphism* of data types. This approach is also known as
*overloading*.
To get a first intuition let's start with a simple example.
We would like to be able to use characters (represented by the data type `Char`) as if they were numbers.
E.g. we would like to be able to things like:
```haskell
λ> 'A' + 25
'Z'
-- please note that in Haskell a string is List of characters: type String = [Char]
λ> map (+ 5) "hello world"
"mjqqt%|twqi"
λ> map (\c -> c - 5) "mjqqt%|twqi"
"hello world"
```
To enable this we will have to *overload* the infix operators `(+)` and `(-)` to work not only on numbers but also on characters.
Now, let's have a look at the type signature of the `(+)` operator:
```haskell
λ> :type (+)
(+) :: Num a => a -> a -> a
```
So `(+)` is not just declared to be of type `(+) :: a -> a -> a` but it contains a **constraint** on the type variable `a`,
namely `Num a =>`.
The whole type signature of `(+)` can be read as: for all types `a` that are members of the type class `Num` the operator `(+)` has the type
`a -> a -> a`.
Next we obtain more information on the type class `Num`:
```haskell
λ> :info Num
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
{-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
-- Defined in `GHC.Num'
instance Num Word -- Defined in `GHC.Num'
instance Num Integer -- Defined in `GHC.Num'
instance Num Int -- Defined in `GHC.Num'
instance Num Float -- Defined in `GHC.Float'
instance Num Double -- Defined in `GHC.Float'
```
This information details what functions a type `a` has to implement to be used as an instance of the `Num` type class.
The line `{-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}` tells us what a minimal complete implementation
has to provide.
It also tells us that the types `Word`, `Integer`, `Int`, `Float` and `Double` are instances of the `Num` type class.
This is all we need to know to make the type `Char` an instance of the `Num` type class, so without further ado we
dive into the implementation (please note that `fromEnum` converts a `Char` into an `Int` and `toEnum` converts
an `Int` into an `Char`):
```haskell
instance Num Char where
a + b = toEnum (fromEnum a + fromEnum b)
a - b = toEnum (fromEnum a - fromEnum b)
a * b = toEnum (fromEnum a * fromEnum b)
abs c = c
signum = toEnum . signum . fromEnum
fromInteger = toEnum . fromInteger
negate c = c
```
This piece of code makes the type `Char` an instance of the `Num` type class. We can then use `(+)` and `(-) as demonstrated
above.
Originally the idea for type classes came up to provide overloading of arithmetic operators
in order to use the same operators across all numeric types.
But the type classes concept proved to be useful in a variety of other cases as well.
This has lead to a rich sets of type classes provided by the Haskell base library and
a wealth of programming techniques that make use of this powerful concept.
Here comes a graphic overview of some of the most important type classes in the Haskell base library:
![The hierarchy of basic type classes](https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Base-classes.svg/510px-Base-classes.svg.png)
I won't go all of these but I I'll cover some of the most important ones.
Let's start with Eq:
```haskell
class Eq a where
(==), (/=) :: a -> a -> Bool
-- Minimal complete definition:
-- (==) or (/=)
x /= y = not (x == y)
x == y = not (x /= y)
```
This definition states two things:
- if a type `a` is to be made an instance of the class `Eq` it must support the
functions `(==)` and `(/=)` both of them having type `a -> a -> Bool`.
- `Eq` provides default definitions for `(==)` and `(/=)` in terms of each other.
As a consequence, there is no need for a type in `Eq` to provide both definitions -
given one of them, the other will be generated automatically.
Now we can turn some of the data types that we defined in the section on
[Algebraic Data Types](#algebraic-data-types) into instances of the `Eq` type class.
```haskell
data Status = Green | Yellow | Red
data Severity = Low | Middle | High
data PairStatusSeverity = PSS Status Severity
instance Eq PairStatusSeverity where
(PSS sta1 sev1) == (PSS sta2 sev2) = (sta1 == sta2) && (sev1 == sev2)
```
- Eq
- Read, Show
- deriving
- complex interfaces
- interpreter style
---
@ -989,35 +1134,20 @@ sign x = cond [(x > 0 , 1 )
This is my scrap book (don't look at it)
- Funktionen sind 1st class citizens (higher order functions, Funktionen könen neue Funktionen erzeugen und andere Funktionen als Argumente haben)
- Abstraktion über Resource management und Abarbeitung (=> deklarativ)
- Immutability ("Variables do not Vary")
- Seiteneffekte müssen in Funktions signaturen explizit gemacht werden.
D.H wenn keine Seiteneffekt angegeben ist, verhindert der Compiler, dass welche auftreten !
Damit lässt sich Seiteneffektfreie Programmierung realisieren ("Purity")
- Evaluierung in Haskell ist "non-strict" (aka "lazy"). Damit lassen sich z.B. abzählbar unendliche Mengen (z.B. alle Primzahlen) sehr elegant beschreiben.
Aber auch kontrollstrukturen lassen sich so selbst bauen (super für DSLs)
- Static and Strong typing (Es gibt kein Casting)
- Type Inferenz. Der Compiler kann die Typ-Signaturen von Funktionen selbst ermitteln. (Eine explizite Signatur ist aber möglich und oft auch sehr hilfreich für Doku und um Klarheit über Code zu gewinnen.)
- Polymorphie (Z.B für "operator overloading", Generische Container Datentypen, etc. auf Basis von "TypKlassen")
- Algebraische Datentypen (Summentypen + Produkttypen) AD helfen typische Fehler, die man von OO Polymorphie kenn zu vermeiden. Sie erlauben es, Code für viele Oerationen auf Datentypen komplett automatisch vom Compiler generieren zu lassen).
- Pattern Matching erlaubt eine sehr klare Verarbeitung von ADTs
- Eleganz: Viele Algorithmen lassen sich sehr kompakt und nah an der Problemdomäne formulieren.
- Data Encapsulation durch Module
- Weniger Bugs durch
- Purity, keine Seiteneffekte

View File

@ -4,19 +4,20 @@ module AlgebraicDataTypes where
import Control.Monad ((>=>))
-- a simple sum type
data Status = Green | Yellow | Red deriving (Eq, Show)
data Severity = Low | Middle | High deriving (Eq, Show)
data Status = Green | Yellow | Red --deriving (Eq, Show)
data Severity = Low | Middle | High --deriving (Eq, Show)
severity :: Status -> Severity
severity Green = Low
severity Yellow = Middle
severity Red = High
data StatusSeverityTuple = SST Status Severity deriving (Eq, Show)
data StatusSeverityTuple = SST Status Severity --deriving (Eq, Show)
data PairStatusSeverity = PSS Status Severity
-- a simple product type
data Pair = P Status Severity deriving (Show)
data Pair = P Status Severity --deriving (Show)
data Tree a = Leaf a | Node (Tree a) (Tree a) deriving (Show)

View File

@ -1,24 +1,18 @@
module TypeClasses where
data Color = Blue | Yellow | Red | Green | White deriving (Eq, Show)
import AlgebraicDataTypes (Status (..), Severity (..), PairStatusSeverity (..))
instance Num Char where
a + b = toEnum (fromEnum a + fromEnum b)
a * b = toEnum (fromEnum a * fromEnum b)
abs c = c
signum c = toEnum (signum (fromEnum c))
a + b = toEnum (fromEnum a + fromEnum b)
a - b = toEnum (fromEnum a - fromEnum b)
a * b = toEnum (fromEnum a * fromEnum b)
abs = id
signum = toEnum . signum . fromEnum
fromInteger = toEnum . fromInteger
negate c = c
negate = id
instance Eq PairStatusSeverity where
(PSS sta1 sev1) == (PSS sta2 sev2) = (sta1 == sta2) && (sev1 == sev2)
-- Y + R = Orange
-- R + B = Violett
-- B + G = Green
-- G + Y = Yellow-Green
-- Y + Orange = Yello-Orange
-- Orange + Red = Orange-Red
-- R + Violett = Red-Violett
-- Violett + B = Blue-Violett
-- B + G = Blue-Green
-- _ + _ = Black
instance Eq Status where