aboutsummaryrefslogtreecommitdiff
path: root/refined/doc.go
blob: 31fe99bdce606edf721ec3a32515d4246ebd0e0a (plain)
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
105
106
107
108
109
110
111
112
113
114
115
/*******************************************************************************
* Copyright 2025 Stefan Majewsky <majewsky@gmx.net>
* SPDX-License-Identifier: GPL-3.0-only
* Refer to the file "LICENSE" for details.
*******************************************************************************/

// Package refined implements refinement types. Those are types that are constrained by a condition.
// For example, consider the following type representing Go variable names:
//
//	var variableNameRx = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`)
//
//	type VariableName string
//
// If you want for this type to only ever contain string values that are valid variable names,
// you would need to ensure that the value is first checked for validity whenever a VariableName instance is created.
// Since programmers inevitably make mistakes, we should rather have the type checker do this work for us.
// We can do this by wrapping the string inside type VariableName into a refined.Value like so:
//
//	type VariableName struct {
//		refined.Value[VariableName, string]
//	}
//
//	func (VariableName) MatchesValue(value string) error { return refined.RegexpMatch(variableNameRx, value) }
//	func (VariableName) Build(v refined.Prevalue[VariableName, string]) VariableName { return VariableName{refined.Build(v)} }
//
// # Why do we need so much boilerplate to declare a refinement type?
//
// As far as I'm aware, this is genuinely the least possible amount of boilerplate.
// To see why, let's go back to the initial form:
//
//	type VariableName string
//
// A newtype like this is not type-safe since it can be constructed by anyone at any time, without checking the implied constraint:
//
//	var illegalVariableName = VariableName("what is this?") // value does not match variableNameRx!
//
// We need to fully seal the contained string value away and prevent outsiders from messing with it without going through a constraint check.
// The only way to do this is with a struct:
//
//	type VariableName struct {
//		...
//	}
//
// Next, we want for this library to provide premade implementations of interfaces like json.Unmarshaler or sql.Scanner to all refinement types.
// This ensures that values inserted into the VariableName type during unmarshaling are properly checked against the refinement type's constraint.
// The respective functions are declared on the refined.Value type and can be inherited by making refined.Value an embedded field:
//
//	type VariableName struct {
//		refined.Value
//	}
//
// The refined.Value type obviously needs a type argument to know which raw value type it's holding:
//
//	type VariableName struct {
//		refined.Value[string]
//	}
//
// But refined.Value also needs a second type argument: It needs to be able to reach the MatchesValue() function that contains the refinement type's constraint.
// This is technically just a single function, but Go does not support providing raw functions as type arguments;
// you need a type implementing the desired function through an interface.
// I decided to require that this interface be implemented on the value type itself, so you don't have to declare a second bogus type:
//
//	type VariableName struct {
//		refined.Value[VariableName, string]
//	}
//
//	func (VariableName) MatchesValue(value string) error { ... }
//
// This would technically be enough, but it would not be very ergonomic.
// Supposing that we have a refined.NewValue() function that constructs refined.Value instances, we could construct VariableName instances like so:
//
//	name := VariableName{Value: refined.NewValue[VariableName](rawName)}
//
// This is rather convoluted and repeats words twice within a single line.
// And that's before considering that refined.NewValue() really ought to be returning an error for when rawName is not a valid variable name:
//
//	nameValue, err := refined.NewValue[VariableName](rawName)
//	if err != nil {
//		...
//	}
//	name := VariableName{Value: nameValue}
//
// We could wrap this in helper functions near the declaration of the VariableName type:
//
//	func NewVariableName(rawName string) (VariableName, error) {
//		v, err := refined.NewValue[VariableName](rawName)
//		return VariableName{v}, err
//	}
//
//	func MustNewVariableName(rawName string) VariableName { // for literals
//		return VariableName{refined.MustNewValue[VariableName](rawName)}
//	}
//
// But I don't want to copy-paste this for every single refinement type. Instead, we add one more method to the interface that already contains MatchValue.
// Our full type declaration becomes:
//
//	type VariableName struct {
//		refined.Value[VariableName, string]
//	}
//
//	func (VariableName) MatchesValue(value string) error { ... }
//	func (VariableName) Build(v refined.Prevalue[VariableName, string]) VariableName { return VariableName{refined.Build(v)} }
//
// This allows us to use library functions with nice names to construct instances of refinement types:
//
//	name, err := refined.New[VariableName](rawName)
//	fooName := refined.Literal[VariableName]("foo")
//
// The library functions check the constraint, build a refined.Prevalue, and then use the Build() method on VariableName to wrap those into proper VariableName instances.
// It would be nice if we could use the actual refined.Value type in Build() instead of its weird sibling refined.Prevalue:
//
//	func (VariableName) Build(v refined.Value[VariableName, string]) VariableName { return VariableName{v} }
//
// But that results in a recursive type declaration in the library, which the Go compiler rejects.
package refined