From 9fede8ef986e4bcf8a0b461075fcc13c0fc33c11 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Thu, 3 Jul 2025 23:38:24 +0200 Subject: refined: lay out a package doc as a mission statement --- README.md | 6 +++++- refined/doc.go | 31 +++++++++++++++++++++++++++++++ refined/scalar.go | 13 +++++++++++++ refined/struct.go | 13 +++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 refined/doc.go create mode 100644 refined/scalar.go create mode 100644 refined/struct.go diff --git a/README.md b/README.md index 75563fe..61ba84e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ SPDX-License-Identifier: Apache-2.0 # gg (Generic Generics) A Go library for the generic types that should be in the standard library, but aren't. -Currently, this includes [Option](./option/). +Currently, this includes: + +- an [Option](./option/) type similar to the one in Rust, and +- a [library](./refined/) for defining [refinement types](https://en.wikipedia.org/wiki/Refinement_type). + I may additional types like `Result`, `Either` or `Pair` if: - there is a compelling usecase for myself, and diff --git a/refined/doc.go b/refined/doc.go new file mode 100644 index 0000000..6560b96 --- /dev/null +++ b/refined/doc.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2025 Stefan Majewsky +// SPDX-License-Identifier: Apache-2.0 + +// Package refined provides refinement types for Go. +// +// A refinement type is built on top of a base type by adding a predicate that accepts or rejects values of the base type. +// An instance of the refinement type can only be constructed if the predicate accepts the proposed value. +// For example, one can use the base type "string" to derive the refinement type "GoIdentifier", using a predicate that only accepts string values that are valid identifiers in Go code. +// +// Refinement types based on this library make use of the type system to make invalid values unrepresentable. +// For example, if a piece of code obtains an instance of the aforementioned GoIdentifier type, the code shall be able to assume that the contained string value is a valid Go identifier, because it is not possible to construct a GoIdentifier instance holding a string value that is not. +// Creating and updating instances of refinement types is only possible through functions and methods in this package that check the proper predicate and throw an error or panic if the value in question is rejected by it. +// +// Because of how the Go language works, there are some significant restrictions on what refinement types can do. +// +// # Restriction #1: Base types must allow deep cloning +// +// When deriving a refinement type from a complex base type, any pointers hiding inside that base type would allow interior mutation that the refinement type cannot block. +// For example, if the refinement type's base type is a struct that contains a []byte somewhere, getting a shallow copy of the held value would allow us to overwrite elements of that slice without needing to uphold the predicate of the refinement type. +// +// To guard against this, we only allow base types that are scalars (single numbers or string values), through type Scalar, or struct types that can deep-clone themselves, through type Struct. +// Refinement types based on structs will refuse to give out shallow copies, which may impose a significant performance penalty for structs containing complex data structures. +// Other structured base types like slices or maps may be added in the future if we can devise an API that is both safe and sufficiently ergonomic. +// +// # Restriction #2: Unmarshaling +// +// Go does a lot of useful things through reflection, the most significant of them being marshalling to/from wire formats like JSON or SQL. +// Reflection sometimes constructs zero-valued instances of arbitrary types without giving the type a chance to intervene. +// To make sure that programs can never use instances of refinement types that did not check their predicate on construction, attempting to use a zero-valued instance of any refinement type results in a panic. +// When receiving data structures containing refinement types from a reflect-based generator like json.Unmarshal(), you need to use func ValidateUnmarshaled to check that all refinement type instances therein are intact. +package refined diff --git a/refined/scalar.go b/refined/scalar.go new file mode 100644 index 0000000..b7099d6 --- /dev/null +++ b/refined/scalar.go @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Stefan Majewsky +// SPDX-License-Identifier: Apache-2.0 + +package refined + +import . "github.com/majewsky/gg/option" + +type Scalar struct { + value Option[any] +} + +// TODO: func ValidateUnmarshaled() +// TODO: constrain base type of Scalar to be a scalar via an explicit type enum diff --git a/refined/struct.go b/refined/struct.go new file mode 100644 index 0000000..924c32e --- /dev/null +++ b/refined/struct.go @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Stefan Majewsky +// SPDX-License-Identifier: Apache-2.0 + +package refined + +import . "github.com/majewsky/gg/option" + +type Struct struct { + value Option[struct{}] +} + +// TODO: require base struct type to have a deep-clone method +// TODO: func Get() that does a deep clone, func Set(), func Update(func(*BaseType)) -- cgit v1.2.3