strongweak

Convert between strong and weak representations of types

https://github.com/raehik/strongweak#readme

Version on this page:0.3.2
LTS Haskell 20.26:0.3.2
Stackage Nightly 2023-04-28:0.4.1
Latest on Hackage:0.6.1

See all snapshots strongweak appears in

MIT licensed by Ben Orchard
Maintained by Ben Orchard
This version can be pinned in stack with:strongweak-0.3.2@sha256:70ac7d942bb8938b5b306c31256b4f63fb926d8a0c25d1c87cb1c0f58991e089,2405

strongweak

Purely convert between pairs of “weak” and “strong”/“validated” types, with good errors and generic derivers.

Definition of strong and weak types

Take a pair of types (strong, weak). We state the following:

  • You may safely convert (“weaken”) any strong value to a weak value.
  • You can try to convert (“strengthen”) any weak value to a strong value, but it may fail.

As a rule, a weak type should be easier to use than its related strong type. That is, it should have fewer invariants to consider or maintain. You could weaken an a to a Maybe a, but since a Maybe a is harder to use, it’s not a candidate for this library.

As an arbitrary limitation for ease of use, a strong type has 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, plus helps type inference.

Examples

The refined library defines a newtype Refined p a = Refined a. To get a Refined, you must test its associated predicate. You may recover the unrefined value by removing the newtype wrapper. Thus, you may strengthen as into Refined p as, and weaken vice versa.

The WordX family are like bounded Naturals. We can consider Natural as a weak type, which can be strengthened into e.g. Word8 by asserting well-boundedness.

Cool points

Validates as much as possible

This is primarily a validation library. Thus, we don’t fail on the first error – we attempt to validate every part of a data type, and collate the errors into a big list. (ApplicativeDo plus Validation is magical.)

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.

Generic strengthening is extremely powerful

There are generic derivers for generating Strengthen and Weaken instances between arbitrary data types. The Strengthen instances annotate errors extensively, telling you the datatype & record for which strengthening failed - recursively, for nested types!

Note that the generic derivers work with any pair of matching data types. But they must match very closely: both types are traversed in tandem, so every pair of fields must be compatible. If you need to do calculation to move between your strong and weak types, consider splitting it into calculation -> strengthening and using the generic derivers. Or write your own instances.

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.

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, possibly at the detriment of performance. However, my expectation is that you only strengthen & weaken at the “edges” of your program, and most of the time will be spent transforming weak representations. This may improve performance if it means invariants don’t have to be continually asserted inline, but it also may slow things down e.g. Naturals are slower than Words.

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.)

Changes

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 inside Weaken - Strengthen now has Weaken 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