Introduction

Introduction #

go-optics is a go library that provides a solution for updating immutable data structures in go.

Immutability in Go. #

There are various current approaches to immutability in Go.

  • use unexported fields with a read only interface
  • special read-only collections

These partially solve the problems with handling immutable data in Go but currently fall short. Take the following example data model.

type Comment struct {
	title   string
	content string
}

type BlogPost struct {
	content  string
	comments []Comment
}

This represents a simple blog post with comments and ratings.

Consider the code needed to update the content of a comment while retaining immutability.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Update the given comment in the blog with the given content and return a new immutable BlogPost
func UpdateCommentContent(source BlogPost, commentIndex int, newContent string) BlogPost {
	//We can't modify the existing comments so we have to clone them
	var updatedComments []Comment
	updatedComments = append(updatedComments, source.Comments...)

	//Comments are stored by value so we can update our copy
	updatedComments[commentIndex].Content = newContent

	//BlogPost was also passed by value so we can update its Comments directly
	source.Comments = updatedComments

	return source
}

Now consider we need to implement or generate the same boilerplate for the other fields of Comment and also perform the same exercise for ratings.

That’s a lot of boilerplate and the only way to reference a comment was by slice index. Optics provide a powerful solution to this problem of handling nested immutable data structures.

Using optics the above code would become

1
2
3
4
5
6
7
8
// Update the given comment in the blog with the given content and return a new immutable BlogPost
func OpticalUpdateCommentContent(blogPost BlogPost, commentIndex int, newContent string) BlogPost {
	return MustSet(
		O.BlogPost().Comments().Nth(commentIndex).Content(),
		newContent,
		blogPost,
	)
}

There is a lot to unpack here. The single statement in the function can be broken down into the following components.

  • MustSet –> This is an action that sets the focused element to the given value
  • O.BlogPost().Comment().Nth(commentIndex),Content() -> This is the optic. It describes a path from a source (BlogPost) to a focus (the Content string of a comment)
  • newContent -> this is the new value that the set action will apply to the optic.
  • blogPost –> this is the source that will be immutably updated

In this example the MustSet action returns a new BlogPost with only the focused comment content updated.

Optics can also be read from. e.g. to read the same content from the BlogPost the following code can be used.

1
2
3
4
	content, found := MustGetFirst(
		O.BlogPost().Comments().Nth(commentIndex).Content(),
		blogPost,
	)

This doesn’t seem all that useful as the user could have easily written blogPost.comments[commentIndex].content But consider again that the comments and content fields are unexported. The optic methods are exported and can be used from outside the defining package while still retaining immutability when updating.

The O.BlogPost() is a convenience function generated by the go-optics makelens tool.

Summary #

The go optics library provides functionality to focus on deeply nested elements within an immutable data structure. When performing an update to an immutable data structure the optics library takes care of all the boilerplate required to build up the immutable data structure surrounding the updated values.