Reconstrain

Reconstrain #

When composing optics their constraints are merged in a CompositionTree.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	var optic Optic[
		Void,
		float64,
		float64,
		float64,
		float64,
		CompositionTree[ReturnOne, ReturnOne],
		CompositionTree[ReadWrite, ReadWrite],
		CompositionTree[BiDir, BiDir],
		CompositionTree[Pure, Pure],
	]
	optic = Compose(
		Mul(1.8),
		Add(32.0),
	)

For normal usage this isn’t normally a problem. However If I were to return this optic from a function then I would have to use the full type including the composition tress.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func celsiusToFahrenheit1() Optic[
	Void,
	float64,
	float64,
	float64,
	float64,
	CompositionTree[ReturnOne, ReturnOne],
	CompositionTree[ReadWrite, ReadWrite],
	CompositionTree[BiDir, BiDir],
	CompositionTree[Pure, Pure],
] {
	return Compose(
		Mul(1.8),
		Add(32.0),
	)
}

Collapsing Reconstraints #

The return type constraint is CompositionTree[ReturnOne, ReturnOne] we could collapse this back down to ReturnOne without affecting the type of the optic. The Ret1 reconstrain does exactly this. It takes an optic with an arbitrarily complex ReturnOne composition path and returns an Optic with ReturnOne as it’s return type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func celsiusToFahrenheit2() Optic[
	Void,
	float64,
	float64,
	float64,
	float64,
	ReturnOne,
	CompositionTree[ReadWrite, ReadWrite],
	CompositionTree[BiDir, BiDir],
	CompositionTree[Pure, Pure],
] {
	return Ret1(
		Compose(
			Mul(1.8),
			Add(32.0),
		),
	)
}

go-optics provides an equivalent reconstrain for each constraint type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func celsiusToFahrenheit3() Optic[
	Void,
	float64,
	float64,
	float64,
	float64,
	ReturnOne,
	ReadWrite,
	BiDir,
	Pure,
] {
	return Ret1(Rw(Bd(EPure(
		Compose(
			Mul(1.8),
			Add(32.0),
		),
	))))
}
  • Rw reconstrains to ReadWrite
  • Bd reconstrains to BiDir
  • EPure reconstrains to Pure

There are also equivalent reconstrains for the “opposing” constraints.

  • RetM reconstarins to ReturnMany
  • Ro reconstrains to ReadOnly
  • Ud reconstrains to UniDir
  • EErr reconstrains to Err

This simple reconstrains suffice for a composed optic returned from a function. However when building custom combinators that receive an optic as a parameter. Additional reconstraints are needed.

Left / Right Reconstraints #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func bytesOf1[I, S any, RET, RW, DIR, ERR any](
	o Optic[I, S, S, string, string, RET, RW, DIR, ERR],
) Optic[
	I,
	S,
	S,
	[]byte,
	[]byte,
	CompositionTree[RET, ReturnOne],
	CompositionTree[RW, ReadWrite],
	CompositionTree[DIR, BiDir],
	CompositionTree[ERR, Pure],
] {
	return ComposeLeft(
		o,
		IsoCast[string, []byte](),
	)
}

The left side of the composition tree is determined by the optic passed to the combinator.

Here we can use the L (left) and R (right) family of reconstraints.

  • RetL reconstrains to the left return type as long as the right side is ReturnOne.
  • RetR reconstrains to the right return type as long as the left side is ReturnOne.
  • RwL reconstrains to the left read write type as long as the right side is ReadWrite.
  • RwR reconstrains to the right read write type as long as the left side is ReadWrite.
  • DirL reconstrains to the left direction type as long as the right side is BiDir.
  • DirR reconstrains to the right direction type as long as the left side is BiDir.
  • EErrL reconstrains to the left error type as long as the right side is Pure.
  • EErrR reconstrains to the right error type as long as the left side is Pure.

In this case we can use the left hand variants.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func bytesOf2[I, S any, RET, RW, DIR, ERR any](
	o Optic[I, S, S, string, string, RET, RW, DIR, ERR],
) Optic[
	I,
	S,
	S,
	[]byte,
	[]byte,
	RET,
	RW,
	DIR,
	ERR,
] {
	return RetL(RwL(DirL(EErrL(
		ComposeLeft(
			o,
			IsoCast[string, []byte](),
		),
	))))
}

