strongweak
Convert between strong and weak representations of types
https://github.com/raehik/strongweak#readme
Version on this page: | 0.9.1 |
LTS Haskell 23.1: | 0.11.0 |
Stackage Nightly 2024-12-09: | 0.11.0 |
Latest on Hackage: | 0.11.0 |
strongweak-0.9.1@sha256:7329b026ce3572d877c75a525dbd50d792d1d9d46770edcb886a16b1f01364dc,2494
Module documentation for 0.9.1
strongweak
Purely convert between pairs of “weak” and “strong”/“validated” types, with extensive failure reporting and powerful generic derivers. Alexis King’s [Parse, don’t validate][parse-dont-validate] pattern as a library.
What? Why?
Haskell is a wonderful language for accurate data modelling. Algebraic data types (and GADTs as a fancy extension) enable defining highly restricted types which prevent even representing invalid or unwanted values. Great! And for the common case where you want to assert some predicate on a value but not change it (i.e. validate), we have the powerful refined library to reflect the existence of an asserted predicate in types. Fantastic!
Sadly I’m often grounded by “Reality”, who insists that we don’t use these features everywhere because manipulating more complex types often means more busywork on the term level. So I resort to less accurate data models, or validating somewhat arbitrarily without assistance from the type system. I can often feel Alexis King looking disapprovingly at me.
What if we defined two separate representations for a given model?
- A strong representation, where no invalid values are permitted. (Promise.)
- A weak representation, which doesn’t necessarily enforce all the invariants that the strong representation does, but is easier to manipulate.
This way, we can use strong representations wherever possible e.g. passing between subsystems, and shift to the weak representation for intensive manipulation (and then back to strong at the end). Potential wins for simplicity, brevity and performance, albeit for some conversion overhead.
Let’s formalize the above as a pair of types S
and W
.
- Given a
strong :: S
, we can always turn it into aweak :: W
. - Given a
weak :: W
, we can only turn it into astrong :: S
if it passes all the checks
We can write these as pure functions.
weaken :: S -> W
strengthen :: W -> Maybe S
Oh! So this is like a parser-printer pair for arbitrary data. It seems like a useful enough pattern. Let’s think of some strongweak pairs:
Refined p a
from the refined library is ana
where the predicatep
has been asserted. This can be weakened into ana
viaunrefine :: Refined p a -> a
.Word8
is a bounded natural number.Natural
can represent any natural number. SoNatural
is a weak type, which can be strengthened intoWord8
(orWord16
,Word32
, …) by asserting well-boundedness.[a]
doesn’t have state any predicates. But we could weaken everya
in the list. So[a]
is a strong type, which can be weakened to[Weak a]
.NonEmpty a
does have a predicate. For useability and other reasons, we only handle this predicate, and don’t also weaken eacha
like above.NonEmpty a
weakens to[a]
.
But there’s a hefty amount of boilerplate:
- You need to model all the data types you want to use like this twice.
- You need to write tons more definitions.
Aaaand it’s already not worth it. Sigh.
Library introduction
strongweak encodes the above strong/weak representation pattern for convenient use, automating as much as possible. Some decisions restrict usage for nicer behaviour. The primary definitions are below:
class Weaken a where
type Weak a :: Type
weaken :: a :: Weak a
type Result = Validation Fails
type Fails = NeAcc Fail
class Weaken a => Strengthen a where
strengthen :: Weak a -> Result a
Note that a strong type may have only one associated weak type. The same weak type may be used for multiple strong types. This restriction guides the design of “good” strong-weak type pairs, keeps them synchronized, and aids type inference.
See the documentation on Hackage for further details.
Cool points
Extreme error clarity
strongweak is primarily a validation library. As such, strengthening failure handling receives special attention:
- Failures do not short-circuit; if a strengthening is made up of multiple smaller strengthenings, all are run and any failures collated.
- Generic strengthening is scarily verbose: see below for details.
One definition, strong + weak views
Using a type-level Strength
switch and the SW
type family, you can write a
single datatype definition and receive both a strong and a weak representation,
which the generic derivers can work with. See the Strongweak.SW
module for
details.
Powerful generic instances
There are generic derivers for generating Strengthen
and Weaken
instances
between compatible data types. The Strengthen
instances annotate errors
extensively, telling you the datatype, constructor and field for which
strengthening failed!
Two types are compatible if
- their generic SOP representations match precisely, and
- every pair of leaf types is either identical or has the appropriate strengthen/weaken instance
The SW
type family is here to help for accomplishing that. Otherwise, if your
types don’t fit:
- convert to a “closer” representation first
- write your own instances (fairly simple with
ApplicativeDo
).
Backdoors included
Sometimes you have can guarantee that a weak value can be safely strengthened, but the compiler doesn’t know - a common problem in parsing. In such cases, you may use efficient unsafe strengthenings, which don’t perform invariant checks. Even better, they might explode your computer if you use them wrong!
What this library isn’t
Not a convertible
This is not a Convertible
library that enumerates transformations between
types into a dictionary. A strong type has exactly one weak representation, and
strengthening may fail while weakening cannot. For safe conversion enumeration
via typeclasses, consider Taylor Fausak’s
witch library.
Not particularly speedy
The emphasis is on safety, which may come at the detriment of performance:
- Strengthening and weakening might be slow. This depends on the type and the implementation. I try a little to ensure good performance, but not a lot.
- Strong types can be more performant than their weak counterparts. For
example, swapping all integrals for
Natural
s andInteger
s will make your program slow.- You may avoid this fairly easily by simply not wrapping certain fields.
On the other hand, by only strengthening at the “edges” of your program and knowing that between those you may transform the weak representation as you like, you may find good performance easier to maintain.
Related projects
barbies
The barbies library is an investigation into how far the higher-kinded data pattern can be stretched. strongweak has some similar ideas:
- Both treat a type definition as a “skeleton” for further types.
- strongweak’s
SW
type family looks a lot like barbies’Wear
.
But I believe we’re irreconcilable. strongweak is concerned with validation via
types. SW
is just a convenience to reuse a definition for two otherwise
distinct types, and assist in handling common patterns. Due to the type family
approach, we can rarely be polymorphic over the strong and weak representations.
Whereas barbies wants to help you swap out functors over records, so it’s very
polymorphic over those, and makes rules for itself that then apply to its users.
You could stack barbies on top of a SW
type no problem. It would enable you to
split strengthening into two phases: strengthening each field, then gathering
via traverse (rather than doing both at once via applicative do). That thinking
helps reassure me that these ideas are separate. (Note: I would hesitate to
write such a type, because the definition would start to get mighty complex.)
Other
Can this be formalized or generalized in some useful way?
I note that this library is basically a couple of type classes and utilities for automating writing parsers and printers for types which are “close”. I can’t find anything in the literature that discusses this sort of thing. If you would have some info there, please do let me know!
Changes
0.9.1 (2024-10-01)
- update rerefined dependency
0.9.0 (2024-10-01)
- fix
Strengthen [a]
instance reversing list - support GHC 9.4 - GHC 9.8
0.8.0 (2024-06-11)
- no longer use
Validation
: failures now must wrap explicitly instead of being implicitly collated - fix some bounds (
text
lower bound was too low) - various tweaks
0.7.1 (2024-05-27)
- bump meta/dependencies
0.7.0 (2024-05-11)
- replaced refined1 with rerefined
- simplify failures
0.6.1 (2024-04-03)
- tests: relax hspec upper bound
0.6.0 (2023-05-10)
- add instances for
Refined1 p f a
; use refined1 library instead of refined (pending upstream)
0.5.0 (2023-05-05)
- allow text-2.0
- refactor strengthening code, rename some definitions
- use NeAcc instead of NonEmpty for strengthening failures
0.4.1 (2023-02-22)
- add
DerivingVia
wrapper for generic instances (likeGenerically
)
0.4.0 (2023-02-22)
- redesign some instances to avoid the decomposer style
- alter
Identity
,Const
instances - remove
Maybe
instance
- alter
- expand sized vector instance
0.3.2 (2022-11-28)
- support GHC 9.4
0.3.1 (2022-07-04)
- update refined (polykind predicate)
0.3.0 (2022-06-08)
- switch to associated type family for
Weak
insideWeaken
-Strengthen
now hasWeaken
as a superclass- I’m fairly confident that things make more sense this way - we get to remove an open type family, improve type inference, and prevent users from writing potentially dangerous instances. For that, a bit of asymmetry is welcome.
- better document generic derivers
- clarify instance design, provide more decomposer instances
- various refactoring
0.2.0 (2022-05-31)
Initial Hackage release (dependency issues prevented uploading).
- fix field indexing in generic errors
- add unsafe strengthening
- add property and error tests
0.1.0 (2022-05-16)
Initial release.
- basic instances (lists, numerics)
- generic derivations
- super explicit errors