diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e32b8b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,242 @@ +# The matrix feature is overused because it lets us do globals and interpolation +# in more places (where we're for some reason forbidden from using workflow +# environment variables). +# +# TODO +# +# * Uploading executables by running `cabal install`, because that saves them +# to a known place. Not ideal, and we guess that "known" place. +# * `cabal build` and `cabal install` have bugs and inconsistencies, stripping +# may not work, our flags might get thrown away between `cabal` calls. + +name: CI + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +# If env.exe exists, jobs will build and upload the specified executable with +# optimizations (-O2). If it doesn't exist, jobs will build without +# optimizations (-O0). +#env: +# exe: bytepatch + +jobs: + + ubuntu-cabal-test: + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / test / GHC ${{ matrix.ghc }}, Cabal + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + cabal: [latest] + ghc: + - 9.2.2 + include: + - ghc: 9.2.2 + build: release + + steps: + + # TODO: GHC decides to recompile based on timestamp, so cache isn't used + # Preferably GHC would work via hashes instead. Stack had this feature + # merged in Aug 2020. + # Upstream GHC issue: https://gitlab.haskell.org/ghc/ghc/-/issues/16495 + # My issue on haskell/actions: https://github.com/haskell/actions/issues/41 + # This also requires us to do a deep fetch, else we don't get the Git commit + # history we need to rewrite mod times. + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set all tracked file modification times to the time of their last commit + run: | + rev=HEAD + IFS=$'\n' + for f in $(git ls-tree -r -t --full-name --name-only "$rev") ; do + touch -d $(git log --pretty=format:%cI -1 "$rev" -- "$f") "$f"; + done + + - name: Setup Haskell build environment + id: setup-haskell-build-env + uses: haskell/actions/setup@v1 + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: ${{ matrix.cabal }} + + - run: cabal freeze + + - name: Cache Cabal build artifacts + uses: actions/cache@v2 + with: + path: | + ${{ steps.setup-haskell-build-env.outputs.cabal-store }} + dist-newstyle + key: test-cabal-build-artifacts-${{ runner.os }}-ghc_${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} + restore-keys: test-cabal-build-artifacts-${{ runner.os }}-ghc_${{ matrix.ghc }} + + - name: Build (exe) + if: "env.exe != 0 && matrix.build == 'release'" + run: cabal build -O2 + - name: Build (skip exe) + if: "env.exe != 0 && matrix.build != 'release'" + run: cabal build -O0 + - name: Build (no exe) + if: "env.exe == 0" + run: cabal build -O0 + + - name: Test + run: cabal test --test-show-details=streaming + env: + HSPEC_OPTIONS: --color + + - name: Install + if: "env.exe != 0 && matrix.build == 'release'" + run: cabal install + + # note that Cabal uses symlinks -- actions/upload-artifact@v2 apparently + # dereferences for us + - name: Upload executable + if: "env.exe != 0 && matrix.build == 'release'" + uses: actions/upload-artifact@v2 + with: + path: ~/.cabal/bin/${{ env.exe }} + name: ${{ env.exe }}-${{ runner.os }}-ghc_${{ matrix.ghc }}-cabal-${{ github.sha }} + if-no-files-found: error + + mac-cabal-test: + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / test / GHC ${{ matrix.ghc }}, Cabal + strategy: + fail-fast: false + matrix: + os: [macos-latest] + cabal: [latest] + ghc: + - 9.2.2 + include: + - ghc: 9.2.2 + build: release + + steps: + + # TODO figure out timestamp fixer on Mac (no Mac available to test) + - uses: actions/checkout@v2 + + - name: Setup Haskell build environment + id: setup-haskell-build-env + uses: haskell/actions/setup@v1 + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: ${{ matrix.cabal }} + + - run: cabal freeze + + - name: Cache Cabal build artifacts + uses: actions/cache@v2 + with: + path: | + ${{ steps.setup-haskell-build-env.outputs.cabal-store }} + dist-newstyle + key: test-cabal-build-artifacts-${{ runner.os }}-ghc_${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} + restore-keys: test-cabal-build-artifacts-${{ runner.os }}-ghc_${{ matrix.ghc }} + + - name: Build (exe) + if: "env.exe != 0 && matrix.build == 'release'" + run: cabal build -O2 + - name: Build (skip exe) + if: "env.exe != 0 && matrix.build != 'release'" + run: cabal build -O0 + - name: Build (no exe) + if: "env.exe == 0" + run: cabal build -O0 + + - name: Test + run: cabal test --test-show-details=streaming + env: + HSPEC_OPTIONS: --color + + - name: Install + if: "env.exe != 0 && matrix.build == 'release'" + run: cabal install + + # note that Cabal uses symlinks -- actions/upload-artifact@v2 apparently + # dereferences for us + - name: Upload executable + if: "env.exe != 0 && matrix.build == 'release'" + uses: actions/upload-artifact@v2 + with: + path: ~/.cabal/bin/${{ env.exe }} + name: ${{ env.exe }}-${{ runner.os }}-ghc_${{ matrix.ghc }}-cabal-${{ github.sha }} + if-no-files-found: error + + windows-cabal-test: + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / test / GHC ${{ matrix.ghc }}, Cabal + strategy: + fail-fast: false + matrix: + os: [windows-latest] + cabal: [latest] + ghc: + - 9.2.2 + include: + - ghc: 9.2.2 + build: release + + steps: + + # TODO can't do cache fixer on Windows b/c it's a Bash script... + - uses: actions/checkout@v2 + + - name: Setup Haskell build environment + id: setup-haskell-build-env + uses: haskell/actions/setup@v1 + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: ${{ matrix.cabal }} + + - run: cabal freeze + + - name: Cache Cabal build artifacts + uses: actions/cache@v2 + with: + path: | + ${{ steps.setup-haskell-build-env.outputs.cabal-store }} + dist-newstyle + key: test-cabal-build-artifacts-${{ runner.os }}-ghc_${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} + restore-keys: test-cabal-build-artifacts-${{ runner.os }}-ghc_${{ matrix.ghc }} + + - name: Build (exe) + if: "env.exe != 0 && matrix.build == 'release'" + run: cabal build -O2 + - name: Build (skip exe) + if: "env.exe != 0 && matrix.build != 'release'" + run: cabal build -O0 + - name: Build (no exe) + if: "env.exe == 0" + run: cabal build -O0 + + - name: Test + run: cabal test --test-show-details=streaming + env: + HSPEC_OPTIONS: --color + + - name: Install + if: "env.exe != 0 && matrix.build == 'release'" + run: cabal install + + # note that Cabal uses symlinks -- actions/upload-artifact@v2 apparently + # dereferences for us + - name: Upload executable + if: "env.exe != 0 && matrix.build == 'release'" + uses: actions/upload-artifact@v2 + with: + path: C:/cabal/bin/${{ env.exe }}.exe + name: ${{ env.exe }}-${{ runner.os }}-ghc_${{ matrix.ghc }}-cabal-${{ github.sha }} + if-no-files-found: error diff --git a/.github/workflows/hackage.yml b/.github/workflows/hackage.yml new file mode 100644 index 0000000..0e888ba --- /dev/null +++ b/.github/workflows/hackage.yml @@ -0,0 +1,100 @@ +# GitHub Actions worflow to build Hackage artifacts for a project: an sdist +# archive, and Haddock docs for uploading to Hackage. +# +# I would love to do this in the same testing workflows, so we're not wasting +# GitHub's resources, but workflow syntax is debilitating and they strip docs in +# their provided GHCs, so there's too much complexity to handle it in one place. +# +# This workflow is based on the expectation that GitHub's runners install GHC +# using ghcup with default settings (installs GHCs to `~/.ghcup/ghc/$VERSION`). + +name: Hackage artifacts + +on: + push: + branches: + - main + +env: + # ghcup needs full version string (e.g. 9.0.1, not 9.0) + ghc: "9.2.2" + package_name: strongweak + +jobs: + hackage: + runs-on: ubuntu-latest + name: Hackage artifacts + + steps: + + # TODO: GHC decides to recompile based on timestamp, so cache isn't used + # Preferably GHC would work via hashes instead. Stack had this feature + # merged in Aug 2020. + # Upstream GHC issue: https://gitlab.haskell.org/ghc/ghc/-/issues/16495 + # My issue on haskell/actions: https://github.com/haskell/actions/issues/41 + # This also requires us to do a deep fetch, else we don't get the Git commit + # history we need to rewrite mod times. + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set all tracked file modification times to the time of their last commit + run: | + rev=HEAD + for f in $(git ls-tree -r -t --full-name --name-only "$rev") ; do + touch -d $(git log --pretty=format:%cI -1 "$rev" -- "$f") "$f"; + done + + - name: Delete preinstalled docs-stripped GHC ${{ env.ghc }} + run: rm -rf $HOME/.ghcup/ghc/${{ env.ghc }} + + - name: Cache GHC ${{ env.ghc }} + uses: actions/cache@v2 + with: + path: ~/.ghcup/ghc/${{ env.ghc }} + key: hackage-ghc-${{ runner.os }}-ghc_${{ env.ghc }} + + - name: Install GHC ${{ env.ghc }} if not present from cache + run: | + if [ ! -d $HOME/.ghcup/ghc/${{ env.ghc }} ]; then + ghcup install ghc --force ${{ env.ghc }} + fi + + - run: ghcup set ghc ${{ env.ghc }} + + - run: cabal update + + - run: cabal freeze + + - name: Cache Cabal build artifacts + uses: actions/cache@v2 + with: + path: | + ~/.cabal/store + dist-newstyle + key: hackage-deps-${{ runner.os }}-ghc_${{ env.ghc }}-${{ hashFiles('cabal.project.freeze') }} + restore-keys: hackage-deps-${{ runner.os }}-ghc_${{ env.ghc }} + + # TODO 2022-04-22: --haddock-options=--quickjump fixes a bug with not + # propagating --haddock-quickjump to building dependency Haddocks + - run: cabal build --enable-documentation --haddock-for-hackage --haddock-options=--quickjump + + - run: cabal sdist + + - name: Upload Hackage sdist + uses: actions/upload-artifact@v2 + with: + path: dist-newstyle/sdist/${{ env.package_name }}-*.tar.gz + name: ${{ env.package_name }}-sdist-${{ github.sha }}.tar.gz + if-no-files-found: error + + - name: Upload Hackage Haddock docs + uses: actions/upload-artifact@v2 + with: + path: dist-newstyle/${{ env.package_name }}-*-docs.tar.gz + name: ${{ env.package_name }}-hackage-haddocks-${{ github.sha }}.tar.gz + if-no-files-found: error + + - name: Delete prepared tarballs (else can't extract just newest next time) + run: | + rm dist-newstyle/${{ env.package_name }}-*-docs.tar.gz + rm dist-newstyle/sdist/${{ env.package_name }}-*.tar.gz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee894e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Stack general +/.stack-work/ + +# Cabal general +/dist-newstyle/ + +/tmp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5aa26d2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +## 0.1.0 (Unreleased) +Initial release. + + * basic instances (lists, numerics) + * generic derivations + * super explicit errors diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d51d0e --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2022 Ben Orchard (@raehik) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..37249ac --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# strongweak +TODO diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/package.yaml b/package.yaml new file mode 100644 index 0000000..64edfc7 --- /dev/null +++ b/package.yaml @@ -0,0 +1,76 @@ +name: strongweak +version: 0.1.0 +synopsis: Convert between strong and weak representations of types +description: Please see README.md. +extra-source-files: +- README.md +- CHANGELOG.md +category: Data +tested-with: GHC ==9.2.2 +license: MIT +license-file: LICENSE + +github: raehik/strongweak +maintainer: Ben Orchard +author: Ben Orchard + +# TODO 2022-04-22 This will be supported eventually - I looked just now, and +# there was a fix 10 hours ago! But it'll take a while to trickle down into an +# hpack release, and then that release in Stack. +language: GHC2021 + +# mostly Alexis King's 2018 recommended defaults +# (most can be replaced with GHC 9.2's GHC2021 language extension +default-extensions: +# essential +- EmptyCase +- FlexibleContexts +- FlexibleInstances +- InstanceSigs +- MultiParamTypeClasses +- PolyKinds +- LambdaCase + +# deriving-related +- DerivingStrategies +- StandaloneDeriving +- DeriveAnyClass +- DeriveGeneric +- DeriveDataTypeable +- DeriveFunctor +- DeriveFoldable +- DeriveTraversable +- DeriveLift + +# essential syntax but too recent +- ImportQualifiedPost # 8.10 +- StandaloneKindSignatures # 8.10 +- DerivingVia # 8.6 + +# less essential but still gimmes +- RoleAnnotations +- TypeApplications +- DataKinds +- TypeFamilies +- TypeOperators +- BangPatterns +- GADTs +- DefaultSignatures +- RankNTypes + +# extra +- UndecidableInstances # honestly fine but... +- MagicHash # pretty much syntactic, but too weird +- ScopedTypeVariables # probs dangerous to have as default + +dependencies: +- base >= 4.14 && < 5 +- vector-sized ^>= 1.5.0 +- refined ^>= 0.6.3 +- prettyprinter # TODO +- validation + +library: + source-dirs: src + ghc-options: + - -Wall diff --git a/src/Strongweak.hs b/src/Strongweak.hs new file mode 100644 index 0000000..6a40519 --- /dev/null +++ b/src/Strongweak.hs @@ -0,0 +1,9 @@ +module Strongweak + ( module Strongweak.Weaken + , module Strongweak.Strengthen + , module Strongweak.SW + ) where + +import Strongweak.Weaken +import Strongweak.Strengthen +import Strongweak.SW diff --git a/src/Strongweak/Example.hs b/src/Strongweak/Example.hs new file mode 100644 index 0000000..33da20c --- /dev/null +++ b/src/Strongweak/Example.hs @@ -0,0 +1,43 @@ +module Strongweak.Example where + +import Strongweak +import Strongweak.Generic + +import GHC.Generics ( Generic ) + +import Data.Word ( Word8 ) + +import Refined hiding ( Weaken(..) ) +import Numeric.Natural + +data Ex1D (s :: Strength) = Ex1C + { ex1f1 :: SW s Word8 + , ex1f2 :: SW s (Refined (LessThan 100) Natural) + } deriving stock Generic +deriving stock instance Show (Ex1D 'Strong) +deriving stock instance Show (Ex1D 'Weak) +instance Weaken (Ex1D 'Strong) (Ex1D 'Weak) where weaken = weakenGeneric +instance Strengthen (Ex1D 'Weak) (Ex1D 'Strong) where strengthen = strengthenGeneric + +data Ex2D (s :: Strength) = Ex2C + { ex2f1 :: Ex1D s + , ex2f2 :: SW s Word8 + } deriving stock Generic +deriving stock instance Show (Ex2D 'Strong) +deriving stock instance Show (Ex2D 'Weak) +instance Weaken (Ex2D 'Strong) (Ex2D 'Weak) where weaken = weakenGeneric +instance Strengthen (Ex2D 'Weak) (Ex2D 'Strong) where strengthen = strengthenGeneric + +ex1w :: Ex1D 'Weak +ex1w = Ex1C 256 210 + +ex2w :: Ex2D 'Weak +ex2w = Ex2C ex1w 256 + +data ExVoid (s :: Strength) deriving stock Generic +instance Weaken (ExVoid 'Strong) (ExVoid 'Weak) where weaken = weakenGeneric +instance Strengthen (ExVoid 'Weak) (ExVoid 'Strong) where strengthen = strengthenGeneric + +data ExUnit (s :: Strength) = ExUnit deriving stock Generic +instance Weaken (ExUnit 'Strong) (ExUnit 'Weak) where weaken = weakenGeneric +instance Strengthen (ExUnit 'Weak) (ExUnit 'Strong) where strengthen = strengthenGeneric diff --git a/src/Strongweak/Generic.hs b/src/Strongweak/Generic.hs new file mode 100644 index 0000000..afd9112 --- /dev/null +++ b/src/Strongweak/Generic.hs @@ -0,0 +1,7 @@ +module Strongweak.Generic + ( weakenGeneric + , strengthenGeneric + ) where + +import Strongweak.Generic.Weaken +import Strongweak.Generic.Strengthen diff --git a/src/Strongweak/Generic/Strengthen.hs b/src/Strongweak/Generic/Strengthen.hs new file mode 100644 index 0000000..baa4948 --- /dev/null +++ b/src/Strongweak/Generic/Strengthen.hs @@ -0,0 +1,86 @@ +{- | +The generic derivation is split into 3 classes, dealing with different layers of +a Haskell data type: datatype, constructor and selector. At each point, we +gather up information about the type and push on. Strengthening occurs at +selectors. If a strengthening fails, the gathered information is pushed into an +error that wraps the original error. +-} + +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE ApplicativeDo #-} + +module Strongweak.Generic.Strengthen where + +import Strongweak.Strengthen +import Data.Validation +import Data.List.NonEmpty + +import GHC.Generics + +strengthenGeneric + :: (Generic w, Generic s, GStrengthenD (Rep w) (Rep s)) + => w -> Validation (NonEmpty StrengthenError) s +strengthenGeneric = fmap to . gstrengthenD . from + +class GStrengthenD w s where + gstrengthenD :: w p -> Validation (NonEmpty StrengthenError) (s p) + +instance (GStrengthenC w s, Datatype dw, Datatype ds) => GStrengthenD (D1 dw w) (D1 ds s) where + gstrengthenD = fmap M1 . gstrengthenC (datatypeName' @dw) (datatypeName' @ds) . unM1 + +class GStrengthenC w s where + gstrengthenC :: String -> String -> w p -> Validation (NonEmpty StrengthenError) (s p) + +-- | Nothing to do for empty datatypes. +instance GStrengthenC V1 V1 where + gstrengthenC _ _ = Success + +instance (GStrengthenS w s, Constructor cw, Constructor cs) => GStrengthenC (C1 cw w) (C1 cs s) where + gstrengthenC dw ds = fmap M1 . gstrengthenS dw ds (conName' @cw) (conName' @cs) . unM1 + +-- | Strengthen sum types by strengthening left or right. +instance (GStrengthenC lw ls, GStrengthenC rw rs) => GStrengthenC (lw :+: rw) (ls :+: rs) where + gstrengthenC dw ds = \case L1 l -> L1 <$> gstrengthenC dw ds l + R1 r -> R1 <$> gstrengthenC dw ds r + +class GStrengthenS w s where + gstrengthenS :: String -> String -> String -> String -> w p -> Validation (NonEmpty StrengthenError) (s p) + +-- | Nothing to do for empty constructors. +instance GStrengthenS U1 U1 where + gstrengthenS _ _ _ _ = Success + +-- | Special case: if source and target types are equal, copy the value through. +instance GStrengthenS (S1 mw (Rec0 w)) (S1 ms (Rec0 w)) where + gstrengthenS _ _ _ _ = Success . M1 . unM1 + +-- | Strengthen a field using the existing 'Strengthen' instance. +instance {-# OVERLAPS #-} (Strengthen w s, Selector mw, Selector ms) => GStrengthenS (S1 mw (Rec0 w)) (S1 ms (Rec0 s)) where + gstrengthenS dw ds cw cs (M1 (K1 w)) = + case strengthen w of + Failure (e :| es) -> Failure $ StrengthenErrorField dw ds cw cs (selName' @mw) (selName' @ms) e :| es + Success s -> Success $ M1 $ K1 s + +-- | Strengthen product types by strengthening left, then right. +instance (GStrengthenS lw ls, GStrengthenS rw rs) => GStrengthenS (lw :*: rw) (ls :*: rs) where + gstrengthenS dw ds cw cs (l :*: r) = do + l' <- gstrengthenS dw ds cw cs l + r' <- gstrengthenS dw ds cw cs r + return $ l' :*: r' + +-------------------------------------------------------------------------------- + +-- | 'conName' without the value (only used as a proxy). Lets us push our +-- 'undefined's into one place. +conName' :: forall c. Constructor c => String +conName' = conName @c undefined + +-- | 'datatypeName' without the value (only used as a proxy). Lets us push our +-- 'undefined's into one place. +datatypeName' :: forall d. Datatype d => String +datatypeName' = datatypeName @d undefined + +-- | 'datatypeName' without the value (only used as a proxy). Lets us push our +-- 'undefined's into one place. +selName' :: forall s. Selector s => String +selName' = selName @s undefined diff --git a/src/Strongweak/Generic/Weaken.hs b/src/Strongweak/Generic/Weaken.hs new file mode 100644 index 0000000..81ab974 --- /dev/null +++ b/src/Strongweak/Generic/Weaken.hs @@ -0,0 +1,40 @@ +module Strongweak.Generic.Weaken where + +import Strongweak.Weaken + +import GHC.Generics + +weakenGeneric :: (Generic s, Generic w, GWeaken (Rep s) (Rep w)) => s -> w +weakenGeneric = to . gweaken . from + +class GWeaken s w where + gweaken :: s p -> w p + +-- | Strip all meta. +instance GWeaken s w => GWeaken (M1 is ms s) (M1 iw mw w) where + gweaken = M1 . gweaken . unM1 + +-- | Nothing to do for empty datatypes. +instance GWeaken V1 V1 where + gweaken = id + +-- | Nothing to do for empty constructors. +instance GWeaken U1 U1 where + gweaken = id + +-- | Special case: if source and target types are equal, copy the value through. +instance GWeaken (Rec0 s) (Rec0 s) where + gweaken = id + +-- | Weaken a field using the existing 'Weaken' instance. +instance {-# OVERLAPS #-} Weaken s w => GWeaken (Rec0 s) (Rec0 w) where + gweaken = K1 . weaken . unK1 + +-- | Weaken product types by weakening left and right. +instance (GWeaken ls lw, GWeaken rs rw) => GWeaken (ls :*: rs) (lw :*: rw) where + gweaken (l :*: r) = gweaken l :*: gweaken r + +-- | Weaken sum types by weakening left or right. +instance (GWeaken ls lw, GWeaken rs rw) => GWeaken (ls :+: rs) (lw :+: rw) where + gweaken = \case L1 l -> L1 $ gweaken l + R1 r -> R1 $ gweaken r diff --git a/src/Strongweak/SW.hs b/src/Strongweak/SW.hs new file mode 100644 index 0000000..3ed8690 --- /dev/null +++ b/src/Strongweak/SW.hs @@ -0,0 +1,45 @@ +module Strongweak.SW where + +import Refined ( Refined ) +import Data.Vector.Sized ( Vector ) +import Data.Kind ( Type ) +import Data.Word +import Data.Int +import Numeric.Natural ( Natural ) + +data Strength = Strong | Weak + +-- | Obtain the weak representation of the given type. +type family Weak (a :: Type) :: Type + +-- machine integers +type instance Weak Word8 = Natural +type instance Weak Word16 = Natural +type instance Weak Word32 = Natural +type instance Weak Word64 = Natural +type instance Weak Int8 = Integer +type instance Weak Int16 = Integer +type instance Weak Int32 = Integer +type instance Weak Int64 = Integer + +-- other +type instance Weak (Vector n a) = [a] +type instance Weak (Refined p a) = a + +{- | +Obtain either the strong or weak representation of a type, depending on the +type-level strength "switch" provided. + +This is intended to be used in data types that take a 'Strength' type. Define +your type using strong fields wrapped in @Switch s@. You then get the weak +representation for free, using the same definition. + +@ +data A (s :: Strength) = A + { aField1 :: Switch s Word8 + , aField2 :: String } +@ +-} +type family SW (s :: Strength) a :: Type where + SW 'Strong a = a + SW 'Weak a = Weak a diff --git a/src/Strongweak/Strengthen.hs b/src/Strongweak/Strengthen.hs new file mode 100644 index 0000000..dc512cc --- /dev/null +++ b/src/Strongweak/Strengthen.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE OverloadedStrings #-} + +module Strongweak.Strengthen where + +import GHC.TypeNats ( Natural, KnownNat ) +import Data.Word +import Data.Int +import Refined ( Refined, refine, Predicate ) +import Data.Vector.Sized qualified as Vector +import Data.Vector.Sized ( Vector ) +import Type.Reflection ( Typeable, typeRep ) + +import Prettyprinter +import Prettyprinter.Render.String + +import Data.Validation +import Data.List.NonEmpty ( NonEmpty( (:|) ) ) +import Data.Foldable qualified as Foldable + +{- | Any 'w' can be "strengthened" into an 's' by asserting some properties. + +For example, you may strengthen some 'Natural' @n@ into a 'Word8' by asserting +@0 <= n <= 255@. + +Note that we restrict strengthened types to having only one corresponding weak +representation using functional dependencies. +-} +class Strengthen w s | s -> w where strengthen :: w -> Validation (NonEmpty StrengthenError) s + +data StrengthenError + = StrengthenErrorBase String String String String + -- ^ weak type, strong type, weak value, msg + | StrengthenErrorField String String String String String String StrengthenError + -- ^ weak datatype name, strong datatype name, + -- weak constructor name, strong constructor name, + -- weak field name, strong field name, + -- error + +instance Show StrengthenError where + showsPrec _ = renderShowS . layoutPretty defaultLayoutOptions . pretty + +instance Pretty StrengthenError where + pretty = \case + StrengthenErrorBase wt st wv msg -> + vsep [ pretty wt<+>"->"<+>pretty st + , pretty wv<+>"->"<+>"FAIL" + , pretty msg ] + StrengthenErrorField dw _ds cw _cs sw _ss err -> + nest 1 $ pretty dw<>"."<>pretty cw<>"."<>pretty sw<>line<>pretty err + +strengthenErrorBase + :: forall s w. (Typeable w, Show w, Typeable s) + => w -> String -> Validation (NonEmpty StrengthenError) s +strengthenErrorBase w msg = Failure (e :| []) + where e = StrengthenErrorBase (show $ typeRep @w) (show $ typeRep @s) (show w) msg + +strengthenErrorPretty :: NonEmpty StrengthenError -> Doc a +strengthenErrorPretty = vsep . map go . Foldable.toList + where go e = "-"<+>indent 0 (pretty e) + +-- | Strengthen each element of a list. +instance Strengthen w s => Strengthen [w] [s] where + strengthen = traverse strengthen + +-- | Obtain a sized vector by asserting the size of a plain list. +instance (KnownNat n, Typeable a, Show a) => Strengthen [a] (Vector n a) where + strengthen w = + case Vector.fromList w of + Nothing -> strengthenErrorBase w "TODO bad size vector" + Just s -> Success s + +-- | Obtain a refined type by applying its associated refinement. +instance (Predicate p a, Typeable a, Show a) => Strengthen a (Refined p a) where + strengthen a = + case refine a of + Left err -> strengthenErrorBase a (show err) + Right ra -> Success ra + +-- Strengthen 'Natural's into Haskell's bounded unsigned numeric types. +instance Strengthen Natural Word8 where strengthen = strengthenBounded +instance Strengthen Natural Word16 where strengthen = strengthenBounded +instance Strengthen Natural Word32 where strengthen = strengthenBounded +instance Strengthen Natural Word64 where strengthen = strengthenBounded + +-- Strengthen 'Integer's into Haskell's bounded signed numeric types. +instance Strengthen Integer Int8 where strengthen = strengthenBounded +instance Strengthen Integer Int16 where strengthen = strengthenBounded +instance Strengthen Integer Int32 where strengthen = strengthenBounded +instance Strengthen Integer Int64 where strengthen = strengthenBounded + +strengthenBounded + :: forall b n + . (Integral b, Bounded b, Show b, Typeable b, Integral n, Show n, Typeable n) + => n -> Validation (NonEmpty StrengthenError) b +strengthenBounded n = + if n <= maxB && n >= minB then Success (fromIntegral n) + else strengthenErrorBase n $ "not well bounded, require: " + <>show minB<>" <= n <= "<>show maxB + where + maxB = fromIntegral @b @n maxBound + minB = fromIntegral @b @n minBound diff --git a/src/Strongweak/Weaken.hs b/src/Strongweak/Weaken.hs new file mode 100644 index 0000000..5e9336c --- /dev/null +++ b/src/Strongweak/Weaken.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE FunctionalDependencies #-} + +module Strongweak.Weaken where + +import Refined ( Refined, unrefine ) +import Numeric.Natural ( Natural ) +import Data.Word +import Data.Int +import Data.Vector.Sized qualified as Vector +import Data.Vector.Sized ( Vector ) + +{- | Any 's' can be "weakened" into a 'w'. + +For example, you may weaken a 'Word8' into a 'Natural'. + +Note that we restrict strengthened types to having only one corresponding weak +representation using functional dependencies. +-} +class Weaken s w | s -> w where weaken :: s -> w + +-- | Weaken each element of a list. +instance Weaken s w => Weaken [s] [w] where weaken = map weaken + +-- | Weaken sized vectors into plain lists. +instance Weaken (Vector n a) [a] where weaken = Vector.toList + +-- | Strip the refinement from refined types. +instance Weaken (Refined p a) a where weaken = unrefine + +-- Weaken the bounded Haskell numeric types using 'fromIntegral'. +instance Weaken Word8 Natural where weaken = fromIntegral +instance Weaken Word16 Natural where weaken = fromIntegral +instance Weaken Word32 Natural where weaken = fromIntegral +instance Weaken Word64 Natural where weaken = fromIntegral +instance Weaken Int8 Integer where weaken = fromIntegral +instance Weaken Int16 Integer where weaken = fromIntegral +instance Weaken Int32 Integer where weaken = fromIntegral +instance Weaken Int64 Integer where weaken = fromIntegral diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..4641078 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,2 @@ +resolver: nightly-2022-03-30 +packages: [.] diff --git a/stack.yaml.lock b/stack.yaml.lock new file mode 100644 index 0000000..0ed608e --- /dev/null +++ b/stack.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: [] +snapshots: +- completed: + size: 539378 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2022/3/30.yaml + sha256: 745431a4c5b78cc93d81e99b2253a1e0eacd4f94e00cf17dab7cc14e665332e3 + original: nightly-2022-03-30 diff --git a/strongweak.cabal b/strongweak.cabal new file mode 100644 index 0000000..1904c9c --- /dev/null +++ b/strongweak.cabal @@ -0,0 +1,82 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.34.4. +-- +-- see: https://github.com/sol/hpack + +name: strongweak +version: 0.1.0 +synopsis: Convert between strong and weak representations of types +description: Please see README.md. +category: Data +homepage: https://github.com/raehik/strongweak#readme +bug-reports: https://github.com/raehik/strongweak/issues +author: Ben Orchard +maintainer: Ben Orchard +license: MIT +license-file: LICENSE +build-type: Simple +tested-with: + GHC ==9.2.2 +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/raehik/strongweak + +library + exposed-modules: + Strongweak + Strongweak.Example + Strongweak.Generic + Strongweak.Generic.Strengthen + Strongweak.Generic.Weaken + Strongweak.Strengthen + Strongweak.SW + Strongweak.Weaken + other-modules: + Paths_strongweak + hs-source-dirs: + src + default-extensions: + EmptyCase + FlexibleContexts + FlexibleInstances + InstanceSigs + MultiParamTypeClasses + PolyKinds + LambdaCase + DerivingStrategies + StandaloneDeriving + DeriveAnyClass + DeriveGeneric + DeriveDataTypeable + DeriveFunctor + DeriveFoldable + DeriveTraversable + DeriveLift + ImportQualifiedPost + StandaloneKindSignatures + DerivingVia + RoleAnnotations + TypeApplications + DataKinds + TypeFamilies + TypeOperators + BangPatterns + GADTs + DefaultSignatures + RankNTypes + UndecidableInstances + MagicHash + ScopedTypeVariables + ghc-options: -Wall + build-depends: + base >=4.14 && <5 + , prettyprinter + , refined >=0.6.3 && <0.7 + , validation + , vector-sized >=1.5.0 && <1.6 + default-language: Haskell2010