In the case that either the left or right side is not ReturnOne,ReadWrite,BiDir or Pure then the simpler RetM,Ro,Ud,EErr can be used to reconstrain.

Merge Reconstraints #

When writing a combinator the left and right constraints may both be the same undefined type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func makeTuple1[I, S any, RET, RW, DIR, ERR any](
	o Optic[I, S, S, string, string, RET, RW, DIR, ERR],
) Optic[
	lo.Tuple2[I, I],
	lo.Tuple2[S, S],
	lo.Tuple2[S, S],
	lo.Tuple2[string, string],
	lo.Tuple2[string, string],
	CompositionTree[
		CompositionTree[ReturnOne, RET],
		CompositionTree[ReturnOne, RET],
	],
	CompositionTree[
		CompositionTree[ReadWrite, RW],
		CompositionTree[ReadWrite, RW],
	],
	UniDir,
	CompositionTree[
		CompositionTree[Pure, ERR],
		CompositionTree[Pure, ERR],
	],
] {
	return T2Of(
		Compose(
			T2A[S, S](),
			o,
		),
		Compose(
			T2B[S, S](),
			o,
		),
	)
}

Here we can see that the constraints on the left and right are identical. We can use the merge family of reconstraints to simplify them.

  • RetMerge
  • RwMerge
  • DirMerge
  • EErrMerge

We can additionally use the right family to reconstrains away the left side of the nested composition tree.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func makeTuple2[I, S any, RET, RW, DIR, ERR any](
	o Optic[I, S, S, string, string, RET, RW, DIR, ERR],
) Optic[
	lo.Tuple2[I, I],
	lo.Tuple2[S, S],
	lo.Tuple2[S, S],
	lo.Tuple2[string, string],
	lo.Tuple2[string, string],
	RET,
	RW,
	UniDir,
	ERR,
] {
	return RetMerge(RwMerge(EErrMerge(
		T2Of(
			RetR(RwR(EErrR(
				Compose(
					T2A[S, S](),
					o,
				),
			))),
			RetR(RwR(EErrR(
				Compose(
					T2B[S, S](),
					o,
				),
			))),
		),
	)))
}

Advanced Reconstraints #

The above reconstraints suffice for most cases. It’s typically a good idea to reconstrain as close to composition as possible to avoid the construction of large complex composition trees. This is however sometimes not possible. For these cases go-optics offers 2 solutions.

  • Tree Reconstraints
  • UnsafeReconstrain

Tree Reconstraints #

Tree reconstraints enable nodes in the composition tree to be switched with one another. The composition tree can then be manipulated until one of the other reconstraints can be used to simplify the node.

Here is an overview of the swap & trans reconstraints.

Swap & Trans overview

  • Swap/SwapL/SwapR swaps the 2 labeled composition tree nodes
  • Merge/MergeL/MergeR merges the 2 labeled composition tree nodes
  • Trans/TransL/TransR transposes the 2 labeled composition tree nodes across a tree boundary.

SwapL and SwapR can be used to move a node into position that one of the Tran operations can transpose it to somewhere else in the tree.

TransL and TransR can be used to move nodes up and down in the tree hierarchy.

MergeL and MergeR are convenience functions to avoid having to TransL/TransR nodes up 1 level in the tree just to merge them.

For complex composition trees these functions can become a little unwieldy. They do however provide compile time safety for the constraint types.

