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.

301 lines
9.8 KiB

% -*- mode: latex; coding: utf-8; TeX-master: ../thesis -*-
% !TEX TS-program = pdflatexmk
% !TEX encoding = UTF-8 Unicode
% !TEX root = ../thesis.tex
\section{Demonstration of the new built-in functions}
The new built-in functions can be used in the same manner as the existing built-ins like
\mintinline{go}|append|. This example serves as a demonstration by modifying a slice
of integers, although they can be used to modify a slice of any type.
\begin{code}
\begin{gocode}
package main
import (
"fmt"
"strconv"
)
// Counterpart to Haskell's `derive Show` through code generation
//go:generate stringer -type parity
type parity int
const even parity = 0
const odd parity = 1
// shouldBe returns a function that returns true if an int is of the
// given parity
func shouldBe(p parity) func(i int) bool {
return func(i int) bool {
return i%2 == int(p)
}
}
func main() {
lst := []int{1, 2, 3, 4, 5}
lstMult := fmap(func(i int) int { return i * 5 }, prepend(0, lst))
addToString := func(s string, i int) string {
return s + strconv.Itoa(i) + " "
}
// fold over even / odd numbers and add them to a string
evens := foldl(addToString, even.String()+": ",
filter(shouldBe(even), lstMult))
odds := foldl(addToString, odd.String()+": ",
filter(shouldBe(odd), lstMult))
fmt.Println(evens, odds) // even: 0 10 20 odd: 5 15 25
}
\end{gocode}
\caption{Demonstration of the new built-in functions\label{code:funcexample}}
\end{code}
\section{Refactoring the Prettyprint Package}
\newglossaryentry{stdout}{name=stdout, description={Standard Output, the default
output stream for programs}}
The code blocks~\ref{code:assign-pos} and~\ref{code:func-reassign} have been
generated by a small package `prettyprint' contained in the funcheck repository.
To see how the newly built-in functions and funcheck can be used, this `prettyprint' package
can be refactored to a purely functional implementation.
The current version of the package is written in what could be considered idiomatic
Go\footnote{
There is no exact definition of what idiomatic Go is, so this interpretation
could be challenged. It is idiomatic Go code to the author.
}.
The prettyprinter is based on the same framework as assigncheck\footnote{Assigncheck
is the main package for funcheck and checks the reassignments}, but instead
of reporting anything, it prints AST information to \gls{stdout}.
Similarly to assigncheck, the main logic of the package is within a
function literal that is being passed to the \mintinline{go}|ast.Inspect|
function.
Prettyprint only checks two AST node types, \mintinline{go}|*ast.DeclStmt|
and \mintinline{go}|*ast.AssignStmt| (declarations and assignments).
For example, for the program
\begin{gocode}
package main
import "fmt"
func main() {
x, y := 1, 2
y = 3
fmt.Println(x, y)
}
\end{gocode}
the following AST information is printed:
\begin{gocode}
Assignment "x, y := 1, 2": 2958101
Ident "x": 2958101
Decl "x, y := 1, 2": 2958101
Ident "y": 2958104
Decl "x, y := 1, 2": 2958101
Assignment "y = 3": 2958115
Ident "y": 2958115
Decl "x, y := 1, 2": 2958101
\end{gocode}
To refactor it to a purely functional version, funcheck can be used to
list reassignments:
\begin{bashcode}
$> funcheck .
prettyprint.go:20:2: internal reassignment (for loop) in "for _, file := range pass.Files { ... }"
prettyprint.go:42:2: internal reassignment (for loop) in "for i := range decl.Specs { ... }"
prettyprint.go:67:2: internal reassignment (for loop) in "for _, expr := range as.Lhs { ... }"
\end{bashcode}
As can be seen in the output, the package uses 3 \mintinline{go}|for| loops to range over
slices. However, there are no other reassignments of variables in the code.
The code to print declarations, which causes the second lint message, is as shown in code block~\ref{code:decl-printing}.
\begin{code}
\begin{gocode}
func checkDecl(as *ast.DeclStmt, fset *token.FileSet) {
fmt.Printf("Declaration %q: %v\n", render(fset, as), as.Pos())
decl, ok := as.Decl.(*ast.GenDecl)
if !ok {
return
}
for i := range decl.Specs {
val, ok := decl.Specs[i].(*ast.ValueSpec)
if !ok {
continue
}
if val.Values != nil {
continue
}
if _, ok := val.Type.(*ast.FuncType); !ok {
continue
}
fmt.Printf("\tIdent %q: %v\n", render(fset, val), val.Names[0].Pos())
}
}
\end{gocode}
\caption{Pretty-printing declarations in idiomatic Go\label{code:decl-printing}}
\end{code}
To convert this for-loop appropriately, the new built-in `foldl' can be used.
To recapitulate, the `foldl' function is being defined as:
\begin{gocode}
func foldl(fn func(Type1, Type) Type1, acc Type1, slice []Type) Type1
\end{gocode}
As `foldl' requires a return type, a dummy type `null" can be introduced, which
is just an empty struct:
\begin{gocode}
type null struct{}
\end{gocode}
Now the code within the for loop can be used to create a function literal:
\begin{gocode}
check := func(_ null, spec ast.Spec) (n null) {
// implementation
}
\end{gocode}
There are two subtleties in regards to the introduced null type:
First, the null value that is being passed as an argument is being discarded
by the use of an empty identifier.
Secondly, the return value is `named', which means the variable `n' is
already declared in the function block. Because of this, `naked returns' can
be used, so there is no need to specify which variable is being returned.
The code snippet~\ref{code:decl-printing} can be translated to:
\begin{code}
\begin{gocode}
func checkDecl(as *ast.DeclStmt, fset *token.FileSet) {
fmt.Printf("Declaration %q: %v\n", render(fset, as), as.Pos())
check := func(_ null, spec ast.Spec) (n null) {
val, ok := spec.(*ast.ValueSpec)
if !ok {
return
}
if val.Values != nil {
return
}
if _, ok := val.Type.(*ast.FuncType); !ok {
return
}
fmt.Printf("\tIdent %q: %v\n", render(fset, val), val.Names[0].Pos())
return
}
if decl, ok := as.Decl.(*ast.GenDecl); ok {
_ = foldl(check, null{}, decl.Specs)
}
}
\end{gocode}
\caption{Pretty-printing declarations in functional Go}
\end{code}
The for-loop has been replaced by a `foldl', where a function closure
that contains the actual processing is passed.
While this still looks similar to the original example, this is mostly due to
the `if' statements. In Haskell, pattern matching would be used and nil checks
could be omitted entirely. Also, as Haskell's type system is more advanced, the
handling of those types would be different too.
However, the goal of this thesis is to make functional code look more familiar
to programmers that are used to imperative code.
And while it may not look like it, the code does not use any mutation of
variables\footnote{Libraries may do, but the scope is not to rewrite any existing
libraries.}, for loops or global state. Therefore, it can be concluded that this
snippet is purely functional as per the definition from Chapter~\ref{sec:func-purity}.
\section{Quicksort}
In Chapter~\ref{code:haskell-quicksort}, a naive implementation of the Quicksort sorting
algorithm has been introduced.
Implementing this algorithm in Go is now straightforward and the similarities between
the Haskell implementation and the functional Go implementation are striking:
\begin{listing}
\begin{gocode}
func quicksort(p []int) []int {
if len(p) == 0 {
return []int{}
}
lesser := filter(func(x int) bool { return p[0] > x }, p[1:])
greater := filter(func(x int) bool { return p[0] <= x }, p[1:])
return append(quicksort(lesser), prepend(p[0], quicksort(greater))...)
}
\end{gocode}
\begin{haskellcode}
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
\end{haskellcode}
\caption{Quicksort implementations compared}
\end{listing}
Again, the Go implementation bridges the gap between being imperative and functional,
while still being obvious about the algorithm.
Furthermore, as expected, when inspecting the code with funcheck, no non-functional
constructs are reported.
\section{Comparison to Java Streams}
In Java 8, concepts from functional programming have been introduced to the language.
The major new feature was Lambda Expressions --- anonymous function literals --- and
streams. Streams are an abstract layer to process data in a functional way, with `map',
`filter', `reduce' and more.
Java Streams are similar to the new built-in functions in this thesis:
\begin{listing}
\begin{javacode}
List<Integer> even = list.stream()
.filter(x -> x % 2 == 0)
.collect(Collectors.toList());
\end{javacode}
\begin{gocode}
even := filter(
func(x int) bool { return x%2 == 0 },
list)
\end{gocode}
\caption{Comparison Java Streams and functional Go}
\end{listing}
The lambda-syntax in Java is more concise than Go's function literals, where the
complete function header has to be provided\footnote{There is an open proposal
to add a lightweight anonymous function syntax to Go 2, which, if implemented,
would resolve this verbosity\autocite{go-lambdas}}.
However, the conversion to a stream and back to a list (with \mintinline{java}|list.stream()| and
\mintinline{java}|.collect(Collectors.toList())|)
is not required in Go, reducing the mental overhead for the programmer.
Apart from syntactical differences, Java Streams contain all the functions that
have been added as built-ins to Go, and a lot more.
On the other hand, Java's Syntax is arguably more complex than Go. An indicator for this might be
the language specification; Go's Language Specification is roughly 110 pages, while
Java's specification spans more than 700 pages\footnote{
The Java 8 Specification is 724\autocite{java-8-spec}, the Java 14
Specification 774\autocite{java-14-spec} pages.}, more than 6 times the size.
The consideration of which language to choose comes down to the experience with either language.
An experienced Java programmer will find it easier to start with Java's toolset, while programmers
coming from a C background may choose Go.