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
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.
|