Consider this extension of the lo.Tuple2 example expanded to a lo.Tuple9

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
func makeTuple9[I, S any, RET, RW, DIR, ERR any](
	o Optic[I, S, S, string, string, RET, RW, DIR, ERR],
) Optic[
	lo.Tuple9[I, I, I, I, I, I, I, I, I],
	lo.Tuple9[S, S, S, S, S, S, S, S, S],
	lo.Tuple9[S, S, S, S, S, S, S, S, S],
	lo.Tuple9[string, string, string, string, string, string, string, string, string],
	lo.Tuple9[string, string, string, string, string, string, string, string, string],
	CompositionTree[
		CompositionTree[
			CompositionTree[
				CompositionTree[RET, RET],
				RET,
			],
			CompositionTree[RET, RET]],
		CompositionTree[
			CompositionTree[RET, RET],
			CompositionTree[RET, RET],
		],
	],
	CompositionTree[
		CompositionTree[
			CompositionTree[
				CompositionTree[RW, RW],
				RW],
			CompositionTree[RW, RW],
		],
		CompositionTree[
			CompositionTree[RW, RW],
			CompositionTree[RW, RW],
		],
	],
	UniDir,
	CompositionTree[
		CompositionTree[
			CompositionTree[
				CompositionTree[ERR, ERR],
				ERR,
			],
			CompositionTree[ERR, ERR],
		],
		CompositionTree[
			CompositionTree[ERR, ERR],
			CompositionTree[ERR, ERR],
		],
	],
] {
	return T9Of(
		RetR(RwR(EErrR(
			Compose(
				T9A[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9B[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9C[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9D[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9E[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9F[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9G[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9H[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
		RetR(RwR(EErrR(
			Compose(
				T9I[S, S, S, S, S, S, S, S, S](),
				o,
			),
		))),
	)
}

The composition trees are deeper than 2 levels so we will have to use TransL and TransR to lift the nodes into reach of the other reconstrains.

This sequence of calls is able to fully merge the compositions tree.

1
2
3
	retMerge := RetMerge(RetMerge(RetTransR(RetMergeL(RetTransL(RetSwapL(RetMergeR(RetTransL(RetMergeR(RetMergeR(makeTuple))))))))))
	rwMerge := RwMerge(RwMerge(RwTransR(RwMergeL(RwTransL(RwSwapL(RwMergeR(RwTransL(RwMergeR(RwMergeR(retMerge))))))))))
	errMerge := EErrMerge(EErrMerge(EErrTransR(EErrMergeL(EErrTransL(EErrSwapL(EErrMergeR(EErrTransL(EErrMergeR(EErrMergeR(rwMerge))))))))))

UnsafeReconstrain #

UnsafeReconstrain will apply the supplied constraints verbatim onto the optic bypassing the typical compile time checks. This can lead to an optic with undefined behavior, including raising panic, when used. With due care and attention the code is much easier to implement and to read.

1
	return UnsafeReconstrain[RET, RW, UniDir, ERR](makeTuple)

UnsafeReconstrain can be made safe by implementing a custom wrapper with the concrete optic type that you are reconstraining.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
func reconstrainMakeTuple9[I, S, RET, RW, ERR any](
	o Optic[
		lo.Tuple9[I, I, I, I, I, I, I, I, I],
		lo.Tuple9[S, S, S, S, S, S, S, S, S],
		lo.Tuple9[S, S, S, S, S, S, S, S, S],
		lo.Tuple9[string, string, string, string, string, string, string, string, string],
		lo.Tuple9[string, string, string, string, string, string, string, string, string],
		CompositionTree[
			CompositionTree[
				CompositionTree[
					CompositionTree[RET, RET],
					RET,
				],
				CompositionTree[RET, RET]],
			CompositionTree[
				CompositionTree[RET, RET],
				CompositionTree[RET, RET],
			],
		],
		CompositionTree[
			CompositionTree[
				CompositionTree[
					CompositionTree[RW, RW],
					RW],
				CompositionTree[RW, RW],
			],
			CompositionTree[
				CompositionTree[RW, RW],
				CompositionTree[RW, RW],
			],
		],
		UniDir,
		CompositionTree[
			CompositionTree[
				CompositionTree[
					CompositionTree[ERR, ERR],
					ERR,
				],
				CompositionTree[ERR, ERR],
			],
			CompositionTree[
				CompositionTree[ERR, ERR],
				CompositionTree[ERR, ERR],
			],
		],
	]) Optic[
	lo.Tuple9[I, I, I, I, I, I, I, I, I],
	lo.Tuple9[S, S, S, S, S, S, S, S, S],
	lo.Tuple9[S, S, S, S, S, S, S, S, S],
	lo.Tuple9[string, string, string, string, string, string, string, string, string],
	lo.Tuple9[string, string, string, string, string, string, string, string, string],
	RET,
	RW,
	UniDir,
	ERR,
] {
	return UnsafeReconstrain[RET, RW, UniDir, ERR](o)
}

If anything changes in the optics constraints the compile will fail and you can reanalyze the constraints to verify they are still valid. This show the provided reconstraints are implemented.