You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
7.3 KiB

% -*- mode: latex; coding: utf-8; TeX-master: ../thesis -*-
% !TEX TS-program = pdflatexmk
% !TEX encoding = UTF-8 Unicode
% !TEX root = ../thesis.tex
To learn functional programming without being introduced to a new syntax at the same time
ensures that programmers can fully concentrate on functional concepts. Although Go already
supported a functional programming style, the programmer may not have known if the code was
purely functional or if there still were imperative constructs embedded.
In the last chapters, functional purity has been defined as a law based on one simple rule: immutability.
Immutability dictates that once assigned, a variable's value never changes.
This in turn leads to function purity, which means that functions do not have side
effects and their return value is solely influenced by the function's parameters.
It has been shown that although purely functional languages like Haskell aim to be completely
pure, this objective is difficult to accomplish. The reason for this are Input / Output actions; user
input, network connections, randomness and time are all impure. Haskell wraps
these impure functions in the IO monad, which is a way to work around the compiler's optimisations
based on functional purity. In addition, although the IO monad does not make impure functions pure, it
does serve as documentation to its users (`if the function has IO, it is impure') and
guarantees a certain execution order.
Go on the other hand does not have this issue, as the Go compiler does not optimise execution
based on purity guarantees. Having a similar construct to the IO monad in Go would, as such,
only serve documentation purposes. Because of this, the decision has been taken to ignore
the impurity that is implied with IO actions.
Apart from IO, to achieve functional purity, the global state of a program should not influence
the return values of specific function. This ties into immutablitiy; if global state can
not be mutated, it can also not influence or change the result value of a function.
Based on these observations, a static code analysis tool has been developed that reports
all reassignments of variables. In other words, it forbids the usage of the regular
assignment operator (\mintinline{go}|=|), only allowing the short variable declarations
(\mintinline{go}|:=|). However, the experienced Go developer may know that the \mintinline{go}|:=|
operator can also reassign previously declared variables, implying that the solution to the
problem is not as simple as forbidding the assignment operator.
Further, there are many more edge cases that have been detected with careful testing:
To recursively call function literals, they must be declared beforehand (before assigning
the actual function to it) because of Go's scoping rules. Additionally, exceptions
had to be made for the blank identifier (\mintinline{go}|_|) and variables that are declared
outside of the current file.
With all of this in place, an algorithm has been chosen that is based on the identifier's
declaration position. In the \gls{ast} that is being checked, every identifier node has a field
which contains the position of it's declaration. If this does not match the current identifier's
position, the operation must be a reassignment.
The resulting binary, called `funcheck', successfully reports such reassignments:
\begin{gocode}
s := "hello world"
fmt.Println(s)
s = "and goodbye"
fmt.Println(s)
\end{gocode}
\begin{bashcode}
$> funcheck .
file.go:3:2: reassignment of s
\end{bashcode}
This linter can be used and executed against any Go package. To eliminate the reported errors,
code has to be rewritten in a purely functional manner.
However, functional code often relies heavily on lists and list-processing functions.
Although Go does not have a built-in list datatype, Go's slices, an abstraction built
on top of arrays,
mitigate a lot of downsides when comparing regular arrays to lists\footnote{Arrays / Slices
and Lists have a different runtime behaviour (indexing, adding or removing elements).
However, the performance of the code was not considered to be in scope for this thesis.}.
What Go's slices lack on the other hand are the typical higher-order functions like `map',
`filter' or `reduce'. These are commonly used in functional programming and most languages
contain implementations of these functions already --- Go does not.
Due to the lack of polymorphism, writing implementations for these functions
would result in a lot of duplicated code. To mitigate this issue, the most
common higher-order functions have been added to the list of Go's built-in functions,
which are registered, type-checked and implemented within the Go compiler.
As these are handled directly at compile time, built-in functions may be polymorphic, for
example allowing the programmer to use the same `filter' function for all list-types.
To determine which higher-order functions are most commonly used, the most popular
open-source Haskell projects (pandoc, shellcheck and purescript, to name a few) have
been analysed. As a result, `fmap', `fold', `filter' and `prepend'
(`cons') have been added as built-ins into the compiler.
These functions make it easier to write purely functional code in Go, in turn helping
the programmer to learn functional programming with a familiar language and syntax.
While implementing these functions in a regular Go program would be a matter of minutes,
adding them to the Go compiler is not as simple. To illustrate, the functions
have been written out in regular Go in the chapters~\ref{ch:impl-fmap} to~\ref{ch:impl-filter}
and are 33 lines of code, all functions combined. In the Go compiler, it is necessary to
register the functions, type-check the calls and manipulate the \gls{ast} instead of writing
the algorithm in Go code directly. This took more than 800 lines of code to do so.
As a result, using these functions is equal to using any other built-in function: there
is documentation in Godoc, type-checking support in the language server\footnote{If the
language server (gopls) is compiled against the modified version of Go, as
described in Appendix~\ref{appendix:build-gopls}}
and in the compilation phase, as well as a polymorphic function header, allowing the
programmer to call the function with any allowed type.
Demonstrations of these functions and how functional Go code looks like can be seen in
Chapter~\ref{ch:application}.
With these additions to Go and its ecosystem, aspiring functional programmers
can fully concentrate of the concepts of functional programming while keeping
a familiar syntax at hand. However, it should not be considered a fully featured
functional programming language. Rather, it should serve as a starting point and
make the transition to a language like Haskell easier.
Differences in the syntax between Haskell and Go exemplify why purely functional programming
languages have a distinct syntax compared to imperative or object-oriented languages.
Many constructs can be expressed more concisely in Haskell, without the additional
overhead that many programming languages, including Go, introduce.
Using `the right tool for the job' is important, and this paper shows that imperative or
object-oriented programming languages are not the right tool for production-grade
functional programming. However, they can serve as a good starting point and help transitioning
to a pure functional programming language.