[FMFP] Restructure summary

This commit is contained in:
2026-07-06 09:49:27 +02:00
parent 67500a14cb
commit 294363f3c8
66 changed files with 127 additions and 101 deletions
@@ -0,0 +1,6 @@
A great type system is essential for all programming languages, but especially so for functional programming languages.
The issue however is that the problem of deciding which expressions are good and which ones aren't is undecidable.
Thus, languages only allow a subset of good expressions.
The goal is to make the type system as unrestrictive as possible while still retaining quick, static code analysis.
@@ -0,0 +1,15 @@
\subsection{Mini-Haskell}
This is a stripped down version of Haskell, used here to explore the type system Haskell uses
\subsubsection{Syntax}
Programs are terms, the core is the lambda-calculus, where $\cV$ is the set of variables and $\Z$ the set of integers:
\[
t :: = \cV \divider (\lambda x. t) \divider (t_1 t_2) \divider True \divider False \divider (\textbf{iszero } t) \divider \Z \divider (t_1 + t_2) \divider t_1 * t_2
\divider \textbf{if } t_0 \textbf{ then } t_1 \textbf{ else } t_2 \divider (t_1, t_2) \divider (\textbf{fst } t) \divider (\textbf{snd } t)
\]
It is easily possible to add additonal syntax and types and we employ syntactic sugar, such as omitting parenthesis.
The types are given by $\tau ::= \cV_T \divider \texttt{Bool} \divider \texttt{Int} \divider (\tau, \tau) \divider (\tau \rightarrow \tau)$, where $\cV_T$ is a set of type variables.
The type system is based on typing judgement of form $\Gamma \vdash t :: r$, where $\Gamma$ is a set of bindings $x_i : \tau_i$
that maps variables to types and can be understood as a typing symbol table.
@@ -0,0 +1,23 @@
\subsubsection{Lambda calculus}
To prove that types are correct, the lambda calculus comes in handy.
The proofs are again structured as derivation trees.
\paragraph{Core rules for Lambda-Calculus}
\[
\begin{prooftree}
\infer0[Var]{\Gamma, x : \tau \vdash x :: \tau}
\end{prooftree}
\qquad
\begin{prooftree}
\hypo{\Gamma, x : \sigma \vdash t :: \tau}
\infer1[Abs]{\Gamma \vdash (\forall x. t) :: \sigma \rightarrow \tau}
\end{prooftree}
\qquad
\begin{prooftree}
\hypo{\Gamma \vdash t_1 :: \sigma \rightarrow \tau}
\hypo{\Gamma \vdash t_2 :: \sigma}
\infer2[App]{\Gamma \vdash (t_1 t_2) :: \tau}
\end{prooftree}
\]
For rule \texttt{Abs}, we require that $x \notin \Gamma$
@@ -0,0 +1,58 @@
\subsubsection{Further rules for mini-Haskell}
\paragraph{Base types}
\[
\begin{prooftree}
\infer0[Int]{\Gamma \vdash n :: \texttt{Int}}
\end{prooftree}
\qquad
\begin{prooftree}
\infer0[True]{\Gamma \vdash \texttt{True} :: \texttt{Bool}}
\end{prooftree}
\qquad
\begin{prooftree}
\infer0[False]{\Gamma \vdash \texttt{False} :: \texttt{Bool}}
\end{prooftree}
\]
\paragraph{Operations}
Let $\textbf{op} \in \{ +, * \}$
\[
\begin{prooftree}
\hypo{\Gamma \vdash t :: \texttt{Int}}
\infer1[iszero]{\Gamma \vdash (\textbf{iszero } t) :: \texttt{Bool}}
\end{prooftree}
\qquad
\begin{prooftree}
\hypo{\Gamma \vdash t_1 :: \texttt{Int}}
\hypo{\Gamma \vdash t_2 :: \texttt{Int}}
\infer2[BinOp]{\Gamma \vdash (t_1 \textbf{ op } t_2) :: \texttt{Int}}
\end{prooftree}
\]
\[
\qquad
\begin{prooftree}
\hypo{\Gamma \vdash t_0 :: \texttt{Bool}}
\hypo{\Gamma \vdash t_1 :: \tau}
\hypo{\Gamma \vdash t_2 :: \tau}
\infer3[if]{\Gamma \vdash (\textbf{if } t_0 \textbf{ then } t_1 \textbf{ else } t_2) :: \tau}
\end{prooftree}
\]
\paragraph{Tuples}
\[
\begin{prooftree}
\hypo{\Gamma \vdash t_1 :: \tau_1}
\hypo{\Gamma \vdash t_2 :: \tau_2}
\infer2[Tuple]{\Gamma \vdash (t_1, t_2) :: (\tau_1, \tau_2)}
\end{prooftree}
\qquad
\begin{prooftree}
\hypo{\Gamma \vdash t :: (\tau_1, \tau_2)}
\infer1[fst]{\Gamma \vdash (\textbf{fst } t) :: \tau_1}
\end{prooftree}
\qquad
\begin{prooftree}
\hypo{\Gamma \vdash t :: (\tau_1, \tau_2)}
\infer1[snd]{\Gamma \vdash (\textbf{snd } t) :: \tau_2}
\end{prooftree}
\]
@@ -0,0 +1,26 @@
\subsubsection{Type inference}
Type inference in general fails, if two (or more) branches fail to resolve to unifiable types.
We start a \bi{type judgement} with judgement $\vdash t :: \tau_0$, then build a derivation tree bottom-up.
Finally, apply constraints / unification to get possible types.
\paragraph{Self application}
This means that you apply a function to itself.
In Haskell, this is not typeable because there would need to be an infinite function type, but all \bi{Haskell types are finite}
\paragraph{Curry-Howard isomorphism}
We can also apply the implication introduction and implication elimination rules:
\[
\begin{prooftree}
\hypo{\Gamma, \sigma \vdash \tau}
\infer1[$\rightarrow$-I]{\Gamma \vdash \sigma \rightarrow \tau}
\end{prooftree}
\qquad
\begin{prooftree}
\hypo{\Gamma \vdash \sigma \rightarrow \tau}
\hypo{\Gamma \vdash \sigma}
\infer2[$\rightarrow$-E]{\Gamma \vdash \tau}
\end{prooftree}
\]
@@ -0,0 +1,15 @@
\subsection{Natural Number Proofs}
To prove $\forall n \in \N. P$, we of course again use induction:
\shade{blue}{Base Case} Show $P[n \mapsto 0]$
\shade{green}{Step Case} Let $m \in \N$ be arbitrary and not free in $P$. We then assume that $P[n \mapsto m]$ and show that $P[n \mapsto m + 1]$
Or the same as a natural deduction rule:
\[
\begin{prooftree}
\hypo{\Gamma \vdash P[n \mapsto 0]}
\hypo{\Gamma, P[n \mapsto m] \vdash P[n \mapsto m + 1]}
\infer2[$m$ not free in $\Gamma, P$]{\Gamma \vdash \forall n \in \N. P}
\end{prooftree}
\]
@@ -0,0 +1,23 @@
\subsubsection{Induction over the natural numbers}
In Haskell, we can also define all the natural numbers using
\mint{haskell}+data Nat = Zero | Succ Nat deriving (Eq, Ord, Show)+
Thus the natural numbers are (isomorphic to) the set
\[
\texttt{Nat} = \{ \texttt{Zero}, \texttt{Succ Zero}, \texttt{Succ (Succ Zero)}, \ldots \}
\]
The data type provides two crucial rules for constructing members of \texttt{Nat}:
\begin{itemize}
\item $\texttt{Zero} \in \texttt{Nat}$
\item If $x \in \texttt{Nat}$, then $\texttt{Succ} x \in \texttt{Nat}$
\end{itemize}
The induction stated as a natural deduction rule:
\[
\begin{prooftree}
\hypo{\Gamma \vdash P[n \mapsto \texttt{Zero}]}
\hypo{\Gamma, P[n \mapsto m] \vdash P[n \mapsto \texttt{Succ}\ m]}
\infer2[$m$ not free in $\Gamma, P$]{\Gamma \vdash \forall n \in \texttt{Nat}. P}
\end{prooftree}
\]
@@ -0,0 +1,12 @@
\subsubsection{Lists}
A possible data type for lists in Haskell is:
\mint{haskell}+data L t = Nil | Cons t (L t)+
A natural deduction rule for induction over lists is:
\[
\begin{prooftree}
\hypo{\Gamma \vdash P[xs \mapsto \texttt{Nil}]}
\hypo{\Gamma, P[xs \mapsto ys] \vdash P[xs \mapsto \texttt{Cons}\ y\ ys]}
\infer2[$y$, $ys$ not free in $\Gamma, P$]{\Gamma \vdash \forall xs \in \texttt{L}\ t. P}
\end{prooftree}
\]
@@ -0,0 +1,12 @@
\subsubsection{Trees}
A possible data type for trees in Haskell is:
\mint{haskell}+data Tree t = Leaf | Node t (Tree t) (Tree t)+
A natural deduction rule for induction over trees is:
\[
\begin{prooftree}
\hypo{\Gamma \vdash P[x \mapsto \texttt{Leaf}]}
\hypo{\Gamma, P[x \mapsto l] \vdash P[xs \mapsto \texttt{Node}\ a\ l\ r]}
\infer2[$a$, $l$, $r$ not free in $\Gamma, P$]{\Gamma \vdash \forall x \in \texttt{Tree}\ t. P}
\end{prooftree}
\]
@@ -0,0 +1,7 @@
\newpage
\subsection{Interpreters}
Interpreters are prevalent in programming languages, database systems, text processors, HDLs, search engines, etc.
They are in concept very simple, as they perform three steps read, evaluate, print.
The implementation of one however is not trivial by any stretch of the imagination.
@@ -0,0 +1,52 @@
\subsubsection{Read step}
During this step, text is turned from text into a more easily handlable format
\paragraph{Lexical Analysis}
During lexical analysis, the input is turned into tokens.
For example: The source code is \texttt{position := initial + rate + 60}
The translation is:
\begin{multicols}{2}
\begin{enumerate}
\item Identifier \texttt{position}
\item Assignment symbol \texttt{:=}
\item Identifier \texttt{initial}
\item Addition symbol \texttt{+}
\item Identifier \texttt{rate}
\item Addition symbol \texttt{+}
\item Number \texttt{60}
\end{enumerate}
\end{multicols}
It also removes whitespaces and comments
\paragraph{Parsing}
The tokens are then turned into an abstract syntax tree.
The syntax is specified by a grammar such as:
\begin{align*}
Expr & ::= Identifier \divider Number \divider Expr\; \texttt{`+`}\; Expr \\
Assign & ::= Identifier\; \texttt{`:=`}\; Expr
\end{align*}
This can also be represented as a haskell type:
\begin{code}{haskell}
data Expr = Identifier Ident | Number Num | Plus Expr Expr
data Assign = Assignment Ident Expr
type Ident = String
type Num = Int
\end{code}
Since some to be parsed statements are more complex to parse, we may do combinatory parsing.
This is much more powerful as it can handle ambiguous grammars typically found in real programming languages.
We can use for example these parser combinators:
\newpage
\inputcode{haskell}{code/10_parser.hs}
These are just some of the functions defined.
You can find a full mini-haskell and lambda-calculus parser on CodeExpert (at the time of writing this that was the case at least)
@@ -0,0 +1,50 @@
\newpage
\subsection{Evaluation}
Evaluation is then done using tree traversal as we have already seen in the Haskell section
\subsubsection{Lazy Evaluation}
Expressions are substituted before evaluation recursively until there are no more expressions to substitute, at which point the expression is evaluated.
This can obviously lead to duplicated evaluation, i.e. a computation reoccurring.
\paragraph{In Haskell}
In Haskell, this is solved using sharing where the terms are represented in a directed graph.
In pattern matching, the arguments are evaluated only as much as is needed to determine a pattern match.
For guards, the execution proceeds sequentially until success occurs.
For instance in an \texttt{OR} statement, only the first statement is evaluated if it evaluates to true
Local definitions are also lazily evaluated (i.e. bound with \texttt{where} clauses)
\paragraph{Applications}
This concept can be used for data-driven programming.
For instance, to determine the minimum value of a list, we could use insertion sort and take the head.
Due to lazy evaluation, we have way fewer evaluations that need to happen.
Additionally for infinite lists or other infinite data structures, lazy evaluation allows creating a finite representation for the infinite data.
It also allows operating on the infinite data given the operation only operates on a finite subset of the data structure.
An application of that is the prime number algorithm Sieve of Eratosthenes:
\begin{enumerate}
\item Generate list: \texttt{[2 ..]} (list of all natural numbers)
\item Mark the first unmarked number: \texttt{head :: [a] -> a} from prelude determines first element
\item Cross out all multiples: \verb|dropMults x ys = filter (\y -> y `mod` x /= 0) ys|
\item Repeat with recursions: \texttt{sieve xs = head xs : sieve (dropMults (head xs) (tail xs))}
\end{enumerate}
Another example is Newton's Algorithm to find roots.
\paragraph{Correctness}
Lazy evaluation makes reasoning about complexity and correctness harder, as types like \texttt{[Int]} include
\begin{enumerate}
\item finite, everywhere defined lists (e.g. \texttt{[1, 3, 5]})
\item finite lists with undefined elements like \texttt{[1, 2, undef]}
\item infinite lists with defined or undefined elements such as \texttt{[1..]}
\end{enumerate}
However, induction is only sound for the first kind. More on this later on