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
301 lines
9.8 KiB
5 years ago
|
% -*- 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.
|