├── stack.yaml ├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── test ├── Doctest.hs ├── Spec.hs └── Test │ ├── Prolens.hs │ ├── Data.hs │ └── Prolens │ ├── Inspection.hs │ └── Property.hs ├── .gitignore ├── CHANGELOG.md ├── .stylish-haskell.yaml ├── prolens.cabal ├── README.md ├── LICENSE └── src └── Prolens.hs /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-17.5 -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @chshersh @vrom911 2 | -------------------------------------------------------------------------------- /test/Doctest.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import Test.DocTest (doctest) 4 | 5 | 6 | main :: IO () 7 | main = doctest 8 | $ "-XLambdaCase" 9 | : "-XInstanceSigs" 10 | : "-XScopedTypeVariables" 11 | : "-XTupleSections" 12 | : "-XTypeApplications" 13 | : 14 | [ "src/Prolens.hs" 15 | ] 16 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import Test.Hspec (hspec) 4 | 5 | import Test.Prolens (unitSpecs) 6 | import Test.Prolens.Inspection (inspectionSpecs) 7 | import Test.Prolens.Property (lensPropertySpecs, typeclassesPropertySpecs) 8 | 9 | 10 | main :: IO () 11 | main = hspec $ do 12 | unitSpecs 13 | inspectionSpecs 14 | lensPropertySpecs 15 | typeclassesPropertySpecs 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Haskell 2 | dist 3 | dist-* 4 | cabal-dev 5 | *.o 6 | *.hi 7 | *.chi 8 | *.chs.h 9 | *.dyn_o 10 | *.dyn_hi 11 | *.prof 12 | *.aux 13 | *.hp 14 | *.eventlog 15 | .virtualenv 16 | .hsenv 17 | .hpc 18 | .cabal-sandbox/ 19 | cabal.sandbox.config 20 | cabal.config 21 | cabal.project.local 22 | .ghc.environment.* 23 | .HTF/ 24 | .hie/ 25 | # Stack 26 | .stack-work/ 27 | stack.yaml.lock 28 | 29 | ### IDE/support 30 | # Vim 31 | [._]*.s[a-v][a-z] 32 | [._]*.sw[a-p] 33 | [._]s[a-v][a-z] 34 | [._]sw[a-p] 35 | *~ 36 | tags 37 | 38 | # IntellijIDEA 39 | .idea/ 40 | .ideaHaskellLib/ 41 | *.iml 42 | 43 | # Atom 44 | .haskell-ghc-mod.json 45 | 46 | # VS 47 | .vscode/ 48 | 49 | # Emacs 50 | *# 51 | .dir-locals.el 52 | TAGS 53 | 54 | # other 55 | .DS_Store 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | `prolens` uses [PVP Versioning][1]. 4 | The changelog is available [on GitHub][2]. 5 | 6 | ## 👩‍👧 0.0.0.1 — Mar 14, 2021 7 | 8 | * [#51](https://github.com/kowainik/prolens/issues/51): 9 | Support GHC 9.0. Upgrade minor versions support to 8.10.4. 10 | * [#28](https://github.com/kowainik/prolens/issues/28): 11 | Inspection tests for prisms, ensuring optimal performance. 12 | * [#46](https://github.com/kowainik/prolens/issues/46): 13 | Document typeclasses laws. 14 | * [#7](https://github.com/kowainik/prolens/issues/7): 15 | Add typeclasses laws tests for the following types: 16 | 17 | + `Profunctor` laws for `Forget` 18 | + `Monoidal` laws for `(->)` 19 | 20 | Thanks @CristhianMotoche, @xplosunn for helping with this release! 21 | 22 | ## 0.0.0.0 23 | 24 | * Initially created. 25 | 26 | [1]: https://pvp.haskell.org 27 | [2]: https://github.com/kowainik/prolens/releases 28 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - simple_align: 3 | cases: true 4 | top_level_patterns: true 5 | records: true 6 | 7 | # Import cleanup 8 | - imports: 9 | align: none 10 | list_align: after_alias 11 | pad_module_names: false 12 | long_list_align: inline 13 | empty_list_align: inherit 14 | list_padding: 4 15 | separate_lists: true 16 | space_surround: false 17 | 18 | - language_pragmas: 19 | style: vertical 20 | remove_redundant: true 21 | 22 | # Remove trailing whitespace 23 | - trailing_whitespace: {} 24 | 25 | columns: 100 26 | 27 | newline: native 28 | 29 | language_extensions: 30 | - BangPatterns 31 | - ConstraintKinds 32 | - DataKinds 33 | - DefaultSignatures 34 | - DeriveAnyClass 35 | - DeriveDataTypeable 36 | - DeriveGeneric 37 | - DerivingStrategies 38 | - DerivingVia 39 | - ExplicitNamespaces 40 | - FlexibleContexts 41 | - FlexibleInstances 42 | - FunctionalDependencies 43 | - GADTs 44 | - GeneralizedNewtypeDeriving 45 | - InstanceSigs 46 | - KindSignatures 47 | - LambdaCase 48 | - MultiParamTypeClasses 49 | - MultiWayIf 50 | - NamedFieldPuns 51 | - NoImplicitPrelude 52 | - OverloadedStrings 53 | - QuasiQuotes 54 | - RecordWildCards 55 | - ScopedTypeVariables 56 | - StandaloneDeriving 57 | - TemplateHaskell 58 | - TupleSections 59 | - TypeApplications 60 | - TypeFamilies 61 | - ViewPatterns 62 | -------------------------------------------------------------------------------- /test/Test/Prolens.hs: -------------------------------------------------------------------------------- 1 | module Test.Prolens 2 | ( unitSpecs 3 | ) where 4 | 5 | import Data.Function ((&)) 6 | import Test.Hspec (Spec, describe, it, shouldBe) 7 | 8 | import Prolens (preview, set, view, (.~), (^.)) 9 | import Test.Data (Grade (..), gradeMark, gradeOther, me, nameL, _Mark) 10 | 11 | 12 | unitSpecs :: Spec 13 | unitSpecs = describe "Prolens unit tests" $ do 14 | lensSpecs 15 | prismSpecs 16 | 17 | 18 | lensSpecs :: Spec 19 | lensSpecs = describe "Lenses" $ do 20 | describe "getter" $ do 21 | it "should get name" $ 22 | view nameL me `shouldBe` "Veronika" 23 | it "should get name with ^." $ 24 | (me ^. nameL) `shouldBe` "Veronika" 25 | describe "setter" $ do 26 | it "should set name" $ 27 | view nameL (set nameL "Dmitrii" me) `shouldBe` "Dmitrii" 28 | it "should set name with .~" $ 29 | (me & nameL .~ "Dmitrii") ^. nameL `shouldBe` "Dmitrii" 30 | 31 | 32 | prismSpecs :: Spec 33 | prismSpecs = describe "Prisms" $ do 34 | describe "preview" $ do 35 | it "should get mark" $ 36 | preview _Mark gradeMark `shouldBe` Just 5 37 | it "should not get mark" $ 38 | preview _Mark gradeOther `shouldBe` Nothing 39 | describe "set" $ do 40 | it "should get mark" $ 41 | set _Mark 4 gradeMark `shouldBe` Mark 4 42 | it "should not get mark" $ 43 | set _Mark 4 gradeOther `shouldBe` gradeOther 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened] 6 | push: 7 | branches: [main] 8 | schedule: 9 | # additionally run once per week (At 00:00 on Sunday) to maintain cache 10 | - cron: '0 0 * * 0' 11 | 12 | jobs: 13 | cabal: 14 | name: ${{ matrix.os }} / ghc ${{ matrix.ghc }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macOS-latest, windows-latest] 19 | cabal: ["3.4"] 20 | ghc: 21 | - "8.6.5" 22 | - "8.8.4" 23 | - "8.10.4" 24 | - "9.0.1" 25 | exclude: 26 | - os: macOS-latest 27 | ghc: 9.0.1 28 | - os: macOS-latest 29 | ghc: 8.8.4 30 | - os: macOS-latest 31 | ghc: 8.6.5 32 | 33 | - os: windows-latest 34 | ghc: 9.0.1 35 | - os: windows-latest 36 | ghc: 8.8.4 37 | - os: windows-latest 38 | ghc: 8.6.5 39 | 40 | steps: 41 | - uses: actions/checkout@v2 42 | 43 | - uses: haskell/actions/setup@v1 44 | id: setup-haskell-cabal 45 | name: Setup Haskell 46 | with: 47 | ghc-version: ${{ matrix.ghc }} 48 | cabal-version: ${{ matrix.cabal }} 49 | 50 | - name: Configure 51 | run: | 52 | cabal configure --enable-tests --test-show-details=direct 53 | 54 | - name: Freeze 55 | run: | 56 | cabal freeze 57 | 58 | - uses: actions/cache@v2.1.3 59 | name: Cache ~/.cabal/store 60 | with: 61 | path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} 62 | key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 63 | 64 | - name: Install dependencies 65 | run: | 66 | cabal build all --only-dependencies 67 | 68 | - name: Build 69 | run: | 70 | cabal build all 71 | 72 | - name: Test 73 | run: | 74 | cabal test all 75 | 76 | stack: 77 | name: stack / ghc ${{ matrix.ghc }} 78 | runs-on: ubuntu-latest 79 | strategy: 80 | matrix: 81 | stack: ["2.5"] 82 | ghc: ["8.10.4"] 83 | 84 | steps: 85 | - uses: actions/checkout@v2 86 | 87 | 88 | - uses: haskell/actions/setup@v1 89 | name: Setup Haskell Stack 90 | with: 91 | ghc-version: ${{ matrix.ghc }} 92 | stack-version: ${{ matrix.stack }} 93 | 94 | - uses: actions/cache@v2.1.3 95 | name: Cache ~/.stack 96 | with: 97 | path: ~/.stack 98 | key: ${{ runner.os }}-${{ matrix.ghc }}-stack 99 | 100 | - name: Install dependencies 101 | run: | 102 | stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies 103 | 104 | - name: Build 105 | run: | 106 | stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks 107 | 108 | - name: Test 109 | run: | 110 | stack test --system-ghc 111 | -------------------------------------------------------------------------------- /test/Test/Data.hs: -------------------------------------------------------------------------------- 1 | module Test.Data 2 | ( -- * Lenses 3 | Haskeller (..) 4 | , nameL 5 | , knowledgeL 6 | 7 | , Knowledge (..) 8 | , syntaxL 9 | 10 | , me 11 | 12 | -- * Prisms 13 | , Grade (..) 14 | , _Mark 15 | 16 | , gradeMark 17 | , gradeOther 18 | 19 | -- * Generators 20 | , genFun 21 | , genFunction 22 | , genForget 23 | , genHaskeller 24 | , genInt 25 | , genKnowledge 26 | , genName 27 | ) where 28 | 29 | import Test.Hspec.Hedgehog (MonadGen) 30 | 31 | import Prolens (Forget (..), Fun (..), Lens', Prism', lens, prism') 32 | 33 | import qualified Hedgehog.Gen as Gen 34 | import qualified Hedgehog.Range as Range 35 | 36 | 37 | data Haskeller = Haskeller 38 | { haskellerName :: String 39 | , haskellerExperience :: Int 40 | , haskellerKnowledge :: Knowledge 41 | } deriving stock (Show, Eq) 42 | 43 | data Knowledge = Knowledge 44 | { kSyntax :: Bool 45 | , kMonads :: Bool 46 | , kLens :: Bool 47 | , kTypeLevelMagic :: Bool 48 | , kNix :: Bool 49 | } deriving stock (Show, Eq) 50 | 51 | me :: Haskeller 52 | me = Haskeller 53 | { haskellerName = "Veronika" 54 | , haskellerExperience = 2 55 | , haskellerKnowledge = Knowledge 56 | { kSyntax = True 57 | , kMonads = True 58 | , kLens = True 59 | , kTypeLevelMagic = True 60 | , kNix = False 61 | } 62 | } 63 | 64 | nameL :: Lens' Haskeller String 65 | nameL = lens haskellerName (\h new -> h { haskellerName = new }) 66 | {-# INLINE nameL #-} 67 | 68 | knowledgeL :: Lens' Haskeller Knowledge 69 | knowledgeL = lens haskellerKnowledge (\h new -> h { haskellerKnowledge = new }) 70 | {-# INLINE knowledgeL #-} 71 | 72 | syntaxL :: Lens' Knowledge Bool 73 | syntaxL = lens kSyntax (\k new -> k { kSyntax = new }) 74 | {-# INLINE syntaxL #-} 75 | 76 | data Grade 77 | = Mark Int 78 | | Other String 79 | deriving stock (Show, Eq) 80 | 81 | _Mark :: Prism' Grade Int 82 | _Mark = prism' Mark $ \case 83 | Mark a -> Just a 84 | _other -> Nothing 85 | {-# INLINE _Mark #-} 86 | 87 | gradeMark :: Grade 88 | gradeMark = Mark 5 89 | 90 | gradeOther :: Grade 91 | gradeOther = Other "Satisfactory" 92 | 93 | -- Generators 94 | 95 | genKnowledge :: (MonadGen m) => m Knowledge 96 | genKnowledge = do 97 | kSyntax <- Gen.bool 98 | kMonads <- Gen.bool 99 | kLens <- Gen.bool 100 | kTypeLevelMagic <- Gen.bool 101 | kNix <- Gen.bool 102 | pure Knowledge{..} 103 | 104 | genHaskeller :: (MonadGen m) => m Haskeller 105 | genHaskeller = do 106 | haskellerName <- genName 107 | haskellerExperience <- Gen.int $ Range.linear 0 50 108 | haskellerKnowledge <- genKnowledge 109 | pure Haskeller{..} 110 | 111 | genName :: MonadGen m => m String 112 | genName = Gen.string (Range.linear 1 50) Gen.alphaNum 113 | 114 | genInt :: MonadGen m => m Int 115 | genInt = Gen.enumBounded 116 | 117 | genFunction :: MonadGen m => m (Int -> Int) 118 | genFunction = genInt >>= \n -> Gen.element 119 | [ id 120 | , const n 121 | , (+ n) 122 | , (* n) 123 | , subtract n 124 | , \x -> if x >= n then 1 else 0 125 | ] 126 | 127 | genFun :: MonadGen m => m (Fun Maybe Int Int) 128 | genFun = genFunction >>= \f -> Gen.element $ map Fun 129 | [ Just 130 | , const Nothing 131 | , Just . f 132 | ] 133 | 134 | genForget :: MonadGen m => m (Forget Int Int a) 135 | genForget = Forget . unFun <$> genFun 136 | -------------------------------------------------------------------------------- /test/Test/Prolens/Inspection.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | {- | Performance tests for @prolens@. Uses the @inspection-testing@ 4 | library to make sure that lenses are as efficient as manual record 5 | getters and update syntax. 6 | -} 7 | 8 | module Test.Prolens.Inspection 9 | ( inspectionSpecs 10 | ) where 11 | 12 | import Test.Hspec (Spec, describe, it, shouldSatisfy) 13 | import Test.Inspection (Result (..), hasNoTypeClasses, inspectTest, (===)) 14 | 15 | import Prolens (preview, set, view) 16 | import Test.Data (Grade (..), Haskeller (..), Knowledge (..), _Mark, knowledgeL, nameL, syntaxL) 17 | 18 | 19 | setNameViaLens :: Haskeller -> Haskeller 20 | setNameViaLens = set nameL "Dmitrii" 21 | 22 | setNameManually :: Haskeller -> Haskeller 23 | setNameManually h = h { haskellerName = "Dmitrii" } 24 | 25 | getNameViaLens :: Haskeller -> String 26 | getNameViaLens = view nameL 27 | 28 | getNameManually :: Haskeller -> String 29 | getNameManually (Haskeller name _ _) = name 30 | 31 | setSyntaxViaLens :: Haskeller -> Haskeller 32 | setSyntaxViaLens = set (knowledgeL . syntaxL) True 33 | 34 | setSyntaxManually :: Haskeller -> Haskeller 35 | setSyntaxManually h = h { haskellerKnowledge = (haskellerKnowledge h) { kSyntax = True } } 36 | 37 | getSyntaxViaLens :: Haskeller -> Bool 38 | getSyntaxViaLens = view (knowledgeL . syntaxL) 39 | 40 | getSyntaxManually :: Haskeller -> Bool 41 | getSyntaxManually (Haskeller _ _ (Knowledge syntax _ _ _ _)) = syntax 42 | 43 | inspectionSpecs :: Spec 44 | inspectionSpecs = describe "Performance Inspection Testing" $ do 45 | lensSpecs 46 | prismSpecs 47 | 48 | lensSpecs :: Spec 49 | lensSpecs = describe "Lens" $ do 50 | describe "set" $ do 51 | it "setting single via lens is efficient as manual record update" $ 52 | $(inspectTest $ 'setNameViaLens === 'setNameManually) `shouldSatisfy` isSuccess 53 | it "setting single via lens doesn't have intermediate typeclasses" $ 54 | $(inspectTest $ hasNoTypeClasses 'setNameViaLens) `shouldSatisfy` isSuccess 55 | it "setting composition via lens is efficient as manual record update" $ 56 | $(inspectTest $ 'setSyntaxViaLens === 'setSyntaxManually) `shouldSatisfy` isSuccess 57 | it "setting composition via lens doesn't have intermediate typeclasses" $ 58 | $(inspectTest $ hasNoTypeClasses 'setSyntaxViaLens) `shouldSatisfy` isSuccess 59 | describe "view" $ do 60 | it "getting single via lens is efficient as plain record function" $ 61 | $(inspectTest $ 'getNameViaLens === 'getNameManually) `shouldSatisfy` isSuccess 62 | it "getting single via lens doesn't have intermediate typeclasses" $ 63 | $(inspectTest $ hasNoTypeClasses 'getNameViaLens) `shouldSatisfy` isSuccess 64 | it "getting composition via lens is efficient as plain record function" $ 65 | $(inspectTest $ 'getSyntaxViaLens === 'getSyntaxManually) `shouldSatisfy` isSuccess 66 | it "getting composition via lens doesn't have intermediate typeclasses" $ 67 | $(inspectTest $ hasNoTypeClasses 'getSyntaxViaLens) `shouldSatisfy` isSuccess 68 | 69 | matchMarkPrism :: Grade -> Maybe Int 70 | matchMarkPrism = preview _Mark 71 | 72 | matchMarkManual :: Grade -> Maybe Int 73 | matchMarkManual grade = case grade of 74 | Mark n -> Just n 75 | _other -> Nothing 76 | 77 | prismSpecs :: Spec 78 | prismSpecs = describe "Prism" $ do 79 | describe "preview" $ do 80 | it "preview _Ctor x ≡ case (Ctor _) of" $ 81 | $(inspectTest $ 'matchMarkPrism === 'matchMarkManual) `shouldSatisfy` isSuccess 82 | 83 | -- Helper functions 84 | 85 | isSuccess :: Result -> Bool 86 | isSuccess (Success _) = True 87 | isSuccess (Failure _) = False 88 | -------------------------------------------------------------------------------- /prolens.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: prolens 3 | version: 0.0.0.1 4 | synopsis: Profunctor-based lightweight implementation of optics 5 | description: 6 | Lightweight and performance implementation of optics — lenses, prisms, traversals. 7 | . 8 | The library uses hardcore abstractions internally, but provides 9 | beginner-friendly, composable and convenient interface for working 10 | with data structures. 11 | homepage: https://github.com/kowainik/prolens 12 | bug-reports: https://github.com/kowainik/prolens/issues 13 | license: MPL-2.0 14 | license-file: LICENSE 15 | author: Veronika Romashkina, Dmitrii Kovanikov 16 | maintainer: Kowainik 17 | copyright: 2020-2021 Kowainik 18 | category: Data, Optics, Lenses 19 | build-type: Simple 20 | extra-doc-files: README.md 21 | CHANGELOG.md 22 | tested-with: GHC == 8.6.5 23 | GHC == 8.8.4 24 | GHC == 8.10.4 25 | GHC == 9.0.1 26 | 27 | source-repository head 28 | type: git 29 | location: https://github.com/kowainik/prolens.git 30 | 31 | common common-options 32 | build-depends: base >= 4.12.0.0 && < 4.16 33 | 34 | ghc-options: -Wall 35 | -Wcompat 36 | -Widentities 37 | -Wincomplete-uni-patterns 38 | -Wincomplete-record-updates 39 | -Wredundant-constraints 40 | if impl(ghc >= 8.2) 41 | ghc-options: -fhide-source-paths 42 | if impl(ghc >= 8.4) 43 | ghc-options: -Wmissing-export-lists 44 | -Wpartial-fields 45 | if impl(ghc >= 8.8) 46 | ghc-options: -Wmissing-deriving-strategies 47 | -fwrite-ide-info 48 | -hiedir=.hie 49 | if impl(ghc >= 8.10) 50 | ghc-options: -Wunused-packages 51 | 52 | default-language: Haskell2010 53 | default-extensions: ConstraintKinds 54 | DeriveGeneric 55 | DerivingStrategies 56 | GeneralizedNewtypeDeriving 57 | InstanceSigs 58 | KindSignatures 59 | LambdaCase 60 | OverloadedStrings 61 | RecordWildCards 62 | ScopedTypeVariables 63 | StandaloneDeriving 64 | TupleSections 65 | TypeApplications 66 | ViewPatterns 67 | 68 | common common-test 69 | import: common-options 70 | hs-source-dirs: test 71 | ghc-options: -threaded 72 | 73 | library 74 | import: common-options 75 | hs-source-dirs: src 76 | exposed-modules: Prolens 77 | 78 | test-suite prolens-test 79 | import: common-test 80 | type: exitcode-stdio-1.0 81 | main-is: Spec.hs 82 | 83 | other-modules: Test.Data 84 | Test.Prolens 85 | Test.Prolens.Property 86 | Test.Prolens.Inspection 87 | 88 | build-depends: prolens 89 | , hedgehog >= 1.0.2 && < 2 90 | , hspec ^>= 2.7.4 91 | , hspec-hedgehog 92 | , inspection-testing ^>= 0.4 93 | ghc-options: -rtsopts 94 | -with-rtsopts=-N 95 | 96 | test-suite doctest 97 | import: common-test 98 | type: exitcode-stdio-1.0 99 | main-is: Doctest.hs 100 | build-depends: doctest >= 0.16.3 && < 0.19 101 | -------------------------------------------------------------------------------- /test/Test/Prolens/Property.hs: -------------------------------------------------------------------------------- 1 | module Test.Prolens.Property 2 | ( lensPropertySpecs 3 | , typeclassesPropertySpecs 4 | ) where 5 | 6 | import Hedgehog (Gen) 7 | import Test.Hspec (Spec, describe, it) 8 | import Test.Hspec.Hedgehog (PropertyT, forAll, forAllWith, hedgehog, (===)) 9 | 10 | import Prolens 11 | import Test.Data (genFun, genFunction, genForget, genHaskeller, genInt, genName, nameL) 12 | 13 | 14 | lensPropertySpecs :: Spec 15 | lensPropertySpecs = describe "Lens Laws" $ do 16 | it "view lens (set lens value source) ≡ value" $ hedgehog $ do 17 | source <- forAll genHaskeller 18 | value <- forAll genName 19 | view nameL (set nameL value source) === value 20 | it "set lens (view lens source) source ≡ source" $ hedgehog $ do 21 | source <- forAll genHaskeller 22 | set nameL (view nameL source) source === source 23 | it "set lens valueNew (set lens value source) ≡ set lens valueNew source" $ hedgehog $ do 24 | source <- forAll genHaskeller 25 | value <- forAll genName 26 | valueNew <- forAll genName 27 | set nameL valueNew (set nameL value source) === set nameL valueNew source 28 | 29 | typeclassesPropertySpecs :: Spec 30 | typeclassesPropertySpecs = describe "Class Laws" $ do 31 | profunctorsSpec 32 | monoidalSpec 33 | 34 | profunctorsSpec :: Spec 35 | profunctorsSpec = describe "Profunctor" $ do 36 | profunctorLaws "(->)" genFunction eqFunction 37 | profunctorLaws "Fun" genFun eqFun 38 | profunctorLaws "Forget" genForget eqForget 39 | 40 | 41 | profunctorLaws 42 | :: Profunctor p 43 | => String 44 | -> Gen (p Int Int) 45 | -> (p Int Int -> p Int Int -> PropertyT IO ()) 46 | -> Spec 47 | profunctorLaws name genProfunctor cmp = 48 | describe name $ do 49 | it "Identity: dimap id id ≡ id" $ hedgehog $ do 50 | f <- forAllWith (const "f") genProfunctor 51 | dimap id id f `cmp` f 52 | 53 | it "Composition: dimap (ab . bc) (yz . xy) ≡ dimap bc yz . dimap ab xy" $ hedgehog $ do 54 | f <- forAllWith (const "f") genProfunctor 55 | ab <- forAllWith (const "ab") genFunction 56 | bc <- forAllWith (const "bc") genFunction 57 | xy <- forAllWith (const "xy") genFunction 58 | yz <- forAllWith (const "xy") genFunction 59 | 60 | dimap (ab . bc) (yz . xy) f `cmp` (dimap bc yz . dimap ab xy) f 61 | 62 | 63 | eqFunction :: (Int -> Int) -> (Int -> Int) -> PropertyT IO () 64 | eqFunction f g = do 65 | n <- forAll genInt 66 | f n === g n 67 | 68 | eqFun :: Fun Maybe Int Int -> Fun Maybe Int Int -> PropertyT IO () 69 | eqFun fun1 fun2 = do 70 | x <- forAll genInt 71 | unFun fun1 x === unFun fun2 x 72 | 73 | eqForget :: Forget Int Int a -> Forget Int Int a -> PropertyT IO () 74 | eqForget forget1 forget2 = do 75 | x <- forAll genInt 76 | unForget forget1 x === unForget forget2 x 77 | 78 | monoidalSpec :: Spec 79 | monoidalSpec = describe "Monoidal" $ do 80 | describe "(->)" $ do 81 | it "Identity: pappend f pempty ≡ first f" $ hedgehog $ do 82 | f <- forAllWith (const "f") genFunction 83 | x <- forAll genInt 84 | y <- forAll genInt 85 | pappend f pempty (x, y) === first f (x, y) 86 | it "Identity: pappend pempty f ≡ second f" $ hedgehog $ do 87 | f <- forAllWith (const "f") genFunction 88 | x <- forAll genInt 89 | y <- forAll genInt 90 | pappend pempty f (x, y) === second f (x, y) 91 | it "Associativity (right)" $ hedgehog $ do 92 | f <- forAllWith (const "f") genFunction 93 | g <- forAllWith (const "g") genFunction 94 | h <- forAllWith (const "h") genFunction 95 | x <- forAll genInt 96 | y <- forAll genInt 97 | z <- forAll genInt 98 | pappend f (pappend g h) (x, (y, z)) === (f x, (g y, h z)) 99 | it "Associativity (left)" $ hedgehog $ do 100 | f <- forAllWith (const "f") genFunction 101 | g <- forAllWith (const "g") genFunction 102 | h <- forAllWith (const "h") genFunction 103 | x <- forAll genInt 104 | y <- forAll genInt 105 | z <- forAll genInt 106 | pappend (pappend f g) h ((x, y), z) === ((f x, g y), h z) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prolens 2 | 3 | ![Prolens Logo](https://user-images.githubusercontent.com/8126674/95865685-da91b080-0d5e-11eb-91cd-b6a7bae29262.png) 4 | 5 | [![GitHub CI](https://github.com/kowainik/prolens/workflows/CI/badge.svg)](https://github.com/kowainik/prolens/actions) 6 | [![Hackage](https://img.shields.io/hackage/v/prolens.svg?logo=haskell)](https://hackage.haskell.org/package/prolens) 7 | [![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE) 8 | 9 | The `prolens` package is a Haskell library with a __minimal__ and 10 | __lightweight__ implementation of _optics_. __Optic__ is a high-level 11 | concept for values that provide _composable_ access to different parts of 12 | structures. 13 | 14 | Prolens implements the following optics: 15 | 16 | * __Lens__ — composable getters and setters 17 | * __Prism__ — composable constructors and deconstructors 18 | * __Traversal__ — composable data structures visitors 19 | 20 | ## Goals 21 | 22 | We created the `prolens` project in pursuit of the following goals: 23 | 24 | 1. __Education__. Teach others how to implement and work with 25 | profunctor optics. This also means that some underlying types or 26 | type variables have different unconventional names 27 | 2. __Learning__. Explore new concepts ourselves and understand better 28 | abstractions used in the implementation. 29 | 3. __Minimalism__. Keep the number of dependencies, features and code 30 | low, but still solve common problems. 31 | 4. __Performance__. Despite being minimalist, implement optics so they 32 | are as fast as manual and clumsy pattern matching. 33 | 5. __Exploration__. Understand how different modern Haskell features 34 | can work on improving interface and bring new flavour into standard 35 | approaches. Because of this, we implement our own `Profunctor` 36 | typeclass with the 37 | [QuantifiedConstraints](https://downloads.haskell.org/ghc/latest/docs/html/users_guide/glasgow_exts.html#quantified-constraints) 38 | feature, which is not present in any other library at the moment. 39 | 6. __Profunctors__. We use profunctor encoding of optics because it 40 | has more elegant design with fewer surprises. 41 | 42 | ## Features 43 | 44 | 1. __Lightweight__. Only 45 | [base](http://hackage.haskell.org/package/base) 46 | in dependencies. The project itself also has a rather small amount 47 | of code. 48 | 2. __Fast__. Despite being lightweight, `prolens` provides a 49 | performant API. We use the 50 | [inspection-testing](https://hackage.haskell.org/package/inspection-testing) 51 | library to guarantee that our implementation of optics compiles to 52 | the same code as plain Haskell getters, record-update syntax and 53 | pattern matching. 54 | 3. __Excellent documentation__. The `prolens` library contains a 55 | mini-tutorial on optics, enough to understand how and when to use 56 | basic lenses and prisms. 57 | 4. __Beginner-friendly__. The abstractions in the implementation are 58 | hardcore, but our documentation presents the concept in a 59 | beginner-friendly and approachable manner. 60 | 5. __Lawful__. We use property-based testing to make sure that laws of 61 | all underlying abstractions are verified. 62 | 63 | ## How to use 64 | 65 | `prolens` is compatible with the latest GHC compiler 66 | versions starting from `8.6.5`. 67 | 68 | In order to start using `prolens` in your project, you 69 | will need to set it up with the three easy steps: 70 | 71 | 1. Add the dependency on `prolens` in your project's 72 | `.cabal` file. For this, you should modify the `build-depends` 73 | section by adding the name of this library. After the adjustment, 74 | this section could look like this: 75 | 76 | ```haskell 77 | build-depends: base ^>= 4.15 78 | , prolens ^>= LATEST_VERSION 79 | ``` 80 | 2. In the module where you wish to use composable getters and setters, 81 | you should add the import: 82 | 83 | ```haskell 84 | import Prolens (Lens', lens, view) 85 | ``` 86 | 3. Now you can use the types and functions from the library: 87 | 88 | ```haskell 89 | data User = User 90 | { userName :: String 91 | , userAge :: Int 92 | } 93 | 94 | nameL :: Lens' User String 95 | nameL = lens userName (\u new -> u { userName = new }) 96 | 97 | main :: IO () 98 | main = putStrln $ view nameL (User "Johnny" 27) 99 | ``` 100 | 101 | ### Usage with Stack 102 | 103 | If `prolens` is not available on your current Stackage 104 | resolver yet, fear not! You can still use it from Hackage by adding 105 | the following to the `extra-deps` section of your `stack.yaml` file: 106 | 107 | ```yaml 108 | extra-deps: 109 | - prolens-CURRENT_VERSION 110 | ``` 111 | 112 | ## Comparison to other libraries 113 | 114 | 1. [lens](https://hackage.haskell.org/package/lens) 115 | 116 | It is the most mature Haskell library for optics. `lens` provides a 117 | richer interface, but it is heavyweight and based on Van Laarhoven (VL) 118 | encoding of lenses. 119 | 120 | 2. [microlens](https://hackage.haskell.org/package/microlens) 121 | 122 | A lightweight implementation of optics compatible with 123 | `lens`. `microlens` is also minimalistic, but it doesn't provide 124 | prisms and is based on VL encoding. 125 | 126 | 3. [optics](https://hackage.haskell.org/package/optics) 127 | 128 | The `optics` library uses the profunctor encoding. It provides much 129 | more features than `prolens`, but at the same time it's 130 | heavyweight. Also, `optics` uses an opaque representation of optics 131 | (e.g. they are wrapped in a newtype), which means that they are 132 | composed using the custom operator `%`, while in `prolens` optics 133 | are type aliases to functions and can be easily composed with the 134 | dot `.` operator. 135 | 136 | 4. [profunctor-optics](https://hackage.haskell.org/package/profunctor-optics) 137 | 138 | This library is also based on profunctor encoding (as the name 139 | suggests) and provides optics as aliases to functions. But it is 140 | more heavyweight, though it provides more features. 141 | 142 | In addition to this per-library comparison, `prolens` has a few unique 143 | features: 144 | 145 | * Beginner-friendly documentation with usage examples 146 | * Usage of `inspection-testing` to guarantee the performance of 147 | optics 148 | * Property-based tests of lens and typeclasses laws to make sure 149 | that all abstractions behave properly 150 | 151 | ## Acknowledgement 152 | 153 | * Edward Kmett for lenses and profunctor typeclasses 154 | * Well-Typed for the implementation of `optics` 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/Prolens.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-redundant-constraints #-} 2 | 3 | {-# LANGUAGE QuantifiedConstraints #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | 7 | {- | 8 | Module : Prolens 9 | Copyright : (c) 2020-2021 Kowainik 10 | SPDX-License-Identifier : MPL-2.0 11 | Maintainer : Kowainik 12 | Stability : Stable 13 | Portability : Portable 14 | 15 | The @prolens@ package is a Haskell library with a minimal and lightweight 16 | implementation of optics based on 'Profunctor's. __'Optic'__ is a high-level 17 | concept for values that provide composable access to different parts of structures. 18 | 19 | "Prolens" implements the following optics: 20 | 21 | * 'Lens' — composable getters and setters 22 | * 'Prism' — composable constructors and deconstructors 23 | * 'Traversal' — composable data structures visitors 24 | 25 | == Usage 26 | 27 | To use lenses or prisms in your project, you need to add @prolens@ package as 28 | the dependency in the @build-depends@ field of your @.cabal@ file. E.g.: 29 | 30 | @ 31 | build-depends: prolens ^>= 0.0.0.0 32 | @ 33 | 34 | You should add the import of this module in the place of lenses usage: 35 | 36 | @ 37 | __import__ "Prolens" 38 | @ 39 | 40 | == Creating your own optics 41 | 42 | We show in each section of this module how to create values of each 43 | kind of optics. 44 | 45 | ⚠️ __The general crucial rule__ for achieving maximum performance: 46 | always add @\{\-\# INLINE ... \#\-\}@ pragmas to your optics. 47 | 48 | == Typeclasses table 49 | 50 | The below table shows required constraints for each 'Optic': 51 | 52 | +-------------+------------------------------+ 53 | | Optic | Constraints | 54 | +=============+==============================+ 55 | | 'Lens' | @'Strong' p@ | 56 | +-------------+------------------------------+ 57 | | 'Prism' | @'Choice' p@ | 58 | +-------------+------------------------------+ 59 | | 'Traversal' | @('Choice' p, 'Monoidal' p)@ | 60 | +-------------+------------------------------+ 61 | 62 | == Usage table: get, set, modify 63 | 64 | Here is a go-to table on how to use getter, setters and modifiers with different 65 | 'Optic's. 66 | 67 | +-------------+------------------+--------------+------------------+------------------+-----------------+-----------------+ 68 | | | get | get operator | set | set operator | modify | modify operator | 69 | +=============+==================+==============+==================+==================+=================+=================+ 70 | | 'Lens' | @'view' l x@ | @x '^.' l@ | @'set' l new x@ | @x & l '.~' new@ | @'over' l f x@ | @x & l '%~' f@ | 71 | +-------------+------------------+--------------+------------------+------------------+-----------------+-----------------+ 72 | | 'Prism' | @'preview' _L x@ | - | @'set' _L new x@ | - | @'over' _L f x@ | - | 73 | +-------------+------------------+--------------+------------------+------------------+-----------------+-----------------+ 74 | | 'Traversal' | @'view' l x@ | - | @'set' l new x@ | - | @'over' l f x@ | - | 75 | +-------------+------------------+--------------+------------------+------------------+-----------------+-----------------+ 76 | 77 | @since 0.0.0.0 78 | -} 79 | 80 | module Prolens 81 | ( -- * Profunctor typeclass 82 | Profunctor (..) 83 | 84 | -- * Optics 85 | , Optic 86 | 87 | -- * Lenses 88 | -- $lenses 89 | 90 | -- ** Lenses types 91 | , Lens 92 | , Lens' 93 | -- ** Strong typeclass 94 | , Strong (..) 95 | 96 | -- ** Lenses functions 97 | , set 98 | , over 99 | , view 100 | , lens 101 | 102 | -- ** Lenses operators 103 | , (^.) 104 | , (.~) 105 | , (%~) 106 | 107 | -- ** Standard lenses 108 | , fstL 109 | , sndL 110 | 111 | -- * Prisms 112 | -- $prisms 113 | 114 | -- ** Prism types 115 | , Prism 116 | , Prism' 117 | -- ** Choice typeclass 118 | , Choice (..) 119 | 120 | -- ** Prism functions 121 | , prism 122 | , prism' 123 | , preview 124 | 125 | -- ** Standard Prisms 126 | , _Just 127 | , _Left 128 | , _Right 129 | 130 | -- * Traversals 131 | 132 | -- ** Traversal types 133 | , Traversal 134 | -- ** Monoidal typeclass 135 | , Monoidal (..) 136 | 137 | -- ** Traversal functions 138 | , traverseOf 139 | 140 | -- ** Standard traversals 141 | , eachPair 142 | , eachMaybe 143 | , eachList 144 | 145 | -- * Internal data types 146 | , Forget (..) 147 | , Fun (..) 148 | ) where 149 | 150 | import Control.Applicative (Const (..), liftA2) 151 | import Data.Coerce (coerce) 152 | import Data.Monoid (First (..)) 153 | 154 | 155 | -- $setup 156 | -- >>> import Data.Function ((&)) 157 | 158 | 159 | {- | The type @p@ is called 'Profunctor' and it means, that a value of 160 | type @p in out@ takes a value of type @in@ as an argument (input) and 161 | outputs a value of type @out@. 'Profunctor' allows mapping of inputs 162 | and outputs. 163 | 164 | @ 165 | +----> Result input 166 | | 167 | | +--> Original profunctor 168 | | +-> Original input | 169 | + + + 170 | dimap :: (in2 -> in1) -> (out1 -> out2) -> p in1 out1 -> p in2 out2 171 | + + 172 | | +-> Result output 173 | | 174 | +-> Original output 175 | @ 176 | 177 | Speaking in terms of other abstractions, 'Profunctor' is 178 | 'Data.Functor.Contravariant.Contravariant' in the first type argument 179 | (type variable @in@) and 'Functor' in the second type argument (type 180 | variable @out@). 181 | 182 | Moreover, @p in@ must have 'Functor' instance first to implement the 183 | 'Profunctor' instance. This required using @QuantifiedConstraints@. 184 | 185 | @ 186 | Contravariant <---+ 187 | | 188 | +-+-+ 189 | + + 190 | (forall a . Functor (p a)) => Profunctor p a b 191 | + + + 192 | | | | 193 | +--> Quantified constraint +++ 194 | | 195 | Functor <--+ 196 | @ 197 | 198 | Instances of 'Profunctor' should satisfy the following laws: 199 | 200 | * __Identity:__ @'dimap' 'id' 'id' ≡ 'id'@ 201 | * __Composition:__ @'dimap' (inAB . inBC) (outYZ . outXY) ≡ 'dimap' inBC outYZ . 'dimap' inAB outXY@ 202 | 203 | @since 0.0.0.0 204 | -} 205 | -- type Profunctor :: (Type -> Type -> Type) -> Constraint 206 | class (forall a . Functor (p a)) => Profunctor p where 207 | dimap 208 | :: (in2 -> in1) -- ^ Map input 209 | -> (out1 -> out2) -- ^ Map output 210 | -> p in1 out1 -- ^ Take @in1@ as input and return @out1@ 211 | -> p in2 out2 -- ^ Take @in2@ as input and return @out2@ 212 | 213 | -- | @since 0.0.0.0 214 | instance Profunctor (->) where 215 | dimap :: (in2 -> in1) -> (out1 -> out2) -> (in1 -> out1) -> (in2 -> out2) 216 | dimap in21 out12 f = out12 . f . in21 217 | {-# INLINE dimap #-} 218 | 219 | {- | @'Fun' m a b@ is a wrapper for function @a -> m b@. 220 | 221 | @since 0.0.0.0 222 | -} 223 | newtype Fun m a b = Fun 224 | { unFun :: a -> m b 225 | } 226 | 227 | -- | @since 0.0.0.0 228 | instance Functor m => Functor (Fun m x) where 229 | fmap :: (a -> b) -> Fun m x a -> Fun m x b 230 | fmap f (Fun xma) = Fun (fmap f . xma) 231 | {-# INLINE fmap #-} 232 | 233 | -- | @since 0.0.0.0 234 | instance Functor m => Profunctor (Fun m) where 235 | dimap :: (in2 -> in1) -> (out1 -> out2) -> Fun m in1 out1 -> Fun m in2 out2 236 | dimap in21 out12 (Fun f) = Fun (fmap out12 . f . in21) 237 | {-# INLINE dimap #-} 238 | 239 | {- | 'Strong' is a 'Profunctor' that can be lifted to take a pair as 240 | an input and return a pair. 241 | 242 | The second element of a pair (variable of type @c@) can be of any 243 | type, and you can decide what type it should be. This is convenient 244 | for implementing various functions. E.g. 'lens' uses this fact. 245 | 246 | Instances of 'Strong' should satisfy the following laws: 247 | 248 | * __'first' via 'second' swap:__ @'first' ≡ 'dimap' 'Data.Tuple.swap' 'Data.Tuple.swap' . 'second'@ 249 | * __'second' via 'first' swap:__ @'second' ≡ 'dimap' 'Data.Tuple.swap' 'Data.Tuple.swap' . 'first'@ 250 | 251 | * __Fst functor:__ @'dimap' 'fst' 'id' ≡ 'fmap' 'fst' . 'first'@ 252 | * __Snd functor:__ @'dimap' 'snd' 'id' ≡ 'fmap' 'snd' . 'second'@ 253 | 254 | * __Distribution over 'first':__ @'dimap' ('second' f) 'id' . 'first' ≡ 'fmap' ('second' f) . 'first'@ 255 | * __Distribution over 'second':__ @'dimap' ('first' f) 'id' . 'second' ≡ 'fmap' ('first' f) . 'second'@ 256 | 257 | * __Associativity of 'first':__ @'first' . 'first' ≡ 'dimap' (\\((a, b), c) -> (a, (b, c))) (\\(a, (b, c)) -> ((a, b), c)) . 'first'@ 258 | * __Associativity of 'second':__ @'second' . 'second' ≡ 'dimap' (\\(a, (b, c)) -> ((a, b), c)) (\\((a, b), c) -> (a, (b, c))) . 'second'@ 259 | 260 | @since 0.0.0.0 261 | -} 262 | class Profunctor p => Strong p where 263 | first :: p a b -> p (a, c) (b, c) 264 | second :: p a b -> p (c, a) (c, b) 265 | 266 | -- | @since 0.0.0.0 267 | instance Strong (->) where 268 | first :: (a -> b) -> (a, c) -> (b, c) 269 | first ab (a, c) = (ab a, c) 270 | {-# INLINE first #-} 271 | 272 | second :: (a -> b) -> (c, a) -> (c, b) 273 | second ab (c, a) = (c, ab a) 274 | {-# INLINE second #-} 275 | 276 | -- | @since 0.0.0.0 277 | instance (Functor m) => Strong (Fun m) where 278 | first :: Fun m a b -> Fun m (a, c) (b, c) 279 | first (Fun amb) = Fun (\(a, c) -> fmap (, c) (amb a)) 280 | {-# INLINE first #-} 281 | 282 | second :: Fun m a b -> Fun m (c, a) (c, b) 283 | second (Fun amb) = Fun (\(c, a) -> fmap (c,) (amb a)) 284 | {-# INLINE second #-} 285 | 286 | {- | 'Choice' is a 'Profunctor' that can be lifted to work with 287 | 'Either' given input or some other value. 288 | 289 | The other element of 'Either' (variable of type @c@) can be of any 290 | type, and you can decide what type it should be. This is convenient 291 | for implementing various functions. E.g. 'prism' uses this fact. 292 | 293 | 294 | Assuming, we have the following functions in scope: 295 | 296 | @ 297 | swapEither :: Either a b -> Either b a 298 | unnestLeft :: Either (Either a b) c -> Either a (Either b c) 299 | unnestRight :: Either a (Either b c) -> Either (Either a b) c 300 | @ 301 | 302 | Instances of 'Choice' should satisfy the following laws: 303 | 304 | * __'left' via 'right' swap:__ @'left' ≡ 'dimap' swapEither swapEither . 'right'@ 305 | * __'right' via 'left' swap:__ @'right' ≡ 'dimap' swapEither swapEither . 'left'@ 306 | 307 | * __'Left' functor:__ @'fmap' 'Left' ≡ 'dimap' 'Left' 'id' . 'left'@ 308 | * __'Right' functor:__ @'fmap' 'Right' ≡ 'dimap' 'Right' 'id' . 'right'@ 309 | 310 | * __Distribution over 'left':__ @'dimap' ('right' f) 'id' . 'left' ≡ 'fmap' ('right' f) . 'left'@ 311 | * __Distribution over 'right':__ @'dimap' ('left' f) 'id' . 'right' ≡ 'fmap' ('left' f) . 'right'@ 312 | 313 | * __Associativity of 'left':__ @'left' . 'left' ≡ 'dimap' unnestLeft unnestRight . 'left'@ 314 | * __Associativity of 'right':__ @'right' . 'right' ≡ 'dimap' unnestRight unnestLeft . 'right'@ 315 | 316 | @since 0.0.0.0 317 | -} 318 | class Profunctor p => Choice p where 319 | left :: p a b -> p (Either a c) (Either b c) 320 | right :: p a b -> p (Either c a) (Either c b) 321 | 322 | -- | @since 0.0.0.0 323 | instance Choice (->) where 324 | left :: (a -> b) -> Either a c -> Either b c 325 | left ab = \case 326 | Left a -> Left $ ab a 327 | Right c -> Right c 328 | {-# INLINE left #-} 329 | 330 | right :: (a -> b) -> Either c a -> Either c b 331 | right ab = \case 332 | Right a -> Right $ ab a 333 | Left c -> Left c 334 | {-# INLINE right #-} 335 | 336 | -- | @since 0.0.0.0 337 | instance (Applicative m) => Choice (Fun m) where 338 | left :: Fun m a b -> Fun m (Either a c) (Either b c) 339 | left (Fun amb)= Fun $ \eitherAc -> case eitherAc of 340 | Left a -> Left <$> amb a 341 | Right c -> pure $ Right c 342 | {-# INLINE left #-} 343 | 344 | right :: Fun m a b -> Fun m (Either c a) (Either c b) 345 | right (Fun amb)= Fun $ \eitherCa -> case eitherCa of 346 | Right a -> Right <$> amb a 347 | Left c -> pure $ Left c 348 | {-# INLINE right #-} 349 | 350 | {- | 'Monoidal' is 'Strong' 'Profunctor' that can be appended. It is 351 | similar to 'Monoid's but for higher-kinded types. 352 | 353 | Instances of 'Monoidal' should satisfy the following laws: 354 | 355 | * __Right identity:__ @'pappend' f 'pempty' ≡ 'first' f@ 356 | * __Left identity:__ @'pappend' 'pempty' f ≡ 'second' f@ 357 | * __Associativity:__ @'pappend' f ('pappend' g h) ⋍ 'pappend' ('pappend' f g) h@ 358 | 359 | ⚠️ __Note:__ The @⋍@ operator in the __associativity__ law is equality 360 | ignoring the structure. The law is written in that way because 361 | 'pappend' returns a tuple and the order of nested tuples depends on 362 | the order of 'pappend' applications. In practice, this means, that if 363 | you want to check the law, you reorder tuples in the following way: 364 | 365 | @ 366 | 'pappend' f ('pappend' g h) ≡ 'dimap' (\\(a, (b, c)) -> ((a, b), c)) (\\((a, b), c) -> (a, (b, c))) ('pappend' ('pappend' f g) h) 367 | @ 368 | 369 | @since 0.0.0.0 370 | -} 371 | class Strong p => Monoidal p where 372 | pappend :: p a b -> p c d -> p (a, c) (b, d) 373 | pempty :: p a a 374 | 375 | -- | @since 0.0.0.0 376 | instance Monoidal (->) where 377 | pappend :: (a -> b) -> (c -> d) -> (a, c) -> (b, d) 378 | pappend ab cd (a, c) = (ab a, cd c) 379 | {-# INLINE pappend #-} 380 | 381 | pempty :: a -> a 382 | pempty = id 383 | {-# INLINE pempty #-} 384 | 385 | -- | @since 0.0.0.0 386 | instance (Applicative m) => Monoidal (Fun m) where 387 | pappend :: Fun m a b -> Fun m c d -> Fun m (a, c) (b, d) 388 | pappend (Fun amb) (Fun cmd) = Fun (\(a, c) -> liftA2 (,) (amb a) (cmd c)) 389 | {-# INLINE pappend #-} 390 | 391 | pempty :: Fun m a a 392 | pempty = Fun (pure . id) 393 | {-# INLINE pempty #-} 394 | 395 | {- | 'Optic' takes a connection from @a@ to @b@ (represented as a 396 | value of type @p a b@) and returns a connection from @source@ to 397 | @target@ (represented as a value of type @p source target@). 398 | 399 | @ 400 | +---> Profunctor 401 | | 402 | | +----> Final input 403 | | | 404 | | | +-> Final output 405 | | | | 406 | + + + 407 | type Optic p source target a b 408 | + + 409 | | | 410 | Given input <--+ | 411 | | 412 | Given output <-------+ 413 | @ 414 | 415 | @since 0.0.0.0 416 | -} 417 | type Optic p source target a b = p a b -> p source target 418 | 419 | 420 | {- $lenses 421 | 422 | == Example 423 | 424 | To understand better how to use this library lets look at some simple example. 425 | Let's say we have the user and address data types in our system: 426 | 427 | >>> :{ 428 | data Address = Address 429 | { addressCountry :: String 430 | , addressCity :: String 431 | , addressIndex :: String 432 | } deriving (Show) 433 | :} 434 | 435 | >>> :{ 436 | data User = User 437 | { userName :: String 438 | , userAge :: Int 439 | , userAddress :: Address 440 | } deriving (Show) 441 | :} 442 | 443 | We can easily get fields of the @User@ and @Address@ types, but 444 | setting values is inconvenient (especially for nested records). To 445 | solve this problem, we can use lenses — composable getters and 446 | setters. 'Lens' is a value, so we need to define lenses for fields of 447 | our data types first. 448 | 449 | To create the lens for the @userName@ field we can use 'lens' function and 450 | manually writing getter and setter function: 451 | 452 | >>> :{ 453 | nameL :: Lens' User String 454 | nameL = lens getter setter 455 | where 456 | getter :: User -> String 457 | getter = userName 458 | setter :: User -> String -> User 459 | setter user newName = user {userName = newName} 460 | :} 461 | 462 | In this manner, we can create other lenses for our @User@ data type. 463 | Usually, lenses are one-liners, and we can define them easily using lambda-functions. 464 | 465 | >>> :{ 466 | ageL :: Lens' User Int 467 | ageL = lens userAge (\u new -> u {userAge = new}) 468 | :} 469 | 470 | >>> :{ 471 | addressL :: Lens' User Address 472 | addressL = lens userAddress (\u new -> u {userAddress = new}) 473 | :} 474 | 475 | We want to have lenses for accessing @Adress@ fields inside @User@, so we want to have the following values: 476 | 477 | @ 478 | countryL :: 'Lens'' User 'String' 479 | cityL :: 'Lens'' User 'String' 480 | indexL :: 'Lens'' User 'String' 481 | @ 482 | 483 | /Note:/ for lenses as @countryL@, @cityL@ etc., we are using composition of the 484 | lenses for the @userAddress@ field. If we have 485 | 486 | >>> :{ 487 | addressCityL :: Lens' Address String 488 | addressCityL = lens addressCity (\a new -> a {addressCity = new}) 489 | :} 490 | 491 | then 492 | 493 | >>> cityL = addressL . addressCityL 494 | 495 | Let's say we have some sample user 496 | 497 | >>> :{ 498 | address = Address 499 | { addressCountry = "UK" 500 | , addressCity = "London" 501 | , addressIndex = "XXX" 502 | } 503 | user :: User 504 | user = User 505 | { userName = "John" 506 | , userAge = 42 507 | , userAddress = address 508 | } 509 | :} 510 | 511 | To view the fields of the User data type we can use 'view' or '^.' 512 | 513 | >>> view ageL user 514 | 42 515 | >>> user ^. cityL 516 | "London" 517 | 518 | If we want to change any of the user's data, we should use 'set' or '.~' 519 | 520 | >>> set nameL "Johnny" user 521 | User {userName = "Johnny", userAge = 42, userAddress = Address {addressCountry = "UK", addressCity = "London", addressIndex = "XXX"}} 522 | >>> user & cityL .~ "Bristol" 523 | User {userName = "John", userAge = 42, userAddress = Address {addressCountry = "UK", addressCity = "Bristol", addressIndex = "XXX"}} 524 | 525 | 'over' or '%~' operator could be useful when, for example, you want to increase the age by one on the user's birthday: 526 | 527 | >>> over ageL succ user 528 | User {userName = "John", userAge = 43, userAddress = Address {addressCountry = "UK", addressCity = "London", addressIndex = "XXX"}} 529 | >>> user & ageL %~ succ 530 | User {userName = "John", userAge = 43, userAddress = Address {addressCountry = "UK", addressCity = "London", addressIndex = "XXX"}} 531 | -} 532 | 533 | {- | 'Lens' represents composable getters and setters. 534 | 535 | 'Lens' is an @'Optic' p@ with the 'Strong' constraint on the @p@ type variable. 536 | 537 | @ 538 | +---> Current object 539 | | 540 | | +-> Final object 541 | | | 542 | + + 543 | type Lens source target a b 544 | + + 545 | | | 546 | Current field <--+ | 547 | | 548 | Final field <-------+ 549 | @ 550 | 551 | @since 0.0.0.0 552 | -} 553 | type Lens source target a b = forall p . Strong p => Optic p source target a b 554 | 555 | {- | The monomorphic lenses which don't change the type of the container (or of 556 | the value inside). It has a 'Strong' constraint, and it can be used whenever a 557 | getter or a setter is needed. 558 | 559 | * @a@ is the type of the value inside of structure 560 | * @source@ is the type of the whole structure 561 | 562 | For most use-cases it's enought to use this 'Lens'' instead of more general 'Lens'. 563 | 564 | @since 0.0.0.0 565 | -} 566 | type Lens' source a = Lens source source a a 567 | 568 | {- | Sets the given value to the structure using a setter. 569 | 570 | @since 0.0.0.0 571 | -} 572 | set :: (p ~ (->)) 573 | => Optic p source target a b -- ^ 'Optic' that can be lens 574 | -> b -- ^ Value to set 575 | -> source -- ^ Object where we want to set value 576 | -> target -- ^ Resulting object with @b@ set 577 | set abst = abst . const 578 | {-# INLINE set #-} 579 | 580 | {- | Applies the given function to the target. 581 | 582 | @since 0.0.0.0 583 | -} 584 | over 585 | :: (p ~ (->)) 586 | => Optic p source target a b -- ^ 'Optic' that can be lens 587 | -> (a -> b) -- ^ Field modification function 588 | -> source -- ^ Object where we want to set value 589 | -> target -- ^ Resulting object with the modified field 590 | over = id 591 | {-# INLINE over #-} 592 | 593 | {- | Gets a value out of a structure using a getter. 594 | 595 | @since 0.0.0.0 596 | -} 597 | view 598 | :: (p ~ Fun (Const a)) 599 | => Optic p source target a b -- ^ 'Optic' that can be lens 600 | -> source -- ^ Object from which we want to get value 601 | -> a -- ^ Field of @source@ 602 | view opt = coerce (opt (Fun Const)) 603 | {-# INLINE view #-} 604 | -- view opt = getConst . unFun (opt (Fun Const)) 605 | -- opt :: Fun (Const a) a b -> Fun (Const a) s t 606 | -- opt :: (a -> Const a b) -> ( s -> Const a t) 607 | 608 | {- | Creates 'Lens' from the getter and setter. 609 | 610 | @since 0.0.0.0 611 | -} 612 | lens 613 | :: (source -> a) -- ^ Getter 614 | -> (source -> b -> target) -- ^ Setter 615 | -> Lens source target a b 616 | lens getter setter = dimap (\s -> (s, getter s)) (uncurry setter) . second 617 | {-# INLINE lens #-} 618 | 619 | {- | The operator form of 'view' with the arguments flipped. 620 | 621 | @since 0.0.0.0 622 | -} 623 | infixl 8 ^. 624 | (^.) :: source -> Lens' source a -> a 625 | s ^. l = view l s 626 | {-# INLINE (^.) #-} 627 | 628 | {- | The operator form of 'set'. 629 | 630 | @since 0.0.0.0 631 | -} 632 | infixr 4 .~ 633 | (.~) :: Lens' source a -> a -> source -> source 634 | (.~) l = set l 635 | {-# INLINE (.~) #-} 636 | 637 | {- | The operator form of 'over'. 638 | 639 | @since 0.0.0.0 640 | -} 641 | infixr 4 %~ 642 | (%~) :: Lens' source a -> (a -> a) -> source -> source 643 | (%~) l = over l 644 | {-# INLINE (%~) #-} 645 | 646 | {- | 'Lens'' for a tuple on the first argument. 647 | 648 | >>> view fstL (42, "str") 649 | 42 650 | 651 | @since 0.0.0.0 652 | -} 653 | fstL :: Lens (a, c) (b, c) a b 654 | fstL = lens fst $ \(_, b) new -> (new, b) 655 | {-# INLINE fstL #-} 656 | 657 | {- | 'Lens'' for a tuple on the second argument. 658 | 659 | >>> view sndL (42, "Hello") 660 | "Hello" 661 | 662 | @since 0.0.0.0 663 | -} 664 | sndL :: Lens (x, a) (x, b) a b 665 | sndL = lens snd $ \(a, _) new -> (a, new) 666 | {-# INLINE sndL #-} 667 | 668 | {- $prisms 669 | Prisms work with sum types. 670 | 671 | == Example 672 | 673 | Let's say we have the user data type in our system: 674 | 675 | >>> :{ 676 | data Address = Address 677 | { addressCountry :: String 678 | , addressCity :: String 679 | } deriving (Show) 680 | :} 681 | 682 | >>> :{ 683 | data Payload 684 | = NamePayload String 685 | | IdPayload Int 686 | | AddressPayload Address 687 | deriving (Show) 688 | :} 689 | 690 | To create the prism for each constructor we can use 'prism'' function and 691 | manually writing getter and setter function: 692 | 693 | /NOTE:/ The naming convention for prisms is the following: 694 | 695 | @ 696 | _ConstructorName 697 | @ 698 | 699 | >>> :{ 700 | _NamePayload :: Prism' Payload String 701 | _NamePayload = prism' construct match 702 | where 703 | match :: Payload -> Maybe String 704 | match p = case p of 705 | NamePayload name -> Just name 706 | _otherPayload -> Nothing 707 | construct :: String -> Payload 708 | construct = NamePayload 709 | :} 710 | 711 | In this manner, we can create other prisms for our @Payload@ data type. 712 | 713 | >>> :{ 714 | _IdPayload :: Prism' Payload Int 715 | _IdPayload = prism' IdPayload $ \p -> case p of 716 | IdPayload i -> Just i 717 | _otherPayload -> Nothing 718 | :} 719 | 720 | >>> :{ 721 | _AddressPayload :: Prism' Payload Address 722 | _AddressPayload = prism' AddressPayload $ \p -> case p of 723 | AddressPayload a -> Just a 724 | _otherPayload -> Nothing 725 | :} 726 | 727 | Let's say we have some sample payload 728 | 729 | >>> :{ 730 | payloadName :: Payload 731 | payloadName = NamePayload "Some name" 732 | :} 733 | 734 | To view the fields of the @Payload@ data type we can use 'preview' 735 | 736 | >>> preview _NamePayload payloadName 737 | Just "Some name" 738 | >>> preview _IdPayload payloadName 739 | Nothing 740 | 741 | If we want to change any of the data, we should use 'set' or '.~' (just like in lenses) 742 | 743 | >>> set _NamePayload "Johnny" payloadName 744 | NamePayload "Johnny" 745 | >>> set _IdPayload 3 payloadName 746 | NamePayload "Some name" 747 | 748 | Note, that you can easily compose lenses and prisms together: 749 | 750 | >>> :{ 751 | address = Address 752 | { addressCountry = "UK" 753 | , addressCity = "London" 754 | } 755 | :} 756 | 757 | >>> :{ 758 | addressCityL :: Lens' Address String 759 | addressCityL = lens addressCity (\a new -> a {addressCity = new}) 760 | :} 761 | 762 | >>> :{ 763 | payloadAddress :: Payload 764 | payloadAddress = AddressPayload address 765 | :} 766 | 767 | >>> set _AddressPayload (address & addressCityL .~ "Bristol") payloadAddress 768 | AddressPayload (Address {addressCountry = "UK", addressCity = "Bristol"}) 769 | -} 770 | 771 | {- | 'Prism' represents composable constructors and deconstructors. 772 | 773 | 'Prism' is an @'Optic' p@ with 'Choice' constraint on the @p@ type 774 | variable. 775 | 776 | @ 777 | +---> Current object 778 | | 779 | | +-> Final object 780 | | | 781 | + + 782 | type Prism source target a b 783 | + + 784 | | | 785 | Field in current constructor <--+ | 786 | | 787 | Field in final constructor <-------+ 788 | @ 789 | 790 | @since 0.0.0.0 791 | -} 792 | type Prism source target a b = forall p . Choice p => Optic p source target a b 793 | 794 | {- | The monomorphic prisms which don't change the type of the container (or of 795 | the value inside). 796 | 797 | * @a@ is the value inside the particular constructor 798 | * @source@ is some sum type 799 | 800 | @since 0.0.0.0 801 | -} 802 | type Prism' source a = Prism source source a a 803 | 804 | {- | Newtype around function @a -> r@. It's called /forget/ because it 805 | forgets about its last type variable. 806 | 807 | @since 0.0.0.0 808 | -} 809 | newtype Forget r a b = Forget 810 | { unForget :: a -> Maybe r 811 | } 812 | 813 | -- | @since 0.0.0.0 814 | instance Functor (Forget r x) where 815 | fmap :: (a -> b) -> Forget r x a -> Forget r x b 816 | fmap _ = coerce 817 | {-# INLINE fmap #-} 818 | 819 | -- | @since 0.0.0.0 820 | instance Profunctor (Forget r) where 821 | dimap :: (a -> b) -> (c -> d) -> Forget r b c -> Forget r a d 822 | dimap ab _cd (Forget br) = Forget (br . ab) 823 | {-# INLINE dimap #-} 824 | 825 | -- | @since 0.0.0.0 826 | instance Strong (Forget r) where 827 | first :: Forget r a b -> Forget r (a, c) (b, c) 828 | first (Forget ar) = Forget (ar . fst) 829 | {-# INLINE first #-} 830 | 831 | second :: Forget r a b -> Forget r (c, a) (c, b) 832 | second (Forget ar) = Forget (ar . snd) 833 | {-# INLINE second #-} 834 | 835 | -- | @since 0.0.0.0 836 | instance Choice (Forget r) where 837 | left :: Forget r a b -> Forget r (Either a c) (Either b c) 838 | left (Forget ar) = Forget (either ar (const Nothing)) 839 | {-# INLINE left #-} 840 | 841 | right :: Forget r a b -> Forget r (Either c a) (Either c b) 842 | right (Forget ar) = Forget (either (const Nothing) ar) 843 | {-# INLINE right #-} 844 | 845 | -- | @since 0.0.0.0 846 | instance Monoidal (Forget r) where 847 | pappend :: Forget r a b -> Forget r c d -> Forget r (a, c) (b, d) 848 | pappend (Forget ar) (Forget cr) = Forget 849 | (\(a, c) -> getFirst $ First (ar a) <> First (cr c)) 850 | {-# INLINE pappend #-} 851 | 852 | pempty :: Forget r a a 853 | pempty = Forget (const Nothing) 854 | {-# INLINE pempty #-} 855 | 856 | {- | Match a value from @source@ type. 857 | 858 | @since 0.0.0.0 859 | -} 860 | preview 861 | :: forall a source p 862 | . (p ~ Forget a) 863 | => Optic p source source a a -- ^ 'Optic' that can be prism 864 | -> source -- ^ Object (possible sum type) 865 | -> Maybe a -- ^ Value of type @a@ from a specific constructor 866 | preview paapss = coerce (paapss wrap) 867 | where 868 | wrap :: Forget a a a 869 | wrap = coerce @(a -> Maybe a) @(Forget a a a) Just 870 | {-# INLINE wrap #-} 871 | {-# INLINE preview #-} 872 | -- preview paapss = getFirst . unForget (paapss (Forget (First . Just))) 873 | -- paapss :: Forget (First a) a a -> Forget (First a) source source 874 | -- paapss :: (a -> First a) -> source -> First a 875 | -- paapss :: (a -> Maybe a) -> source -> Maybe a 876 | 877 | {- | Create 'Prism' from constructor and matching function. 878 | 879 | @since 0.0.0.0 880 | -} 881 | prism 882 | :: (b -> target) -- ^ Constructor 883 | -> (source -> Either target a) -- ^ Matching function 884 | -> Prism source target a b 885 | -- prism :: (b -> target) -> (source -> Either target a) -> p a b -> p source target 886 | prism ctor match = dimap match (either id ctor) . right 887 | {-# INLINE prism #-} 888 | 889 | {- | Create monomorphic 'Prism'' from constructor and matching function. 890 | 891 | @since 0.0.0.0 892 | -} 893 | prism' 894 | :: (a -> source) -- ^ Constructor 895 | -> (source -> Maybe a) -- ^ Matching function 896 | -> Prism' source a 897 | prism' ctor match = prism ctor (\s -> maybe (Left s) Right $ match s) 898 | {-# INLINE prism' #-} 899 | 900 | {- | 'Prism' for a 'Just' of 'Maybe'. 901 | 902 | >>> preview _Just (Just 42) 903 | Just 42 904 | 905 | >>> preview _Just Nothing 906 | Nothing 907 | 908 | @since 0.0.0.0 909 | -} 910 | _Just :: Prism (Maybe a) (Maybe b) a b 911 | _Just = prism Just $ \case 912 | Just a -> Right a 913 | Nothing -> Left Nothing 914 | {-# INLINE _Just #-} 915 | 916 | 917 | {- | 'Prism' for a 'Left' of 'Either'. 918 | 919 | >>> preview _Left (Left 42) 920 | Just 42 921 | 922 | >>> preview _Left (Right "Hello") 923 | Nothing 924 | 925 | @since 0.0.0.0 926 | -} 927 | _Left :: Prism (Either a x) (Either b x) a b 928 | _Left = prism Left $ \case 929 | Left l -> Right l 930 | Right r -> Left $ Right r 931 | {-# INLINE _Left #-} 932 | 933 | {- | 'Prism' for a 'Left' of 'Either'. 934 | 935 | >>> preview _Right (Left 42) 936 | Nothing 937 | 938 | >>> preview _Right (Right "Hello") 939 | Just "Hello" 940 | 941 | @since 0.0.0.0 942 | -} 943 | _Right :: Prism (Either x a) (Either x b) a b 944 | _Right = prism Right $ \case 945 | Right a -> Right a 946 | Left x -> Left $ Left x 947 | {-# INLINE _Right #-} 948 | 949 | 950 | {- | 'Traversal' provides composable ways to visit different parts of 951 | a data structure. 952 | 953 | 'Traversal' is an @'Optic' p@ with the 'Choice' and 'Monoidal' 954 | constraints on the @p@ type variable. 955 | 956 | @ 957 | +---> Current collection 958 | | 959 | | +-> Final collection 960 | | | 961 | + + 962 | type Traversal source target a b 963 | + + 964 | | | 965 | Current element <--+ | 966 | | 967 | Final element <-------+ 968 | @ 969 | 970 | @since 0.0.0.0 971 | -} 972 | type Traversal source target a b 973 | = forall p 974 | . (Choice p, Monoidal p) 975 | => Optic p source target a b 976 | 977 | {- | Traverse a data structure using given 'Traversal'. 978 | 979 | >>> traverseOf eachPair putStrLn ("Hello", "World!") 980 | Hello 981 | World! 982 | ((),()) 983 | 984 | @since 0.0.0.0 985 | -} 986 | traverseOf 987 | :: (Applicative f, p ~ Fun f) 988 | => Optic p source target a b -- ^ 'Optic' that can be a traversal 989 | -> (a -> f b) -- ^ Traversing function 990 | -> source -- ^ Data structure to traverse 991 | -> f target -- ^ Traversing result 992 | traverseOf pabPst aFb = unFun (pabPst (Fun aFb)) 993 | -- pabPst :: Fun f a b -> Fun f source target 994 | -- pabPst :: (a -> f b) -> Fun f source target 995 | 996 | {- | 'Traversal' for a pair of same type elements. 997 | 998 | >>> over eachPair (+ 1) (3, 7) 999 | (4,8) 1000 | 1001 | @since 0.0.0.0 1002 | -} 1003 | eachPair :: Traversal (a, a) (b, b) a b 1004 | eachPair pab = pappend pab pab 1005 | 1006 | {- | 'Traversal' for a 'Maybe'. 1007 | 1008 | >>> over eachMaybe (+ 1) (Just 3) 1009 | Just 4 1010 | >>> over eachMaybe (+ 1) Nothing 1011 | Nothing 1012 | 1013 | @since 0.0.0.0 1014 | -} 1015 | eachMaybe :: Traversal (Maybe a) (Maybe b) a b 1016 | eachMaybe pab = dimap maybeToEither eitherToMaybe (left pab) 1017 | where 1018 | maybeToEither :: Maybe a -> Either a () 1019 | maybeToEither = \case 1020 | Just a -> Left a 1021 | Nothing -> Right () 1022 | 1023 | eitherToMaybe :: Either a () -> Maybe a 1024 | eitherToMaybe = \case 1025 | Left a -> Just a 1026 | Right () -> Nothing 1027 | 1028 | {- | 'Traversal' for lists. 1029 | 1030 | >>> over eachList (+ 1) [1..5] 1031 | [2,3,4,5,6] 1032 | >>> over eachList (+ 1) [] 1033 | [] 1034 | 1035 | @since 0.0.0.0 1036 | -} 1037 | eachList :: Traversal [a] [b] a b 1038 | eachList pab = dimap listToEither eitherToList $ left $ pappend pab (eachList pab) 1039 | where 1040 | listToEither :: [a] -> Either (a, [a]) () 1041 | listToEither = \case 1042 | [] -> Right () 1043 | x:xs -> Left (x, xs) 1044 | 1045 | eitherToList :: Either (a, [a]) () -> [a] 1046 | eitherToList = \case 1047 | Right () -> [] 1048 | Left (x, xs) -> x:xs 1049 | --------------------------------------------------------------------------------