├── .cache ├── git │ ├── hooks │ │ └── pre-push │ └── config └── vale │ ├── Vocab │ └── dualizer │ │ └── accept.txt │ └── config │ └── vocabularies │ └── dualizer │ └── accept.txt ├── resources ├── curry.png ├── coproduct.png ├── product.png ├── unknown.png ├── product-haskell.png ├── unknown-haskell.png ├── coproduct-haskell.png ├── FormationLogo_320x132.png ├── expanding-brain-category.jpg ├── README.md ├── duality-nescala.org └── duality-boulder-haskell.org ├── .envrc ├── .dir-locals.el ├── .github ├── renovate.json ├── workflows │ ├── hackage-publish.yml │ └── build.yml └── settings.yml ├── .config ├── mustache.yaml └── project │ ├── default.nix │ └── hlint.nix ├── .vale.ini ├── dualizer ├── tests │ └── doctests.hs ├── CHANGELOG.md ├── src │ └── Categorical │ │ ├── Dual │ │ ├── Lens.hs │ │ ├── Base.hs │ │ ├── Prelude.hs │ │ └── Example.hs │ │ └── Dual.hs ├── Setup.hs ├── dualizer.cabal ├── README.md └── docs │ └── license-report.md ├── .editorconfig ├── .gitignore ├── cabal.project ├── .gitattributes ├── .hlint.yaml ├── garnix.yaml ├── README.md ├── flake.nix ├── flake.lock └── LICENSE /.cache/git/hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | nix flake check 4 | -------------------------------------------------------------------------------- /resources/curry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/curry.png -------------------------------------------------------------------------------- /resources/coproduct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/coproduct.png -------------------------------------------------------------------------------- /resources/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/product.png -------------------------------------------------------------------------------- /resources/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/unknown.png -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | direnv_layout_dir="$PWD/.cache/direnv" 3 | use flake 4 | -------------------------------------------------------------------------------- /resources/product-haskell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/product-haskell.png -------------------------------------------------------------------------------- /resources/unknown-haskell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/unknown-haskell.png -------------------------------------------------------------------------------- /resources/coproduct-haskell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/coproduct-haskell.png -------------------------------------------------------------------------------- /resources/FormationLogo_320x132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/FormationLogo_320x132.png -------------------------------------------------------------------------------- /resources/expanding-brain-category.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sellout/dualizer/HEAD/resources/expanding-brain-category.jpg -------------------------------------------------------------------------------- /.cache/git/config: -------------------------------------------------------------------------------- 1 | ; This file was generated by Project Manager. 2 | [commit "template"] 3 | contents = "" 4 | path = ".config/git/template/commit.txt" 5 | 6 | [core] 7 | hooksPath = "../.cache/git/hooks" 8 | -------------------------------------------------------------------------------- /.cache/vale/Vocab/dualizer/accept.txt: -------------------------------------------------------------------------------- 1 | direnv 2 | formatter 3 | garnix 4 | [Nn]ix 5 | Pfeil 6 | ShellCheck 7 | API 8 | bugfix 9 | comonad 10 | conditionalize 11 | formatter 12 | functor 13 | GADT 14 | inline 15 | Kleisli 16 | Kmett 17 | pragma 18 | unformatted 19 | widening 20 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil 2 | (eglot-workspace-configuration 3 | :nil (:formatting (:command '("project-manager" "fmt" "--" "--")))) 4 | (fill-column . 80) 5 | (indent-tabs-mode . nil) 6 | (projectile-project-configure-cmd . "nix flake update") 7 | (sentence-end-double-space . nil))) 8 | -------------------------------------------------------------------------------- /.cache/vale/config/vocabularies/dualizer/accept.txt: -------------------------------------------------------------------------------- 1 | direnv 2 | formatter 3 | garnix 4 | [Nn]ix 5 | Pfeil 6 | ShellCheck 7 | API 8 | bugfix 9 | comonad 10 | conditionalize 11 | formatter 12 | functor 13 | GADT 14 | inline 15 | Kleisli 16 | Kmett 17 | pragma 18 | unformatted 19 | widening 20 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | {"$schema":"https://docs.renovatebot.com/renovate-schema.json","extends":["config:recommended"],"lockFileMaintenance":{"automerge":true,"enabled":true},"nix":{"enabled":true},"packageRules":[{"automerge":true,"matchCurrentVersion":"!/^0/","matchUpdateTypes":["minor","patch"]}]} -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # Talks, etc. on duality 2 | 3 | - [Duality and How to Delete Half (minus ε) of Your Code](duality-boulder-haskell.org) – Boulder **Haskell** Meetup, January 2018 4 | - [Duality and How to Delete Half (minus ε) of Your Code](duality-nescala.org) – NE**Scala** Boston, March 2018 ([video](https://www.youtube.com/watch?v=VGZi4nTgZxs)) 5 | -------------------------------------------------------------------------------- /.config/mustache.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "project": 3 | { 4 | "description": "A library for defining duals automatically, as well as labeling duals in existing packages.", 5 | "name": "dualizer", 6 | "repo": "sellout/dualizer", 7 | "summary": "Automatically generate dual constructions", 8 | }, 9 | "type": { "name": "haskell" }, 10 | } 11 | -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | ; This file was generated by Project Manager. 2 | MinAlertLevel=suggestion 3 | Packages=Microsoft 4 | StylesPath=.cache/vale 5 | Vocab=dualizer 6 | 7 | [*] 8 | BasedOnStyles=Vale, Microsoft 9 | Microsoft.Dashes=NO 10 | Microsoft.GeneralURL=NO 11 | Microsoft.Headings=NO 12 | Microsoft.Quotes=NO 13 | Microsoft.Ranges=NO 14 | Microsoft.Vocab=NO 15 | Microsoft.We=NO 16 | -------------------------------------------------------------------------------- /dualizer/tests/doctests.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE Unsafe #-} 2 | 3 | module Main (main) where 4 | 5 | import safe "base" Data.Function (($)) 6 | import safe "base" Data.Semigroup (Semigroup ((<>))) 7 | import safe "base" System.IO (IO) 8 | import "doctest" Test.DocTest (doctest) 9 | import "this" Build_doctests (flags, module_sources, pkgs) 10 | 11 | main :: IO () 12 | main = doctest $ flags <> pkgs <> module_sources 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | root=true 3 | 4 | [*] 5 | binary_next_line=true 6 | charset=utf-8 7 | end_of_line=lf 8 | indent_size=2 9 | indent_style=space 10 | insert_final_newline=true 11 | space_redirects=true 12 | switch_case_indent=true 13 | trim_trailing_whitespace=true 14 | 15 | [*.{diff,patch}] 16 | trim_trailing_whitespace=false 17 | 18 | [*.{el,lisp}] 19 | indent_size=unset 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /result 2 | /source 3 | dist-newstyle 4 | /.cache/vale/* 5 | !/.cache/vale/config/ 6 | /.cache/vale/config/* 7 | !/.cache/vale/config/vocabularies/ 8 | /.cache/vale/config/vocabularies/* 9 | !/.cache/vale/config/vocabularies/dualizer/ 10 | !/.cache/vale/Vocab/ 11 | /.cache/vale/Vocab/* 12 | !/.cache/vale/Vocab/dualizer/ 13 | /.local/state/project-manager/ 14 | /.local/state/nix/profiles/ 15 | /.cache/direnv/ 16 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | constraints: 2 | -- This is only a transitive dependency, and only used before `Contravariant` 3 | -- was added to `base` (GHC 8.10), but versions before 1.2.1 don’t enforce 4 | -- `Safe`, and can cause compilation failures in our dependencies. 5 | StateVar >= 1.2.1, 6 | -- This is only a transitive dependency, but 0.5 doesn’t build in some cases. 7 | distributive >= 0.5.2, 8 | 9 | flags: 10 | -noisy-deprecations 11 | 12 | program-options 13 | ghc-options: 14 | -Werror 15 | 16 | tests: True 17 | 18 | packages: 19 | ./dualizer/dualizer.cabal 20 | -------------------------------------------------------------------------------- /dualizer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). 7 | 8 | ## [0.2.0.0] - 2025-04-07 9 | 10 | ### Added 11 | 12 | - support for GHC 9.10 13 | 14 | ## [0.1.0.1] - 2019-01-05 15 | 16 | ## [0.1.0.0] - 2019-01-04 17 | 18 | ### Added 19 | 20 | - initial release of this package 21 | 22 | [0.1.0.0]: https://github.com/sellout/dualizer/releases/tag/v0..1.0.0 23 | -------------------------------------------------------------------------------- /dualizer/src/Categorical/Dual/Lens.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE Unsafe #-} 3 | 4 | -- | Dual mappings for types in 'lens'. 5 | module Categorical.Dual.Lens 6 | ( lensDuals, 7 | ) 8 | where 9 | 10 | import Categorical.Dual (exportDuals, importDuals, labelDual, labelSelfDual) 11 | import Categorical.Dual.Prelude (preludeDuals) 12 | import safe Control.Lens (Equality, Iso, Lens, Prism, alongside, without) 13 | 14 | importDuals preludeDuals 15 | 16 | labelDual ''Lens ''Prism 17 | labelDual 'alongside 'without 18 | 19 | labelSelfDual ''Iso 20 | 21 | labelSelfDual ''Equality 22 | 23 | -- | Duals for 'lens'. 24 | exportDuals "lensDuals" 25 | -------------------------------------------------------------------------------- /dualizer/Setup.hs: -------------------------------------------------------------------------------- 1 | -- __NB__: `custom-setup` doesn’t have any way to specify extensions or options, 2 | -- so any we want need to be specified here. 3 | {-# LANGUAGE PackageImports #-} 4 | {-# LANGUAGE Unsafe #-} 5 | {-# LANGUAGE NoImplicitPrelude #-} 6 | {-# OPTIONS_GHC -Weverything #-} 7 | -- Warns even when `Unsafe` is explicit, not inferred. See 8 | -- https://gitlab.haskell.org/ghc/ghc/-/issues/16689 9 | {-# OPTIONS_GHC -Wno-unsafe #-} 10 | 11 | module Main (main) where 12 | 13 | import safe "base" System.IO (IO) 14 | import "cabal-doctest" Distribution.Extra.Doctest (defaultMainWithDoctests) 15 | 16 | main :: IO () 17 | main = defaultMainWithDoctests "doctests" 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.cache/git/config linguist-generated 2 | /.cache/git/hooks/pre-push linguist-generated 3 | /.cache/vale/Vocab/dualizer/accept.txt linguist-generated 4 | /.cache/vale/config/vocabularies/dualizer/accept.txt linguist-generated 5 | /.dir-locals.el linguist-generated 6 | /.editorconfig linguist-generated 7 | /.envrc linguist-generated 8 | /.gitattributes linguist-generated 9 | /.github/renovate.json linguist-generated 10 | /.github/settings.yml linguist-generated 11 | /.gitignore linguist-generated 12 | /.hlint.yaml linguist-generated 13 | /.vale.ini linguist-generated 14 | /.github/workflows/build.yml linguist-generated 15 | /garnix.yaml linguist-generated 16 | /.github/workflows/hackage-publish.yml linguist-generated 17 | -------------------------------------------------------------------------------- /.github/workflows/hackage-publish.yml: -------------------------------------------------------------------------------- 1 | {"jobs":{"hackage-publish":{"permissions":{"contents":"read","id-token":"write"},"runs-on":"ubuntu-24.04","steps":[{"uses":"actions/checkout@v4","with":{"ref":"${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"}},{"id":"setup-haskell-cabal","uses":"haskell-actions/setup@v2","with":{"cabal-version":"3.12.1.0","ghc-version":"9.10.1"}},{"run":"cabal v2-sdist --output-directory='${{ runner.temp }}/packages/' all"},{"uses":"haskell-actions/hackage-publish@v1","with":{"hackageToken":"${{ secrets.HACKAGE_AUTH_TOKEN }}","packagesPath":"${{ runner.temp }}/packages/","publish":true}}]}},"name":"Publish release to Hackage","on":{"push":{"tags":["v?[0-9]+.[0-9]+.[0-9]+*"]},"workflow_dispatch":{"inputs":{"tag":{"description":"The existing version to publish to Hackage","required":true,"type":"string"}}}}} -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | [{"group":{"enabled":true,"name":"dollar"}},{"group":{"enabled":true,"name":"future"}},{"group":{"enabled":true,"name":"generalise"}},{"ignore":{"name":"Eta reduce"}},{"ignore":{"name":"Evaluate"}},{"ignore":{"name":"Reduce duplication"}},{"ignore":{"name":"Use list comprehension"}},{"ignore":{"name":"Use section"}},{"package":{"modules":["import Control.Monad"],"name":"monad"}},{"package":{"modules":["import Data.Foldable","import Data.Traversable"],"name":"traversable"}},{"group":{"imports":["package monad","package traversable"],"name":"generalize","rules":[{"warn":{"lhs":"forM","rhs":"for"}},{"warn":{"lhs":"forM_","rhs":"for_"}},{"warn":{"lhs":"map","rhs":"fmap"}},{"warn":{"lhs":"mapM","rhs":"traverse"}},{"warn":{"lhs":"mapM_","rhs":"traverse_"}},{"warn":{"lhs":"return","rhs":"pure"}},{"warn":{"lhs":"sequence","rhs":"sequenceA"}},{"warn":{"lhs":"sequence_","rhs":"sequenceA_"}}]}},{"group":{"imports":["package traversable"],"name":"generalize","rules":[{"hint":{"lhs":"maybe (pure ())","note":"IncreasesLaziness","rhs":"traverse_"}},{"warn":{"lhs":"mappend","rhs":"(<>)"}},{"warn":{"lhs":"(++)","rhs":"(<>)"}}]}}] -------------------------------------------------------------------------------- /dualizer/src/Categorical/Dual/Base.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE Unsafe #-} 3 | 4 | -- | Dual mappings for types in 'base'. 5 | module Categorical.Dual.Base 6 | ( baseDuals, 7 | ) 8 | where 9 | 10 | import Categorical.Dual 11 | ( exportDuals, 12 | importDuals, 13 | labelDual, 14 | labelSelfDual, 15 | labelSemiDual, 16 | ) 17 | import Categorical.Dual.Prelude (preludeDuals) 18 | import safe Control.Arrow 19 | ( Arrow, 20 | arr, 21 | returnA, 22 | (&&&), 23 | (***), 24 | (+++), 25 | (<<<), 26 | (>>>), 27 | (|||), 28 | ) 29 | import safe Control.Category (Category) 30 | import safe Control.Category qualified as Category 31 | import safe Prelude qualified 32 | 33 | importDuals preludeDuals 34 | 35 | labelSelfDual ''Arrow 36 | labelSelfDual 'arr 37 | labelDual '(***) '(+++) 38 | labelDual '(&&&) '(|||) 39 | labelSelfDual 'returnA 40 | 41 | labelSelfDual ''Category 42 | labelSelfDual 'Prelude.id 43 | labelSelfDual 'Category.id 44 | labelSemiDual '(Prelude..) '(>>>) 45 | labelSemiDual '(Category..) '(>>>) 46 | labelDual '(<<<) '(>>>) 47 | 48 | -- | Duals for 'base'. 49 | exportDuals "baseDuals" 50 | -------------------------------------------------------------------------------- /.config/project/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | flaky, 4 | lib, 5 | pkgs, 6 | self, 7 | supportedSystems, 8 | ... 9 | }: { 10 | project = { 11 | name = "dualizer"; 12 | summary = "Automatically generate dual constructions"; 13 | }; 14 | 15 | imports = [./hlint.nix]; 16 | 17 | ## dependency management 18 | services.renovate.enable = true; 19 | 20 | ## development 21 | programs = { 22 | direnv.enable = true; 23 | # This should default by whether there is a .git file/dir (and whether it’s 24 | # a file (worktree) or dir determines other things – like where hooks 25 | # are installed. 26 | git.enable = true; 27 | }; 28 | 29 | ## formatting 30 | editorconfig.enable = true; 31 | 32 | programs = { 33 | treefmt.enable = true; 34 | vale.enable = true; 35 | }; 36 | 37 | ## CI 38 | services.garnix.enable = true; 39 | ## FIXME: Shouldn’t need `mkForce` here (or to duplicate the base contexts). 40 | ## Need to improve module merging. 41 | services.github.settings.branches.main.protection.required_status_checks.contexts = 42 | lib.mkForce 43 | ([ 44 | "All Garnix checks" 45 | "check-bounds" 46 | "check-licenses" 47 | ] 48 | ++ lib.concatMap (sys: 49 | lib.concatMap (ghc: [ 50 | "build (${ghc}, ${sys})" 51 | "build (--prefer-oldest, ${ghc}, ${sys})" 52 | ]) 53 | self.lib.nonNixTestedGhcVersions) 54 | self.lib.githubSystems); 55 | services.haskell-ci = { 56 | inherit (self.lib) defaultGhcVersion; 57 | systems = self.lib.githubSystems; 58 | ghcVersions = self.lib.nonNixTestedGhcVersions; 59 | cabalPackages = {"${config.project.name}" = config.project.name;}; 60 | latestGhcVersion = "9.10.1"; 61 | ## FIXME: These should live in Flaky, only here for testing! 62 | exclude = [ 63 | { 64 | ghc = "9.4.1"; 65 | os = "macos-14"; 66 | } 67 | ]; 68 | include = map (bounds: { 69 | inherit bounds; 70 | ghc = "9.4.5"; 71 | os = "macos-14"; 72 | }) ["--prefer-oldest" ""]; 73 | }; 74 | 75 | ## publishing 76 | services.github.enable = true; 77 | services.github.settings.repository.topics = []; 78 | } 79 | -------------------------------------------------------------------------------- /garnix.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | {"builds":[{"exclude":["devShells.*.lax-checks","*.x86_64-darwin","*.x86_64-darwin.*","*.x86_64-darwin-example"],"include":["homeConfigurations.*","nixosConfigurations.*","checks.aarch64-darwin.*","devShells.aarch64-darwin.default","packages.aarch64-darwin.default","devShells.aarch64-darwin.ghc966","packages.aarch64-darwin.ghc966_all","devShells.aarch64-darwin.ghc8107","packages.aarch64-darwin.ghc8107_all","devShells.aarch64-darwin.ghc902","packages.aarch64-darwin.ghc902_all","devShells.aarch64-darwin.ghc925","packages.aarch64-darwin.ghc925_all","devShells.aarch64-darwin.ghc945","packages.aarch64-darwin.ghc945_all","devShells.aarch64-darwin.ghc963","packages.aarch64-darwin.ghc963_all","devShells.aarch64-darwin.ghc981","packages.aarch64-darwin.ghc981_all","devShells.aarch64-darwin.ghc9101","packages.aarch64-darwin.ghc9101_all","checks.aarch64-linux.*","devShells.aarch64-linux.default","packages.aarch64-linux.default","devShells.aarch64-linux.ghc966","packages.aarch64-linux.ghc966_all","devShells.aarch64-linux.ghc8107","packages.aarch64-linux.ghc8107_all","devShells.aarch64-linux.ghc902","packages.aarch64-linux.ghc902_all","devShells.aarch64-linux.ghc925","packages.aarch64-linux.ghc925_all","devShells.aarch64-linux.ghc945","packages.aarch64-linux.ghc945_all","devShells.aarch64-linux.ghc963","packages.aarch64-linux.ghc963_all","devShells.aarch64-linux.ghc981","packages.aarch64-linux.ghc981_all","devShells.aarch64-linux.ghc9101","packages.aarch64-linux.ghc9101_all","checks.i686-linux.*","devShells.i686-linux.default","packages.i686-linux.default","devShells.i686-linux.ghc966","packages.i686-linux.ghc966_all","devShells.i686-linux.ghc8107","packages.i686-linux.ghc8107_all","devShells.i686-linux.ghc902","packages.i686-linux.ghc902_all","devShells.i686-linux.ghc925","packages.i686-linux.ghc925_all","devShells.i686-linux.ghc945","packages.i686-linux.ghc945_all","devShells.i686-linux.ghc963","packages.i686-linux.ghc963_all","devShells.i686-linux.ghc981","packages.i686-linux.ghc981_all","devShells.i686-linux.ghc9101","packages.i686-linux.ghc9101_all","checks.x86_64-linux.*","devShells.x86_64-linux.default","packages.x86_64-linux.default","devShells.x86_64-linux.ghc966","packages.x86_64-linux.ghc966_all","devShells.x86_64-linux.ghc8107","packages.x86_64-linux.ghc8107_all","devShells.x86_64-linux.ghc902","packages.x86_64-linux.ghc902_all","devShells.x86_64-linux.ghc925","packages.x86_64-linux.ghc925_all","devShells.x86_64-linux.ghc945","packages.x86_64-linux.ghc945_all","devShells.x86_64-linux.ghc963","packages.x86_64-linux.ghc963_all","devShells.x86_64-linux.ghc981","packages.x86_64-linux.ghc981_all","devShells.x86_64-linux.ghc9101","packages.x86_64-linux.ghc9101_all"]}],"servers":[]} -------------------------------------------------------------------------------- /dualizer/src/Categorical/Dual/Prelude.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE Unsafe #-} 3 | 4 | -- | Dual mappings for types in the 'Prelude'. 5 | module Categorical.Dual.Prelude 6 | ( preludeDuals, 7 | ) 8 | where 9 | 10 | import Categorical.Dual 11 | ( emptyDuals, 12 | exportDuals, 13 | importDuals, 14 | labelDual, 15 | labelSelfDual, 16 | ) 17 | import safe Data.Bool (Bool, not, (&&), (||)) 18 | import safe Data.Char (Char) 19 | import safe Data.Either (Either (Left, Right)) 20 | import safe Data.Eq (Eq, (/=), (==)) 21 | import safe Data.Function (const, id) 22 | import safe Data.Functor (Functor, fmap, (<$), (<$>)) 23 | import safe Data.Int (Int) 24 | import safe Data.List (map) 25 | import safe Data.Ord (Ord, Ordering, max, min, (<), (<=), (>), (>=)) 26 | import safe Data.Ratio (Rational) 27 | import safe Data.String (String) 28 | import safe Data.Tuple (fst, snd) 29 | import safe Data.Word (Word) 30 | import safe System.IO 31 | ( IO, 32 | getChar, 33 | getLine, 34 | putChar, 35 | putStrLn, 36 | readFile, 37 | writeFile, 38 | ) 39 | import safe Text.Read (Read, read) 40 | import safe Text.Show (Show, show) 41 | import safe Prelude 42 | ( Bounded, 43 | Double, 44 | Enum, 45 | Float, 46 | Floating, 47 | Fractional, 48 | Integer, 49 | Integral, 50 | Num, 51 | Real, 52 | RealFrac, 53 | maxBound, 54 | minBound, 55 | ) 56 | 57 | importDuals emptyDuals 58 | 59 | labelSelfDual ''Bool 60 | labelDual '(&&) '(||) 61 | labelSelfDual 'not 62 | 63 | labelDual ''Either ''(,) 64 | 65 | labelSelfDual ''Ordering 66 | 67 | labelSelfDual ''Char 68 | 69 | labelSelfDual ''String 70 | 71 | labelDual 'fst 'Left 72 | labelDual 'snd 'Right 73 | 74 | labelSelfDual ''Eq 75 | labelSelfDual '(==) 76 | labelSelfDual '(/=) 77 | 78 | labelSelfDual ''Ord 79 | labelDual '(<) '(>=) 80 | labelDual '(<=) '(>) 81 | labelDual 'max 'min 82 | 83 | labelSelfDual ''Enum 84 | 85 | labelSelfDual ''Bounded 86 | labelDual 'minBound 'maxBound 87 | 88 | labelSelfDual ''Int 89 | 90 | labelSelfDual ''Integer 91 | 92 | labelSelfDual ''Float 93 | 94 | labelSelfDual ''Double 95 | 96 | labelSelfDual ''Rational 97 | 98 | labelSelfDual ''Word 99 | 100 | labelSelfDual ''Num 101 | 102 | labelSelfDual ''Real 103 | 104 | labelSelfDual ''Integral 105 | 106 | labelSelfDual ''Fractional 107 | 108 | labelSelfDual ''Floating 109 | 110 | labelSelfDual ''RealFrac 111 | 112 | -- Monoid has a dual in the hask package (Comonoid) 113 | 114 | labelSelfDual ''Functor 115 | labelSelfDual 'fmap 116 | labelSelfDual '(<$) 117 | labelSelfDual '(<$>) 118 | 119 | -- Monad has a dual in the comonads package (Comonad) 120 | 121 | -- Traversable has a dual in the distributive package (Distributive) 122 | 123 | labelSelfDual 'id 124 | labelSelfDual 'const 125 | 126 | -- labelSelfDual 'undefined 127 | 128 | labelSelfDual 'map 129 | 130 | labelDual ''Show ''Read 131 | labelDual 'show 'read 132 | 133 | labelSelfDual ''IO 134 | labelDual 'putChar 'getChar 135 | labelDual 'putStrLn 'getLine 136 | 137 | labelDual 'readFile 'writeFile 138 | 139 | -- | Duals for the 'Prelude'. 140 | exportDuals "preludeDuals" 141 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | {"jobs":{"build":{"env":{"CONFIG":"--enable-tests --enable-benchmarks ${{ matrix.bounds }}"},"runs-on":"${{ matrix.os }}","steps":[{"uses":"actions/checkout@v4"},{"id":"setup-haskell-cabal","uses":"haskell-actions/setup@v2","with":{"cabal-version":"3.12.1.0","ghc-version":"${{ matrix.ghc }}"}},{"run":"cabal v2-freeze $CONFIG"},{"uses":"actions/cache@v4","with":{"key":"${{ matrix.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}","path":"${{ steps.setup-haskell-cabal.outputs.cabal-store }}\ndist-newstyle\n"}},{"run":"cabal v2-build all $CONFIG"},{"run":"cabal v2-test all $CONFIG"},{"run":"mv dist-newstyle/cache/plan.json plan-${{ matrix.os }}-${{ matrix.ghc }}${{ matrix.bounds }}.json"},{"name":"Upload build plan as artifact","uses":"actions/upload-artifact@v4","with":{"name":"plan-${{ matrix.os }}-${{ matrix.ghc }}${{ matrix.bounds }}","path":"plan-${{ matrix.os }}-${{ matrix.ghc }}${{ matrix.bounds }}.json"}}],"strategy":{"fail-fast":false,"matrix":{"bounds":["--prefer-oldest",""],"exclude":[{"ghc":"8.10.1","os":"macos-14"},{"ghc":"9.0.1","os":"macos-14"},{"ghc":"9.2.1","os":"macos-14"},{"ghc":"9.4.1","os":"macos-14"}],"ghc":["8.10.1","9.0.1","9.2.1","9.4.1","9.6.1","9.8.1","9.10.1"],"include":[{"bounds":"--prefer-oldest","ghc":"9.4.5","os":"macos-14"},{"bounds":"","ghc":"9.4.5","os":"macos-14"}],"os":["macos-13","macos-14","ubuntu-24.04","windows-2022"]}}},"check-bounds":{"if":"always()","needs":["build"],"runs-on":"ubuntu-24.04","steps":[{"uses":"actions/checkout@v4"},{"id":"setup-haskell-cabal","uses":"haskell-actions/setup@v2","with":{"cabal-version":"3.12.1.0","ghc-version":"9.6.6"}},{"run":"cabal install cabal-plan-bounds"},{"name":"download Cabal plans","uses":"actions/download-artifact@v4","with":{"merge-multiple":true,"path":"plans","pattern":"plan-*"}},{"name":"Cabal plans considered in generated bounds","run":"find plans/"},{"name":"check if bounds have changed","run":"diffs=\"$(find . -name '*.cabal' -exec \\\n cabal-plan-bounds \\\n --dry-run \\\n \\\n plans/*.json \\\n --cabal {} \\;)\"\nif [[ -n \"$diffs\" ]]; then\n echo \"$diffs\"\n exit 1\nfi\n"}]},"check-licenses":{"if":"always()","needs":["build"],"runs-on":"ubuntu-24.04","steps":[{"uses":"actions/checkout@v4"},{"id":"setup-haskell-cabal","uses":"haskell-actions/setup@v2","with":{"cabal-version":"3.12.1.0","ghc-version":"9.6.6"}},{"run":"cabal install cabal-plan \\\n --flags='exe license-report' \\\n --ghc-options='-Wwarn'\n"},{"name":"download Cabal plans","uses":"actions/download-artifact@v4","with":{"merge-multiple":true,"path":"plans","pattern":"plan-*"}},{"run":"mkdir -p dist-newstyle/cache\nmv plans/plan-ubuntu-24.04-9.10.1.json dist-newstyle/cache/plan.json\n"},{"name":"check if licenses have changed","run":"declare -A packages=([dualizer]=dualizer)\nfor package in \"${!packages[@]}\"; do\n {\n echo \"**NB**: This captures the licenses associated with a particular set of dependency versions. If your own build solves differently, it’s possible that the licenses may have changed, or even that the set of dependencies itself is different. Please make sure you run [\\`cabal-plan license-report\\`](https://hackage.haskell.org/package/cabal-plan) on your own components rather than assuming this is authoritative.\"\n echo\n cabal-plan license-report \"$package:lib:$package\"\n } >\"${packages[$package]}/docs/license-report.md\"\ndone\ngit diff --exit-code */docs/license-report.md\n"}]}},"name":"CI","on":{"pull_request":{"types":["opened","synchronize"]},"push":{"branches":["main"]}}} -------------------------------------------------------------------------------- /.config/project/hlint.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | pkgs, 4 | ... 5 | }: { 6 | ## Haskell linter 7 | programs.treefmt.programs.hlint.enable = true; 8 | ## TODO: Wrap this to find our generated hlint config in the store. 9 | project.devPackages = [pkgs.hlint]; 10 | project.file.".hlint.yaml".text = lib.generators.toYAML {} [ 11 | { 12 | group = { 13 | name = "dollar"; 14 | enabled = true; 15 | }; 16 | } 17 | { 18 | group = { 19 | name = "future"; 20 | enabled = true; 21 | }; 22 | } 23 | { 24 | group = { 25 | name = "generalise"; 26 | enabled = true; 27 | }; 28 | } 29 | 30 | {ignore = {name = "Eta reduce";};} 31 | {ignore = {name = "Evaluate";};} 32 | {ignore = {name = "Reduce duplication";};} 33 | {ignore = {name = "Use list comprehension";};} 34 | {ignore = {name = "Use section";};} 35 | 36 | { 37 | package = { 38 | name = "monad"; 39 | modules = ["import Control.Monad"]; 40 | }; 41 | } 42 | 43 | { 44 | package = { 45 | name = "traversable"; 46 | modules = [ 47 | "import Data.Foldable" 48 | "import Data.Traversable" 49 | ]; 50 | }; 51 | } 52 | 53 | { 54 | group = { 55 | name = "generalize"; 56 | imports = [ 57 | "package monad" 58 | "package traversable" 59 | ]; 60 | rules = [ 61 | { 62 | warn = { 63 | lhs = "forM"; 64 | rhs = "for"; 65 | }; 66 | } 67 | { 68 | warn = { 69 | lhs = "forM_"; 70 | rhs = "for_"; 71 | }; 72 | } 73 | { 74 | warn = { 75 | lhs = "map"; 76 | rhs = "fmap"; 77 | }; 78 | } 79 | { 80 | warn = { 81 | lhs = "mapM"; 82 | rhs = "traverse"; 83 | }; 84 | } 85 | { 86 | warn = { 87 | lhs = "mapM_"; 88 | rhs = "traverse_"; 89 | }; 90 | } 91 | { 92 | warn = { 93 | lhs = "return"; 94 | rhs = "pure"; 95 | }; 96 | } 97 | { 98 | warn = { 99 | lhs = "sequence"; 100 | rhs = "sequenceA"; 101 | }; 102 | } 103 | { 104 | warn = { 105 | lhs = "sequence_"; 106 | rhs = "sequenceA_"; 107 | }; 108 | } 109 | ]; 110 | }; 111 | } 112 | 113 | { 114 | group = { 115 | name = "generalize"; 116 | imports = ["package traversable"]; 117 | rules = [ 118 | { 119 | hint = { 120 | lhs = "maybe (pure ())"; 121 | rhs = "traverse_"; 122 | note = "IncreasesLaziness"; 123 | }; 124 | } 125 | { 126 | warn = { 127 | lhs = "mappend"; 128 | rhs = "(<>)"; 129 | }; 130 | } 131 | { 132 | warn = { 133 | lhs = "(++)"; 134 | rhs = "(<>)"; 135 | }; 136 | } 137 | ]; 138 | }; 139 | } 140 | ]; 141 | } 142 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | {"actions":{"permissions":{"workflow":{"can_approve_pull_request_reviews":true}}},"branches":[{"name":"main","protection":{"allow_force_pushes":false,"enforce_admins":true,"required_linear_history":false,"required_pull_request_reviews":null,"required_status_checks":{"contexts":["All Garnix checks","check-bounds","check-licenses","build (8.10.1, macos-13)","build (--prefer-oldest, 8.10.1, macos-13)","build (9.0.1, macos-13)","build (--prefer-oldest, 9.0.1, macos-13)","build (9.2.1, macos-13)","build (--prefer-oldest, 9.2.1, macos-13)","build (9.4.1, macos-13)","build (--prefer-oldest, 9.4.1, macos-13)","build (9.6.1, macos-13)","build (--prefer-oldest, 9.6.1, macos-13)","build (9.8.1, macos-13)","build (--prefer-oldest, 9.8.1, macos-13)","build (9.10.1, macos-13)","build (--prefer-oldest, 9.10.1, macos-13)","build (8.10.1, macos-14)","build (--prefer-oldest, 8.10.1, macos-14)","build (9.0.1, macos-14)","build (--prefer-oldest, 9.0.1, macos-14)","build (9.2.1, macos-14)","build (--prefer-oldest, 9.2.1, macos-14)","build (9.4.1, macos-14)","build (--prefer-oldest, 9.4.1, macos-14)","build (9.6.1, macos-14)","build (--prefer-oldest, 9.6.1, macos-14)","build (9.8.1, macos-14)","build (--prefer-oldest, 9.8.1, macos-14)","build (9.10.1, macos-14)","build (--prefer-oldest, 9.10.1, macos-14)","build (8.10.1, ubuntu-24.04)","build (--prefer-oldest, 8.10.1, ubuntu-24.04)","build (9.0.1, ubuntu-24.04)","build (--prefer-oldest, 9.0.1, ubuntu-24.04)","build (9.2.1, ubuntu-24.04)","build (--prefer-oldest, 9.2.1, ubuntu-24.04)","build (9.4.1, ubuntu-24.04)","build (--prefer-oldest, 9.4.1, ubuntu-24.04)","build (9.6.1, ubuntu-24.04)","build (--prefer-oldest, 9.6.1, ubuntu-24.04)","build (9.8.1, ubuntu-24.04)","build (--prefer-oldest, 9.8.1, ubuntu-24.04)","build (9.10.1, ubuntu-24.04)","build (--prefer-oldest, 9.10.1, ubuntu-24.04)","build (8.10.1, windows-2022)","build (--prefer-oldest, 8.10.1, windows-2022)","build (9.0.1, windows-2022)","build (--prefer-oldest, 9.0.1, windows-2022)","build (9.2.1, windows-2022)","build (--prefer-oldest, 9.2.1, windows-2022)","build (9.4.1, windows-2022)","build (--prefer-oldest, 9.4.1, windows-2022)","build (9.6.1, windows-2022)","build (--prefer-oldest, 9.6.1, windows-2022)","build (9.8.1, windows-2022)","build (--prefer-oldest, 9.8.1, windows-2022)","build (9.10.1, windows-2022)","build (--prefer-oldest, 9.10.1, windows-2022)"],"strict":false},"restrictions":null}}],"labels":[{"color":"","description":"Created automatically by some service or process","name":"automated"},{"color":"#d73a4a","description":"Something isn’t working","name":"bug"},{"color":"#333333","description":"Updates or other changes to dependencies","name":"dependencies"},{"color":"#0075ca","description":"Improvements or additions to documentation","name":"documentation"},{"color":"#a2eeef","description":"New feature or request","name":"enhancement"},{"color":"#7057ff","description":"Good for newcomers","name":"good first issue"},{"color":"#000000","description":"Issues you want contributors to help with.","name":"hacktoberfest"},{"color":"#ff7518","description":"Indicates acceptance for Hacktoberfest criteria, even if not merged yet.","name":"hacktoberfest-accepted"},{"color":"#008672","description":"Extra attention is needed","name":"help wanted"},{"color":"#333333","description":"Unaccepted contributions that haven’t been closed for some reason.","name":"invalid"},{"color":"#d876e3","description":"Further information is requested","name":"question"},{"color":"#ffc0cb","description":"Topic created in bad faith. Services like Hacktoberfest use this to identify bad actors.","name":"spam"},{"color":"#d4af37","description":"Work prioritized by a sponsor","name":"sponsored"}],"repository":{"allow_merge_commit":true,"allow_rebase_merge":false,"allow_squash_merge":false,"default_branch":"main","delete_branch_on_merge":true,"description":"Automatically generate dual constructions","enable_automated_security_fixes":true,"enable_vulnerability_alerts":true,"has_downloads":false,"has_issues":true,"has_projects":true,"has_wiki":true,"merge_commit_message":"PR_BODY","merge_commit_title":"PR_TITLE","name":"dualizer","private":false,"topics":"hacktoberfest"}} -------------------------------------------------------------------------------- /dualizer/dualizer.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | 3 | name: dualizer 4 | version: 0.2.0.0 5 | synopsis: Automatically generate dual constructions 6 | description: A library for defining duals automatically, as well as labeling duals in existing packages. 7 | author: Greg Pfeil 8 | maintainer: Greg Pfeil 9 | copyright: 2017-2024 Greg Pfeil 10 | homepage: https://github.com/sellout/dualizer#readme 11 | bug-reports: https://github.com/sellout/dualizer/issues 12 | category: Categories 13 | build-type: Custom 14 | license: AGPL-3.0-or-later 15 | license-files: 16 | LICENSE 17 | extra-doc-files: 18 | CHANGELOG.md 19 | README.md 20 | docs/*.md 21 | tested-with: 22 | GHC == { 23 | 8.10.1, 8.10.7, 24 | 9.0.1, 9.0.2, 25 | 9.2.1, 9.2.5, 26 | 9.4.1, 9.4.5, 27 | 9.6.1, 9.6.3, 9.6.6, 28 | 9.8.1, 29 | 9.10.1 30 | } 31 | 32 | source-repository head 33 | type: git 34 | location: https://github.com/sellout/dualizer 35 | 36 | -- This mimics the GHC2024 extension 37 | -- (https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/control.html?highlight=doandifthenelse#extension-GHC2024), 38 | -- but supporting compilers back to GHC 7.10. If the oldest supported compiler 39 | -- is GHC 9.10, then this stanza can be removed and `import: GHC2024` can be 40 | -- replaced by `default-language: GHC2024`. If the oldest supported compiler is 41 | -- GHC 9.2, then this can be simplified by setting `default-language: GHC2021` 42 | -- and only including the extensions added by GHC2024. 43 | common GHC2024 44 | default-language: Haskell2010 45 | default-extensions: 46 | BangPatterns 47 | BinaryLiterals 48 | ConstraintKinds 49 | DataKinds 50 | DeriveDataTypeable 51 | DeriveGeneric 52 | DeriveLift 53 | DeriveTraversable 54 | DerivingStrategies 55 | DisambiguateRecordFields 56 | DoAndIfThenElse 57 | EmptyCase 58 | ExistentialQuantification 59 | FlexibleContexts 60 | FlexibleInstances 61 | GADTs 62 | GeneralizedNewtypeDeriving 63 | HexFloatLiterals 64 | ImportQualifiedPost 65 | InstanceSigs 66 | LambdaCase 67 | MagicHash 68 | MonadComprehensions 69 | MonomorphismRestriction 70 | MultiParamTypeClasses 71 | NamedFieldPuns 72 | NamedWildCards 73 | NumericUnderscores 74 | PolyKinds 75 | PostfixOperators 76 | RankNTypes 77 | RoleAnnotations 78 | ScopedTypeVariables 79 | StandaloneDeriving 80 | StandaloneKindSignatures 81 | TupleSections 82 | TypeApplications 83 | TypeOperators 84 | UnicodeSyntax 85 | 86 | flag noisy-deprecations 87 | description: 88 | Prior to GHC 9.10, the `DEPRECATED` pragma can’t distinguish between terms 89 | and types. Consenquently, you can get spurious warnings when there’s a name 90 | collision and the name in the other namespace is deprecated. Or you can 91 | choose to not get those warnings, at the risk of not being warned when 92 | there’s a name collision and the namespace you’re referencing is the one 93 | that’s deprecated. 94 | 95 | common defaults 96 | import: GHC2024 97 | build-depends: 98 | base ^>= {4.14.0, 4.15.0, 4.16.0, 4.17.0, 4.18.0, 4.19.0, 4.20.0}, 99 | ghc-options: 100 | -Weverything 101 | -- This one just reports unfixable things, AFAICT. 102 | -Wno-all-missed-specialisations 103 | -- Type inference good. 104 | -Wno-missing-local-signatures 105 | -- Warns even when `Unsafe` is explicit, not inferred. See 106 | -- https://gitlab.haskell.org/ghc/ghc/-/issues/16689 107 | -Wno-unsafe 108 | -- remove if the oldest supported version is GHC 9.2.1+ 109 | if impl(ghc >= 9.2.1) 110 | ghc-options: 111 | -Wno-missing-kind-signatures 112 | if impl(ghc >= 9.8.1) 113 | ghc-options: 114 | -- remove if the oldest supported version is GHC 9.2.1+ 115 | -Wno-missing-poly-kind-signatures 116 | -- Inference good. 117 | -Wno-missing-role-annotations 118 | default-extensions: 119 | BlockArguments 120 | DefaultSignatures 121 | ExplicitNamespaces 122 | FunctionalDependencies 123 | LiberalTypeSynonyms 124 | -- replace with `LexicalNegation` if the oldest supported version is GHC 9.0.1+ 125 | NegativeLiterals 126 | PackageImports 127 | ParallelListComp 128 | -- QualifiedDo - uncomment if the oldest supported version is GHC 9.0.1+ 129 | RecursiveDo 130 | -- RequiredTypeArguments - uncomment if the oldest supported version is GHC 9.10.1+ 131 | StrictData 132 | TemplateHaskellQuotes 133 | TransformListComp 134 | NoGeneralizedNewtypeDeriving 135 | NoImplicitPrelude 136 | NoMonomorphismRestriction 137 | NoPatternGuards 138 | NoStarIsType 139 | NoTypeApplications 140 | if flag(noisy-deprecations) 141 | cpp-options: -DSELLOUT_NOISY_DEPRECATIONS 142 | 143 | custom-setup 144 | setup-depends: 145 | -- TODO: Remove `Cabal` dep once haskell/cabal#3751 is fixed. 146 | Cabal ^>= {3.0.0, 3.2.0, 3.4.0, 3.6.0, 3.8.0, 3.10.0, 3.12.0}, 147 | base ^>= {4.8.2, 4.9.0, 4.10.0, 4.11.0, 4.12.0, 4.13.0, 4.14.0, 4.15.0, 4.16.0, 4.17.0, 4.18.0, 4.19.0, 4.20.0}, 148 | cabal-doctest ^>= {1.0.0}, 149 | 150 | library 151 | import: defaults 152 | hs-source-dirs: src 153 | build-depends: 154 | containers ^>= {0.6.2, 0.7}, 155 | lens ^>= {4.19, 5, 5.1, 5.2, 5.3}, 156 | template-haskell ^>= {2.16.0, 2.17.0, 2.18.0, 2.19.0, 2.20.0, 2.21.0, 2.22.0}, 157 | transformers ^>= {0.5.6, 0.6.1}, 158 | exposed-modules: 159 | Categorical.Dual 160 | Categorical.Dual.Base 161 | Categorical.Dual.Example 162 | Categorical.Dual.Lens 163 | Categorical.Dual.Prelude 164 | 165 | test-suite doctests 166 | import: defaults 167 | type: exitcode-stdio-1.0 168 | hs-source-dirs: tests 169 | main-is: doctests.hs 170 | build-depends: 171 | doctest ^>= {0.16.3, 0.18.1, 0.20.1, 0.21.1, 0.22.2}, 172 | dualizer, 173 | -- TODO: The sections below here are necessary because we don’t have control 174 | -- over the generated `Build_doctests.hs` file. So we have to silence 175 | -- all of its warnings one way or another. 176 | ghc-options: 177 | -Wno-missing-deriving-strategies 178 | -Wno-missing-export-lists 179 | -Wno-missing-import-lists 180 | -Wno-safe 181 | -- `doctest` requires the package containing the doctests as a dependency 182 | -- to ensure it gets built before this test-suite, even though the package 183 | -- appears to be unused. 184 | -Wno-unused-packages 185 | default-extensions: 186 | -- Since we can’t add `{-# LANGUAGE Safe -#}` to the generated 187 | -- “Build_doctests.hs”, we set it here, and that means it has to match 188 | -- doctests.hs, which is `Unsafe`. 189 | Unsafe 190 | -------------------------------------------------------------------------------- /dualizer/src/Categorical/Dual/Example.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE GADTs #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | {-# LANGUAGE TypeFamilies #-} 5 | {-# LANGUAGE UndecidableInstances #-} 6 | {-# LANGUAGE Unsafe #-} 7 | -- to allow hlint annotations 8 | {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} 9 | 10 | {- ORMOLU_DISABLE -} 11 | {- because it can’t handle CPP within a declaration -} 12 | -- | This should be tests, but if you look for the source of this module, 13 | -- you’ll see how to use the package. 14 | module Categorical.Dual.Example 15 | ( Coapplicative (..), 16 | Comonad (..), 17 | Distributive (..), 18 | consume, 19 | Algebra, 20 | Coalgebra, 21 | GAlgebra, 22 | GCoalgebra, 23 | ElgotAlgebra, 24 | ElgotCoalgebra, 25 | NewEither (..), 26 | NewTuple (..), 27 | NewEither' (..), 28 | NewTuple' (..), 29 | TestA, 30 | DualA, 31 | TestB, 32 | DualB, 33 | (>^>), 34 | (<^<), 35 | Mu (..), 36 | Nu (..), 37 | Fix (..), 38 | cata, 39 | ana, 40 | exampleDuals, 41 | testF, 42 | testT, 43 | testV, 44 | testV', 45 | testQ, 46 | ) 47 | where 48 | {- ORMOLU_ENABLE -} 49 | 50 | import Categorical.Dual 51 | ( dualType, 52 | exportDuals, 53 | importDuals, 54 | labelDual, 55 | labelSelfDual, 56 | labelSemiDual, 57 | makeDualClass, 58 | makeDualDec, 59 | ) 60 | import Categorical.Dual.Base (baseDuals) 61 | import Categorical.Dual.Lens (lensDuals) 62 | import safe Control.Applicative (Applicative, pure) 63 | import safe Control.Arrow ((>>>)) 64 | import safe Control.Category ((.)) 65 | import safe Control.Monad (Monad, (=<<), (>>=)) 66 | import safe Data.Bool (Bool) 67 | import safe Data.Char (Char, ord) 68 | import safe Data.Either (Either (Right)) 69 | import safe Data.Foldable (Foldable) 70 | import safe Data.Function (($)) 71 | import safe Data.Functor (Functor, fmap) 72 | import safe Data.Int (Int) 73 | import safe Data.Traversable (Traversable) 74 | import safe Data.Traversable qualified as T 75 | import safe Data.Void (Void) 76 | import safe Prelude (undefined) 77 | 78 | -- hlint sees some $() splices as redundant brackets 79 | {-# HLINT ignore "Redundant bracket" #-} 80 | 81 | importDuals baseDuals 82 | importDuals lensDuals 83 | 84 | -- TODO: this is much uglier than type families 85 | testF :: $(dualType =<< [t|Int -> Char|]) 86 | testF = ord 87 | 88 | testT :: $(dualType =<< [t|Either Int Char|]) 89 | testT = (7, 'a') 90 | 91 | testV :: $(dualType =<< [t|Either () Char|]) 92 | testV = undefined :: (Void, Char) 93 | 94 | testV' :: $(dualType =<< [t|((), Char)|]) 95 | testV' = Right 'a' :: Either Void Char 96 | 97 | testQ :: $(dualType =<< [t|forall a b. Either (a -> Int) Char -> (Bool, Either Char (Int -> b))|]) 98 | testQ = undefined :: Either Bool (Char, b -> Int) -> (Int -> a, Char) 99 | 100 | -- These are done as separate dual mappings (rather than something like `labelDualClass`) to ease a lot of the issues with not-quite dual constructions. 101 | -- labelDual ''Monad ''Comonad -- `fail` has no dual, so it’ll fail to convert if 102 | -- that method is hit, but not otherwise. 103 | -- labelDual 'pure 'extract -- these operations exist in different classes 104 | -- labelSemiDual 'return 'extract -- only maps one way, hopefully using some other 105 | -- mapping for the other direction, good for 106 | -- aliases, especially overconstrained ones. 107 | -- labelDual '(>>=) '(=>>) 108 | -- labelDual 'join 'duplicate -- the latter is a class method, but the former is a 109 | -- function 110 | 111 | -- | This should get mapped to the newly created class … right? 112 | makeDualClass ''Applicative "Coapplicative" [('pure, "extract")] 113 | 114 | -- | This should get mapped to the newly created class … right? 115 | makeDualClass ''Monad "Comonad" [('(>>=), "=>>")] 116 | 117 | -- FIXME: These semi-duals can be dangerous. It’s fine for overconstrained 118 | -- mappings (like `mapM` -> `traverse`), but in cases like `Foldable` and 119 | -- `Applicative`, you can’t reasonably round-trip. I.e., you can’t auto- 120 | -- dualize `Traversable` from `Distributive`, because the constraint will 121 | -- be too weak. 122 | labelSemiDual ''Foldable ''Functor 123 | 124 | makeDualClass 125 | ''Traversable 126 | "Distributive" 127 | [ ('T.traverse, "cotraverse"), 128 | ('T.sequenceA, "distribute") 129 | ] 130 | 131 | -- TODO: Doesn’t really belong here, but is the dual to `collect`. 132 | consume :: (Traversable g, Applicative f) => (g b -> a) -> g (f b) -> f a 133 | consume f = fmap f . T.sequenceA 134 | 135 | -- labelSemiDual 'return 'extract 136 | 137 | -- makeDualValue 'join 'duplicate 138 | -- makeDualValue '(=<<) '(<<=) -- aka, extend 139 | -- makeDualValue '(>=>) '(=>=) 140 | -- makeDualValue '(<=<) '(=<=) 141 | 142 | -- | Sometimes the doc is mapped to the original. 143 | makeDualDec [d|type Algebra f a = f a -> a|] "Coalgebra" 144 | 145 | -- | Other times, to the dual. 146 | makeDualDec [d|type GAlgebra w f a = f (w a) -> a|] "GCoalgebra" 147 | 148 | -- | I’m not sure why one or the other happens. 149 | makeDualDec [d|type ElgotAlgebra w f a = w (f a) -> a|] "ElgotCoalgebra" 150 | 151 | makeDualDec [d|newtype NewEither a b = NewEither (Either a b)|] "NewTuple" 152 | makeDualDec [d|data NewEither' a b = NewEither' (Either a b)|] "NewTuple'" 153 | 154 | -- FIXME: doesn’t terminate 155 | -- makeDualDec [d|data Mu f = Mu (forall a. Algebra f a -> a)|] "NotNu" 156 | 157 | -- | I wonder if 158 | makeDualDec [d|data family TestA a |] "DualA" 159 | 160 | -- | This always 161 | makeDualDec [d|type family TestB a |] "DualB" 162 | 163 | -- FIXME: Complains “‘TestC_0’ is not in scope at a reify” 164 | -- -- | Happens. 165 | -- makeDualDec 166 | -- [d| type family TestC a where 167 | -- TestC (Either b _c) = b 168 | -- TestC Int = Char |] 169 | -- "DualC" 170 | 171 | -- | These docs are going to end up on `<^<`, which is not what I’d expect. 172 | makeDualDec 173 | [d| 174 | (>^>) :: (a -> b) -> (b -> c) -> a -> c 175 | (>^>) = (>>>) 176 | |] 177 | "<^<" 178 | 179 | -- withDual [d| { infix 3 >^> } |] 180 | 181 | labelSelfDual '($) 182 | 183 | newtype Mu f = Mu (forall a. Algebra f a -> a) 184 | 185 | data Nu f where Nu :: Coalgebra f a -> a -> Nu f 186 | 187 | labelDual ''Mu ''Nu 188 | 189 | newtype Fix f = Fix {unfix :: f (Fix f)} 190 | 191 | labelSelfDual ''Fix -- not really 192 | labelDual 'Fix 'unfix 193 | 194 | -- | Interestingly, the documentation for a dualized function definition is 195 | -- added to the dual, not the explicitly-defined name. I don’t know why this 196 | -- behaves differently than the other cases. 197 | makeDualDec 198 | [d| 199 | cata :: (Functor f) => (f a -> a) -> Fix f -> a 200 | cata f = f . fmap (cata f) . unfix 201 | |] 202 | "ana" 203 | 204 | -- | Duals for this module. 205 | exportDuals "exampleDuals" 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # → dualizer ← 2 | 3 | [![built with garnix](https://img.shields.io/endpoint?url=https%3A%2F%2Fgarnix.io%2Fapi%2Fbadges%2Fsellout%2Fdualizer)](https://garnix.io/repo/sellout/dualizer) 4 | [![Join the chat at https://gitter.im/dualizer/Lobby](https://badges.gitter.im/dualizer/Lobby.svg)](https://gitter.im/dualizer/Lobby) 5 | 6 | Automatically generate dual constructions 7 | 8 | A library for defining duals automatically, as well as labeling duals in existing packages. 9 | 10 | ## usage 11 | 12 | See [the package README](./dualizer/README.md) for usage information. 13 | 14 | ## building 15 | 16 | Especially if you are unfamiliar with the haskell ecosystem, there is a Nix build (both with and without a flake). If you are unfamiliar with Nix, [Nix adjacent](...) can help you get things working in the shortest time and least effort possible. 17 | 18 | ### if you have `nix` installed 19 | 20 | `nix build` will build and test the project fully. 21 | 22 | `nix develop` will put you into an environment where the traditional build tooling works. If you also have `direnv` installed, then you should automatically be in that environment when you're in a directory in this project. 23 | 24 | ### traditional build 25 | 26 | This project is built with [Cabal](https://cabal.readthedocs.io/en/stable/index.html). Individual packages will work with older versions, but ./cabal.package requires Cabal 3.6+. 27 | 28 | ## development 29 | 30 | ### environment 31 | 32 | We recommend the following steps to make working in this repository as easy as possible. 33 | 34 | #### `direnv allow` 35 | 36 | This command ensures that any work you do within this repository is done within a consistent reproducible environment. That environment provides various debugging tools, etc. When you leave this directory, you will leave that environment behind, so it doesn’t impact anything else on your system. 37 | 38 | #### `git config --local include.path ../.config/git/config` 39 | 40 | This will apply our repository-specific Git configuration to `git` commands run against this repository. It’s lightweight (you should definitely look at it before applying this command) – it does things like telling `git blame` to ignore formatting-only commits. 41 | 42 | ### CI failures 43 | 44 | There are a few jobs that may fail during CI and indicate specific changes that need to be made to your PR. If you run into any failures other than those that are listed here, they likely have remedies that are specific to your changes. If you need help replicating or resolving them, or think that they represent general patterns like the ones listed below, inform the maintainers. They can help you resolve them and decide if they should be called out with generic resolution processes. 45 | 46 | #### CI / check-bounds (check if bounds have changed) 47 | 48 | A failure in the “check if bounds have changed” step indicates that the bounds on direct dependencies have changed. 49 | 50 | It currently means that the discovered bounds have been restricted, which is always a breaking change. Unfortunately, this is sometimes not due to anything in the PR, but it does indicate we’re no longer testing the versions we used to – the Cabal solver will sometimes start choosing different packages, depending on releases. Due to the behavior of the solver, the most likely ones to change are in the middle of the range. There are a few ways to address this problem: 51 | 52 | 1. Simply change the bounds as the output recommends, and make sure the PR bumps the major version number. If this change is already bumping the major version, this is probably the right choice to make. 53 | 2. Try to force Cabal to try the previous bounds. If you had manually changed the bounds because you needed some new feature, is it possible to conditionalize use of that feature so that we can also still use and test with older bounds? 54 | 3. Tell CI that you want to keep the bounds the same even though they’re not tested. You do this by adding the old bound to the `extraDependencyVersions` list in flake.nix. This should be done carefully, but one use case is where those bounds _are_ tested by the Nix builds, but not by GitHub. 55 | 56 | #### CI / check-licenses (check if licenses have changed) 57 | 58 | This means there has _possibly_ been some change in the licensing, but it’s not foolproof. This only captures the licensing for one particular Cabal solution, so other solutions may have different transitive dependencies or licenses. 59 | 60 | If there is a new license type in the list, it could affect how consumers of this can use our library. If the new license isn’t compatible with the existing set, then that’s a breaking change. If a package has changed its license, then we can alternatively restrict that package to versions that only use the previous license. Since making a license more restrictive introduces incompatibilities, this should only happen when they bump their major version, but there is no guarantee. In that case, this should just prevent us from extending the bounds, which is fine. But if it requires restricting bounds at the minor or revision level, then that’s still a breaking change on our side. Ideally we wouldn’t have to restrict that, but just make sure the consumer is informed about the license change and how to avoid it, but I don’t know how to convey that yet. 61 | 62 | If there is a new dependency that has appeared, that should already be reflected in a major version bump. However, not all libraries introduce a major version bump when they add a dependency, and supporting wider version ranges means we may pick up a new dependency without excluding solutions that don’t involve that dependency. 63 | 64 | It’s tempting to think that moving a dependency from the transitive list to the direct list doesn’t involve a version bump, but that’s not necessarily true. First, the transitive dependency must exist on all possible dependency solutions for that to be true. Then, it’s also possible for a new revision of a library to _remove_ dependencies, which means they will no longer appear in the transitive graph, invalidating our previous assumption. For this reason, we shouldn’t treat a move from transitive to direct as any different from a new dependency. 65 | 66 | #### check formatter 67 | 68 | There is some unformatted code (or perhaps some lint that needs addressing). If you use Nix, running `nix fmt` should automatically fix most of the formatting, and at least report additional lint that needs addressing. 69 | 70 | If you don’t use Nix, the CI log should contain some lines like 71 | 72 | ``` 73 | treefmt 0.6.1 74 | [INFO ] #alejandra: 1 files processed in 43.00ms 75 | [INFO ] #prettier: 7 files processed in 423.85ms 76 | [INFO ] #ormolu: 39 files processed in 1.60s 77 | [INFO ] #hlint: 39 files processed in 2.15s 78 | 0 files changed in 2s (found 66, matched 86, cache misses 86) 79 | ``` 80 | 81 | Those `INFO` lines indicate which formatters were run. Running those same ones individually should address the issues. You can also just indicate in your PR that you don’t use Nix, and a maintainer will happily fix the formatting for you. 82 | 83 | This implies a revision bump in any package that has been reformatted, as well as a revision bump in the repository. 84 | 85 | #### check project-manager-files 86 | 87 | Some files committed to the repository don’t match the ones that would be generated by Project Manager. This can happen either because you modified some of the Nix project configuration and forgot to regenerate the files, or because you edited generated files directly rather than editing the Nix project configuration. 88 | 89 | If you use Nix, running `project-manager switch` from a project `devShell` (or `nix run github:sellout/project-manager -- switch`) anywhere should fix this (although check to see if you lost intentional changes to generated files, and add them via the Nix project configuration instead). 90 | 91 | If you don’t use Nix, you will need to mention that in your PR so that one of the maintainers can resolve this for you. 92 | 93 | ## comparisons 94 | 95 | See [the package README](./dualizer/README.md) for comparisons with other similar projects. 96 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Automatically generate dual constructions"; 3 | 4 | nixConfig = { 5 | ## https://github.com/NixOS/rfcs/blob/master/rfcs/0045-deprecate-url-syntax.md 6 | extra-experimental-features = ["no-url-literals"]; 7 | extra-substituters = ["https://cache.garnix.io"]; 8 | extra-trusted-public-keys = [ 9 | "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" 10 | ]; 11 | ## Isolate the build. 12 | sandbox = "relaxed"; 13 | use-registries = false; 14 | }; 15 | 16 | ### This is a complicated flake. Here’s the rundown: 17 | ### 18 | ### overlays.default – includes all of the packages from cabal.project 19 | ### packages = { 20 | ### default = points to `packages.${defaultGhcVersion}` 21 | ### - = an individual package compiled for one 22 | ### GHC version 23 | ### -all = all of the packages in cabal.project compiled for one 24 | ### GHC version 25 | ### }; 26 | ### devShells = { 27 | ### default = points to `devShells.${defaultGhcVersion}` 28 | ### = a shell providing all of the dependencies for all 29 | ### packages in cabal.project compiled for one GHC version 30 | ### }; 31 | ### checks.format = verify that code matches Ormolu expectations 32 | outputs = { 33 | flake-utils, 34 | flaky, 35 | flaky-haskell, 36 | nixpkgs, 37 | self, 38 | systems, 39 | }: let 40 | pname = "dualizer"; 41 | 42 | supportedSystems = import systems; 43 | 44 | cabalPackages = pkgs: hpkgs: 45 | flaky-haskell.lib.cabalProject2nix 46 | ./cabal.project 47 | pkgs 48 | hpkgs 49 | (old: { 50 | configureFlags = old.configureFlags ++ ["--ghc-options=-Werror"]; 51 | }); 52 | in 53 | { 54 | schemas = { 55 | inherit 56 | (flaky.schemas) 57 | overlays 58 | homeConfigurations 59 | packages 60 | devShells 61 | projectConfigurations 62 | checks 63 | formatter 64 | ; 65 | }; 66 | 67 | # see these issues and discussions: 68 | # - NixOS/nixpkgs#16394 69 | # - NixOS/nixpkgs#25887 70 | # - NixOS/nixpkgs#26561 71 | # - https://discourse.nixos.org/t/nix-haskell-development-2020/6170 72 | overlays = { 73 | default = final: 74 | nixpkgs.lib.composeManyExtensions [ 75 | flaky.overlays.default 76 | (flaky-haskell.lib.overlayHaskellPackages 77 | (map self.lib.nixifyGhcVersion 78 | (self.lib.supportedGhcVersions final.system)) 79 | (final: prev: 80 | nixpkgs.lib.composeManyExtensions [ 81 | ## TODO: I think this overlay is only needed by formatters, 82 | ## devShells, etc., so it shouldn’t be included in the 83 | ## standard overlay. 84 | (flaky.overlays.haskellDependencies final prev) 85 | (self.overlays.haskell final prev) 86 | (self.overlays.haskellDependencies final prev) 87 | ])) 88 | ] 89 | final; 90 | 91 | haskell = flaky-haskell.lib.haskellOverlay cabalPackages; 92 | 93 | ## NB: Dependencies that are overridden because they are broken in 94 | ## Nixpkgs should be pushed upstream to Flaky. This is for 95 | ## dependencies that we override for reasons local to the project. 96 | haskellDependencies = final: prev: hfinal: hprev: {}; 97 | }; 98 | 99 | homeConfigurations = 100 | builtins.listToAttrs 101 | (builtins.map 102 | (flaky.lib.homeConfigurations.example self [ 103 | ({pkgs, ...}: { 104 | nixpkgs.overlays = [self.overlays.default]; 105 | home.packages = [ 106 | (pkgs.haskellPackages.ghcWithPackages (hpkgs: [hpkgs.${pname}])) 107 | ]; 108 | }) 109 | ]) 110 | supportedSystems); 111 | 112 | lib = { 113 | nixifyGhcVersion = version: 114 | "ghc" + nixpkgs.lib.replaceStrings ["."] [""] version; 115 | 116 | ## TODO: Extract this automatically from `pkgs.haskellPackages`. 117 | defaultGhcVersion = "9.6.6"; 118 | 119 | ## Test the oldest revision possible for each minor release. If it’s not 120 | ## available in nixpkgs, test the oldest available, then try an older 121 | ## one via GitHub workflow. Additionally, check any revisions that have 122 | ## explicit conditionalization. And check whatever version `pkgs.ghc` 123 | ## maps to in the nixpkgs we depend on. 124 | testedGhcVersions = system: [ 125 | self.lib.defaultGhcVersion 126 | "8.10.7" 127 | "9.0.2" 128 | "9.2.5" 129 | "9.4.5" 130 | "9.6.3" 131 | "9.8.1" 132 | "9.10.1" 133 | # "ghcHEAD" # doctest doesn’t work on current HEAD 134 | ]; 135 | 136 | ## The versions that are older than those supported by Nix that we 137 | ## prefer to test against. 138 | nonNixTestedGhcVersions = [ 139 | "8.10.1" 140 | "9.0.1" 141 | "9.2.1" 142 | "9.4.1" 143 | "9.6.1" 144 | ## since `cabal-plan-bounds` doesn’t work under Nix 145 | "9.8.1" 146 | "9.10.1" 147 | ]; 148 | 149 | ## However, provide packages in the default overlay for _every_ 150 | ## supported version. 151 | supportedGhcVersions = system: 152 | self.lib.testedGhcVersions system 153 | ++ [ 154 | "9.2.6" 155 | "9.2.7" 156 | "9.2.8" 157 | "9.4.6" 158 | "9.4.7" 159 | "9.4.8" 160 | "9.6.4" 161 | "9.6.5" 162 | "9.8.2" 163 | ]; 164 | 165 | githubSystems = [ 166 | "macos-13" # x86_64-darwin 167 | "macos-14" # aarch64-darwin 168 | "ubuntu-24.04" # x86_64-linux 169 | "windows-2022" 170 | ]; 171 | }; 172 | } 173 | // flake-utils.lib.eachSystem supportedSystems 174 | (system: let 175 | pkgs = nixpkgs.legacyPackages.${system}.appendOverlays [ 176 | ## NB: This uses `self.overlays.default` because packages need to be 177 | ## able to find other packages in this flake as dependencies. 178 | self.overlays.default 179 | ]; 180 | in { 181 | packages = 182 | { 183 | default = 184 | self.packages.${system}."${self.lib.nixifyGhcVersion self.lib.defaultGhcVersion}_all"; 185 | } 186 | // flaky-haskell.lib.mkPackages 187 | pkgs 188 | (map self.lib.nixifyGhcVersion (self.lib.supportedGhcVersions system)) 189 | cabalPackages; 190 | 191 | devShells = 192 | { 193 | default = 194 | self.devShells.${system}.${self.lib.nixifyGhcVersion self.lib.defaultGhcVersion}; 195 | } 196 | // self.projectConfigurations.${system}.devShells 197 | // flaky-haskell.lib.mkDevShells 198 | pkgs 199 | (map self.lib.nixifyGhcVersion (self.lib.supportedGhcVersions system)) 200 | cabalPackages 201 | (hpkgs: 202 | [self.projectConfigurations.${system}.packages.path] 203 | ## NB: Haskell Language Server no longer supports GHC <9.2, and 9.4 204 | ## has an issue with it on i686-linux. 205 | ## TODO: Remove the restriction on GHC 9.10 once 206 | ## https://github.com/NixOS/nixpkgs/commit/e87381d634cb1ddd2bd7e121c44fbc926a8c026a 207 | ## finds its way into 24.05. 208 | ++ nixpkgs.lib.optional 209 | ( 210 | ( 211 | if system == "i686-linux" 212 | then nixpkgs.lib.versionAtLeast hpkgs.ghc.version "9.4" 213 | else nixpkgs.lib.versionAtLeast hpkgs.ghc.version "9.2" 214 | ) 215 | && nixpkgs.lib.versionOlder hpkgs.ghc.version "9.10" 216 | ) 217 | hpkgs.haskell-language-server); 218 | 219 | projectConfigurations = 220 | flaky.lib.projectConfigurations.haskell {inherit pkgs self;}; 221 | 222 | checks = self.projectConfigurations.${system}.checks; 223 | formatter = self.projectConfigurations.${system}.formatter; 224 | }); 225 | 226 | inputs = { 227 | ## Flaky should generally be the source of truth for its inputs. 228 | flaky.url = "github:sellout/flaky"; 229 | 230 | flake-utils.follows = "flaky/flake-utils"; 231 | nixpkgs.follows = "flaky/nixpkgs"; 232 | systems.follows = "flaky/systems"; 233 | 234 | flaky-haskell = { 235 | inputs.flaky.follows = "flaky"; 236 | url = "github:sellout/flaky-haskell"; 237 | }; 238 | }; 239 | } 240 | -------------------------------------------------------------------------------- /dualizer/README.md: -------------------------------------------------------------------------------- 1 | # → dualizer ← 2 | 3 | [![Packaging status](https://repology.org/badge/tiny-repos/haskell:dualizer.svg)](https://repology.org/project/haskell:dualizer/versions) 4 | [![latest packaged versions](https://repology.org/badge/latest-versions/haskell:dualizer.svg)](https://repology.org/project/haskell:dualizer/versions) 5 | [![Join the chat at https://gitter.im/dualizer/Lobby](https://badges.gitter.im/dualizer/Lobby.svg)](https://gitter.im/dualizer/Lobby) 6 | 7 | **Delete half (minus ε) of your Haskell code!** 8 | 9 | ## usage 10 | 11 | Dualizer allows you to eliminate the dual of all your code. Rather than implementing, say, `Comonad` directly, you can define it in terms of its dual – `Monad`: 12 | 13 | ```haskell 14 | -- indicates that Functor is its own dual 15 | labelSelfDual ''Functor 16 | 17 | -- expands to: 18 | -- 19 | -- class Functor f => Coapplicative f where 20 | -- extract :: f a -> a -- the dual of pure 21 | makeDualClass ''Applicative "Coapplicative" [('pure, "extract")] 22 | 23 | -- expands to: 24 | -- 25 | -- class Coapplicative m => Comonad m where 26 | -- (=>>) :: m b -> (m b -> a) -> m a 27 | makeDualClass ''Monad "Comonad" [('(>>=) , "=>>")] 28 | ``` 29 | 30 | See [`Categorical.Dual.Example`](src/Categorical/Dual/Example.hs) for a bit more. 31 | 32 | ### The Template Haskell You Need to Know 33 | 34 | This library is written using Template Haskell, and while it tries to minimize the familiarity needed to use it (and accepting suggestions/PRs for reducing it further), some still leaks through. Here’s what you need to know. 35 | 36 | When naming an _existing_ type, prefix with `''` (e.g., `''Either`), and for an _existing_ value, prefix with `'` (e.g., `'fmap`). Names of things to be created are plain `String`s. 37 | 38 | To allow code to be reified into an AST that Template Haskell can work with, it uses a special “quasiquotation” syntax, opening with `[d|` and closing with `|]` that looks like `[d|your :: Code -> Here|]` when used. There are variants of this that use something other than `d` in the opening, but we don’t need them in this library. 39 | 40 | ### Defining Duals 41 | 42 | There are three approaches here to defining duals, and they are listed in order of preference. 43 | 44 | 1. define them simultaneously 45 | 2. define the dual of an existing thing 46 | 3. label two existing things as duals of each other 47 | 48 | Simultaneous definition is the only way to automatically define duals of expressions. If you define the dual of an existing value, you will only get its type, and you will still need to provide the expression. 49 | 50 | You can, however, still label existing values as duals of each other. 51 | 52 | #### Defining Duals Simultaneously 53 | 54 | ```haskell 55 | makeDualDec 56 | [d| cata :: Functor f => (f a -> a) -> Fix f -> a 57 | cata f = f . fmap (cata f) . unfix |] 58 | "ana" 59 | 60 | makeDualDec [d|type Algebra f a = f a -> a|] "Coalgebra" 61 | ``` 62 | 63 | This form can also be nested, allowing the definition of duals for type classes, etc. (NB: This can’t actually work this way). 64 | 65 | ```haskell 66 | makeDualDec [d| 67 | class Functor f => Applicative f where 68 | $$(makeDualDec [d|pure :: a -> f a|] "extract") 69 | |] "Coapplicative" 70 | 71 | makeDualDec [d| 72 | class Applicative f => Monad f where 73 | $$(makeDualDec [d|>>= :: f a -> (a -> f b) -> f b|] "=>>") 74 | fail :: f () 75 | |] "Comonad" 76 | ``` 77 | 78 | #### Defining the Dual of an Existing Thing 79 | 80 | If one side of the construct already exists, then you can assign the duals like 81 | 82 | ```haskell 83 | makeDualType 'cata "ana" 84 | makeDualType ''Algebra "Coalgebra" 85 | ``` 86 | 87 | ```haskell 88 | makeDualClass ''Applicative "Coapplicative" [('pure, "extract")] 89 | makeDualClass ''Monad "Comonad" [('(>>=) , "=>>")] 90 | ``` 91 | 92 | #### Labeling Existing Duals 93 | 94 | Labeling is especially useful when things are duals of themselves. 95 | 96 | ```haskell 97 | labelSelfDual ''Functor 98 | labelSelfDual 'fmap -- not implied by the former because: 99 | 100 | class Steppable t f | t -> f where 101 | project :: t -> f t 102 | embed :: f t -> t 103 | 104 | labelSelfDual ''Steppable 105 | labelDual 'project 'embed 106 | ``` 107 | 108 | Also, if there are things that are both equivalent to some other thing, you can label one as “semi-dual”, mapping in one direction but not the other. 109 | 110 | ```haskell 111 | labelDual 'pure 'extract 112 | 113 | -- `return` is overconstrained, so we let it dualize to `extract`, but `extract` 114 | -- will be converted to `pure` on any return trip. 115 | labelSemiDual 'return 'extract 116 | ``` 117 | 118 | ## versioning 119 | 120 | This project largely follows the [Haskell Package Versioning Policy](https://pvp.haskell.org/) (PVP), but is more strict in some ways. 121 | 122 | The version always has four components, `A.B.C.D`. The first three correspond to those required by PVP, while the fourth matches the “patch” component from [Semantic Versioning](https://semver.org/). 123 | 124 | Here is a breakdown of some of the constraints: 125 | 126 | ### sensitivity to additions to the API 127 | 128 | PVP recommends that clients follow [these import guidelines](https://wiki.haskell.org/Import_modules_properly) in order that they may be considered insensitive to additions to the API. However, this isn’t sufficient. We expect clients to follow these additional recommendations for API insensitivity 129 | 130 | If you don’t follow these recommendations (in addition to the ones made by PVP), you should ensure your dependencies don’t allow a range of `C` values. That is, your dependencies should look like 131 | 132 | ```cabal 133 | yaya >=1.2.3 && <1.2.4 134 | ``` 135 | 136 | rather than 137 | 138 | ```cabal 139 | yaya >=1.2.3 && <1.3 140 | ``` 141 | 142 | #### use package-qualified imports everywhere 143 | 144 | If your imports are [package-qualified](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/package_qualified_imports.html?highlight=packageimports#extension-PackageImports), then a dependency adding new modules can’t cause a conflict with modules you already import. 145 | 146 | #### avoid orphans 147 | 148 | Because of the transitivity of instances, orphans make you sensitive to your dependencies’ instances. If you have an orphan instance, you are sensitive to the APIs of the packages that define the class and the types of the instance. 149 | 150 | One way to minimize this sensitivity is to have a separate package (or packages) dedicated to any orphans you have. Those packages can be sensitive to their dependencies’ APIs, while the primary package remains insensitive, relying on the tighter ranges of the orphan packages to constrain the solver. 151 | 152 | ### transitively breaking changes (increments `A`) 153 | 154 | #### removing a type class instance 155 | 156 | Type class instances are imported transitively, and thus changing them can impact packages that only have your package as a transitive dependency. 157 | 158 | #### widening a dependency range with new major versions 159 | 160 | This is a consequence of instances being transitively imported. A new major version of a dependency can remove instances, and that can break downstream clients that unwittingly depended on those instances. 161 | 162 | A library _may_ declare that it always bumps the `A` component when it removes an instance (as this policy dictates). In that case, only `A` widenings need to induce `A` bumps. `B` widenings can be `D` bumps like other widenings, Alternatively, one may compare the APIs when widening a dependency range, and if no instances have been removed, make it a `D` bump. 163 | 164 | ### breaking changes (increments `B`) 165 | 166 | #### restricting an existing dependency’s version range in any way 167 | 168 | Consumers have to contend not only with our version bounds, but also with those of other libraries. It’s possible that some dependency overlapped in a very narrow way, and even just restricting a particular patch version of a dependency could make it impossible to find a dependency solution. 169 | 170 | #### restricting the license in any way 171 | 172 | Making a license more restrictive may prevent clients from being able to continue using the package. 173 | 174 | #### adding a dependency 175 | 176 | A new dependency may make it impossible to find a solution in the face of other packages dependency ranges. 177 | 178 | ### non-breaking changes (increments `C`) 179 | 180 | #### adding a module 181 | 182 | This is also what PVP recommends. However, unlike in PVP, this is because we recommend that package-qualified imports be used on all imports. 183 | 184 | ### other changes (increments `D`) 185 | 186 | #### widening a dependency range for non-major versions 187 | 188 | This is fairly uncommon, in the face of `^>=`-style ranges, but it can happen in a few situations. 189 | 190 | #### deprecation 191 | 192 | **NB**: This case is _weaker_ than PVP, which indicates that packages should bump their major version when adding `deprecation` pragmas. 193 | 194 | We disagree with this because packages shouldn’t be _publishing_ with `-Werror`. The intent of deprecation is to indicate that some API _will_ change. To make that signal a major change itself defeats the purpose. You want people to start seeing that warning as soon as possible. The major change occurs when you actually remove the old API. 195 | 196 | Yes, in development, `-Werror` is often (and should be) used. However, that just helps developers be aware of deprecations more immediately. They can always add `-Wwarn=deprecation` in some scope if they need to avoid updating it for the time being. 197 | 198 | ## licensing 199 | 200 | This package is licensed under [The GNU AGPL 3.0 or later](./LICENSE). If you need a license for usage that isn’t covered under the AGPL, please contact [Greg Pfeil](mailto:greg@technomadic.org?subject=licensing%20dualizer). 201 | 202 | You should review the [license report](docs/license-report.md) for details about dependency licenses. 203 | 204 | ## comparisons 205 | 206 | Other projects similar to this one, and how they differ. 207 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "bash-strict-mode": { 4 | "inputs": { 5 | "flake-utils": [ 6 | "flaky", 7 | "bash-strict-mode", 8 | "flaky", 9 | "flake-utils" 10 | ], 11 | "flaky": [ 12 | "flaky" 13 | ], 14 | "nixpkgs": [ 15 | "flaky", 16 | "bash-strict-mode", 17 | "flaky", 18 | "nixpkgs" 19 | ], 20 | "shellcheck-nix-attributes": "shellcheck-nix-attributes", 21 | "systems": [ 22 | "flaky", 23 | "bash-strict-mode", 24 | "flaky", 25 | "systems" 26 | ] 27 | }, 28 | "locked": { 29 | "lastModified": 1743406175, 30 | "narHash": "sha256-e4WAruZxiqayccvf/N1kk2ov81K4DdjRh9a9zVZ8c6w=", 31 | "owner": "sellout", 32 | "repo": "bash-strict-mode", 33 | "rev": "4c7ca465d189bfbc8d3366ad7f210a1c0afc77c7", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "sellout", 38 | "repo": "bash-strict-mode", 39 | "type": "github" 40 | } 41 | }, 42 | "flake-schemas": { 43 | "locked": { 44 | "lastModified": 1701189173, 45 | "narHash": "sha256-ieQZLjqfvOP+52WeMdkD/x5njEIAqjKuAc/jnmruaoc=", 46 | "owner": "sellout", 47 | "repo": "flake-schemas", 48 | "rev": "68727227a8f349fe0831f6389d9a91064637b70c", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "sellout", 53 | "ref": "patch-1", 54 | "repo": "flake-schemas", 55 | "type": "github" 56 | } 57 | }, 58 | "flake-utils": { 59 | "inputs": { 60 | "systems": [ 61 | "flaky", 62 | "systems" 63 | ] 64 | }, 65 | "locked": { 66 | "lastModified": 1731533236, 67 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 68 | "owner": "numtide", 69 | "repo": "flake-utils", 70 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "numtide", 75 | "repo": "flake-utils", 76 | "type": "github" 77 | } 78 | }, 79 | "flaky": { 80 | "inputs": { 81 | "bash-strict-mode": "bash-strict-mode", 82 | "flake-utils": "flake-utils", 83 | "garnix-systems": "garnix-systems", 84 | "home-manager": "home-manager", 85 | "nixpkgs": "nixpkgs", 86 | "project-manager": "project-manager", 87 | "systems": "systems" 88 | }, 89 | "locked": { 90 | "lastModified": 1744024854, 91 | "narHash": "sha256-pCho/B/adgvE4DCUUNu2rnycN2kvMqRc8rsrBhLifII=", 92 | "owner": "sellout", 93 | "repo": "flaky", 94 | "rev": "7abee0e17098b57088b2eaaaa91697e5a1a91b02", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "sellout", 99 | "repo": "flaky", 100 | "type": "github" 101 | } 102 | }, 103 | "flaky-haskell": { 104 | "inputs": { 105 | "flake-utils": [ 106 | "flaky-haskell", 107 | "flaky", 108 | "flake-utils" 109 | ], 110 | "flaky": [ 111 | "flaky" 112 | ], 113 | "nixpkgs": [ 114 | "flaky-haskell", 115 | "flaky", 116 | "nixpkgs" 117 | ], 118 | "systems": [ 119 | "flaky-haskell", 120 | "flaky", 121 | "systems" 122 | ] 123 | }, 124 | "locked": { 125 | "lastModified": 1742795464, 126 | "narHash": "sha256-l39wNbqKZbovTedTHFFgsN8d8MtBFkPkOpodSblVyUY=", 127 | "owner": "sellout", 128 | "repo": "flaky-haskell", 129 | "rev": "6564f8ba874cca2a12a8e43b67e1fa864d2555bb", 130 | "type": "github" 131 | }, 132 | "original": { 133 | "owner": "sellout", 134 | "repo": "flaky-haskell", 135 | "type": "github" 136 | } 137 | }, 138 | "garnix-systems": { 139 | "locked": { 140 | "lastModified": 1723225755, 141 | "narHash": "sha256-y17fVgb4LJheN4VDsHCmZRHrOJwKj8oUPvXk2FqUOnI=", 142 | "owner": "garnix-io", 143 | "repo": "nix-systems", 144 | "rev": "c2466c5c1d3591933b4389868902e2f47a47fadb", 145 | "type": "github" 146 | }, 147 | "original": { 148 | "owner": "garnix-io", 149 | "repo": "nix-systems", 150 | "type": "github" 151 | } 152 | }, 153 | "home-manager": { 154 | "inputs": { 155 | "nixpkgs": [ 156 | "flaky", 157 | "nixpkgs" 158 | ] 159 | }, 160 | "locked": { 161 | "lastModified": 1743808813, 162 | "narHash": "sha256-2lDQBOmlz9ggPxcS7/GvcVdzXMIiT+PpMao6FbLJSr0=", 163 | "owner": "nix-community", 164 | "repo": "home-manager", 165 | "rev": "a9f8b3db211b4609ddd83683f9db89796c7f6ac6", 166 | "type": "github" 167 | }, 168 | "original": { 169 | "owner": "nix-community", 170 | "ref": "release-24.11", 171 | "repo": "home-manager", 172 | "type": "github" 173 | } 174 | }, 175 | "nixpkgs": { 176 | "locked": { 177 | "lastModified": 1743987495, 178 | "narHash": "sha256-46T2vMZ4/AfCK0Y2OjlFzJPxmdpP8GtsuEqSSJv3oe4=", 179 | "owner": "NixOS", 180 | "repo": "nixpkgs", 181 | "rev": "db8f4fe18ce772a9c8f3adf321416981c8fe9371", 182 | "type": "github" 183 | }, 184 | "original": { 185 | "owner": "NixOS", 186 | "ref": "release-24.11", 187 | "repo": "nixpkgs", 188 | "type": "github" 189 | } 190 | }, 191 | "nixpkgs-22_11": { 192 | "locked": { 193 | "lastModified": 1688392541, 194 | "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", 195 | "owner": "NixOS", 196 | "repo": "nixpkgs", 197 | "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", 198 | "type": "github" 199 | }, 200 | "original": { 201 | "owner": "NixOS", 202 | "ref": "release-22.11", 203 | "repo": "nixpkgs", 204 | "type": "github" 205 | } 206 | }, 207 | "nixpkgs-23_05": { 208 | "locked": { 209 | "lastModified": 1705957679, 210 | "narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=", 211 | "owner": "NixOS", 212 | "repo": "nixpkgs", 213 | "rev": "9a333eaa80901efe01df07eade2c16d183761fa3", 214 | "type": "github" 215 | }, 216 | "original": { 217 | "owner": "NixOS", 218 | "ref": "release-23.05", 219 | "repo": "nixpkgs", 220 | "type": "github" 221 | } 222 | }, 223 | "nixpkgs-23_11": { 224 | "locked": { 225 | "lastModified": 1720535198, 226 | "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=", 227 | "owner": "NixOS", 228 | "repo": "nixpkgs", 229 | "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5", 230 | "type": "github" 231 | }, 232 | "original": { 233 | "owner": "NixOS", 234 | "ref": "release-23.11", 235 | "repo": "nixpkgs", 236 | "type": "github" 237 | } 238 | }, 239 | "nixpkgs-24_05": { 240 | "locked": { 241 | "lastModified": 1735651292, 242 | "narHash": "sha256-YLbzcBtYo1/FEzFsB3AnM16qFc6fWPMIoOuSoDwvg9g=", 243 | "owner": "NixOS", 244 | "repo": "nixpkgs", 245 | "rev": "0da3c44a9460a26d2025ec3ed2ec60a895eb1114", 246 | "type": "github" 247 | }, 248 | "original": { 249 | "owner": "NixOS", 250 | "ref": "release-24.05", 251 | "repo": "nixpkgs", 252 | "type": "github" 253 | } 254 | }, 255 | "nixpkgs-unstable": { 256 | "locked": { 257 | "lastModified": 1743259260, 258 | "narHash": "sha256-ArWLUgRm1tKHiqlhnymyVqi5kLNCK5ghvm06mfCl4QY=", 259 | "owner": "NixOS", 260 | "repo": "nixpkgs", 261 | "rev": "eb0e0f21f15c559d2ac7633dc81d079d1caf5f5f", 262 | "type": "github" 263 | }, 264 | "original": { 265 | "owner": "NixOS", 266 | "ref": "nixpkgs-unstable", 267 | "repo": "nixpkgs", 268 | "type": "github" 269 | } 270 | }, 271 | "project-manager": { 272 | "inputs": { 273 | "flake-schemas": "flake-schemas", 274 | "flake-utils": [ 275 | "flaky", 276 | "project-manager", 277 | "flaky", 278 | "flake-utils" 279 | ], 280 | "flaky": [ 281 | "flaky" 282 | ], 283 | "nixpkgs": [ 284 | "flaky", 285 | "project-manager", 286 | "flaky", 287 | "nixpkgs" 288 | ], 289 | "nixpkgs-22_11": "nixpkgs-22_11", 290 | "nixpkgs-23_05": "nixpkgs-23_05", 291 | "nixpkgs-23_11": "nixpkgs-23_11", 292 | "nixpkgs-24_05": "nixpkgs-24_05", 293 | "nixpkgs-unstable": "nixpkgs-unstable", 294 | "systems": [ 295 | "flaky", 296 | "project-manager", 297 | "flaky", 298 | "systems" 299 | ], 300 | "treefmt-nix": "treefmt-nix" 301 | }, 302 | "locked": { 303 | "lastModified": 1743405078, 304 | "narHash": "sha256-DRZ4PyEMeZX75Gqn84o59K4TX0bsDqCdi+NB4zudDow=", 305 | "owner": "sellout", 306 | "repo": "project-manager", 307 | "rev": "81d1fc5cdb624feb8e712fcf2e0377f83fb47a7e", 308 | "type": "github" 309 | }, 310 | "original": { 311 | "owner": "sellout", 312 | "repo": "project-manager", 313 | "type": "github" 314 | } 315 | }, 316 | "root": { 317 | "inputs": { 318 | "flake-utils": [ 319 | "flaky", 320 | "flake-utils" 321 | ], 322 | "flaky": "flaky", 323 | "flaky-haskell": "flaky-haskell", 324 | "nixpkgs": [ 325 | "flaky", 326 | "nixpkgs" 327 | ], 328 | "systems": [ 329 | "flaky", 330 | "systems" 331 | ] 332 | } 333 | }, 334 | "shellcheck-nix-attributes": { 335 | "flake": false, 336 | "locked": { 337 | "lastModified": 1586929030, 338 | "narHash": "sha256-a0WyWaz+nMYFWI43Ip9EUnPuBW0O4EIiTzYZKGqNjss=", 339 | "owner": "Fuuzetsu", 340 | "repo": "shellcheck-nix-attributes", 341 | "rev": "51b49d5fe65ece69eb5e2003396bf096083ec281", 342 | "type": "github" 343 | }, 344 | "original": { 345 | "owner": "Fuuzetsu", 346 | "repo": "shellcheck-nix-attributes", 347 | "type": "github" 348 | } 349 | }, 350 | "systems": { 351 | "locked": { 352 | "lastModified": 1726558681, 353 | "narHash": "sha256-vta8mxLa5XxpHfb9HCiavFqBrX2xJDvZiKHZOy4kvlA=", 354 | "owner": "sellout", 355 | "repo": "nix-systems", 356 | "rev": "aa9520a9a0f92098d3576c3b4eafcb32c13d6800", 357 | "type": "github" 358 | }, 359 | "original": { 360 | "owner": "sellout", 361 | "repo": "nix-systems", 362 | "type": "github" 363 | } 364 | }, 365 | "treefmt-nix": { 366 | "inputs": { 367 | "nixpkgs": [ 368 | "flaky", 369 | "project-manager", 370 | "nixpkgs" 371 | ] 372 | }, 373 | "locked": { 374 | "lastModified": 1743081648, 375 | "narHash": "sha256-WRAylyYptt6OX5eCEBWyTwOEqEtD6zt33rlUkr6u3cE=", 376 | "owner": "numtide", 377 | "repo": "treefmt-nix", 378 | "rev": "29a3d7b768c70addce17af0869f6e2bd8f5be4b7", 379 | "type": "github" 380 | }, 381 | "original": { 382 | "owner": "numtide", 383 | "repo": "treefmt-nix", 384 | "type": "github" 385 | } 386 | } 387 | }, 388 | "root": "root", 389 | "version": 7 390 | } 391 | -------------------------------------------------------------------------------- /dualizer/docs/license-report.md: -------------------------------------------------------------------------------- 1 | **NB**: This captures the licenses associated with a particular set of dependency versions. If your own build solves differently, it’s possible that the licenses may have changed, or even that the set of dependencies itself is different. Please make sure you run [`cabal-plan license-report`](https://hackage.haskell.org/package/cabal-plan) on your own components rather than assuming this is authoritative. 2 | 3 | # Dependency License Report 4 | 5 | Bold-faced **`package-name`**s denote standard libraries bundled with `ghc-9.10.1`. 6 | 7 | ## Direct dependencies of `dualizer:lib:dualizer` 8 | 9 | | Name | Version | [SPDX](https://spdx.org/licenses/) License Id | Description | Also depended upon by | 10 | | --- | --- | --- | --- | --- | 11 | | **`base`** | [`4.20.0.0`](http://hackage.haskell.org/package/base-4.20.0.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/base-4.20.0.0/src/LICENSE) | Core data structures and operations | *(core library)* | 12 | | **`containers`** | [`0.7`](http://hackage.haskell.org/package/containers-0.7) | [`BSD-3-Clause`](http://hackage.haskell.org/package/containers-0.7/src/LICENSE) | Assorted concrete container types | `adjunctions`, `bifunctors`, `binary`, `comonad`, `free`, `hashable`, `indexed-traversable`, `invariant`, `kan-extensions`, `lens`, `parallel`, `semigroupoids`, `th-abstraction` | 13 | | `lens` | [`5.3.4`](http://hackage.haskell.org/package/lens-5.3.4) | [`BSD-2-Clause`](http://hackage.haskell.org/package/lens-5.3.4/src/LICENSE) | Lenses, Folds and Traversals | | 14 | | **`template-haskell`** | [`2.22.0.0`](http://hackage.haskell.org/package/template-haskell-2.22.0.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/template-haskell-2.22.0.0/src/LICENSE) | Support library for Template Haskell | `OneTuple`, `bifunctors`, `bytestring`, `containers`, `exceptions`, `filepath`, `free`, `invariant`, `lens`, `os-string`, `primitive`, `reflection`, `semigroupoids`, `tagged`, `text`, `th-abstraction`, `unordered-containers` | 15 | | **`transformers`** | [`0.6.1.1`](http://hackage.haskell.org/package/transformers-0.6.1.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/transformers-0.6.1.1/src/LICENSE) | Concrete functor and monad transformers | `StateVar`, `adjunctions`, `bifunctors`, `comonad`, `contravariant`, `distributive`, `exceptions`, `free`, `indexed-traversable`, `invariant`, `kan-extensions`, `lens`, `mtl`, `primitive`, `profunctors`, `semigroupoids`, `strict`, `tagged`, `transformers-base`, `transformers-compat` | 16 | 17 | ## Indirect transitive dependencies 18 | 19 | | Name | Version | [SPDX](https://spdx.org/licenses/) License Id | Description | Depended upon by | 20 | | --- | --- | --- | --- | --- | 21 | | `OneTuple` | [`0.4.2`](http://hackage.haskell.org/package/OneTuple-0.4.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/OneTuple-0.4.2/src/LICENSE) | Singleton Tuple | `indexed-traversable-instances` | 22 | | `StateVar` | [`1.2.2`](http://hackage.haskell.org/package/StateVar-1.2.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/StateVar-1.2.2/src/LICENSE) | State variables | `contravariant`, `invariant` | 23 | | `adjunctions` | [`4.4.3`](http://hackage.haskell.org/package/adjunctions-4.4.3) | [`BSD-2-Clause`](http://hackage.haskell.org/package/adjunctions-4.4.3/src/LICENSE) | Adjunctions and representable functors | `kan-extensions` | 24 | | **`array`** | [`0.5.7.0`](http://hackage.haskell.org/package/array-0.5.7.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/array-0.5.7.0/src/LICENSE) | Mutable and immutable arrays | `adjunctions`, `binary`, `containers`, `deepseq`, `indexed-traversable`, `invariant`, `kan-extensions`, `lens`, `parallel`, `stm`, `text` | 25 | | `assoc` | [`1.1.1`](http://hackage.haskell.org/package/assoc-1.1.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/assoc-1.1.1/src/LICENSE) | swap and assoc: Symmetric and Semigroupy Bifunctors | `bifunctors`, `lens`, `strict`, `these` | 26 | | `base-orphans` | [`0.9.3`](http://hackage.haskell.org/package/base-orphans-0.9.3) | [`MIT`](http://hackage.haskell.org/package/base-orphans-0.9.3/src/LICENSE) | Backwards-compatible orphan instances for base | `distributive`, `lens`, `profunctors`, `semigroupoids`, `transformers-base` | 27 | | `bifunctors` | [`5.6.2`](http://hackage.haskell.org/package/bifunctors-5.6.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/bifunctors-5.6.2/src/LICENSE) | Bifunctors | `invariant`, `lens`, `profunctors`, `semigroupoids` | 28 | | **`binary`** | [`0.8.9.2`](http://hackage.haskell.org/package/binary-0.8.9.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/binary-0.8.9.2/src/LICENSE) | Binary serialisation for Haskell values using lazy ByteStrings | `strict`, `text`, `these` | 29 | | **`bytestring`** | [`0.12.1.0`](http://hackage.haskell.org/package/bytestring-0.12.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/bytestring-0.12.1.0/src/LICENSE) | Fast, compact, strict and lazy byte strings with a list interface | `binary`, `filepath`, `hashable`, `lens`, `os-string`, `strict`, `text` | 30 | | `call-stack` | [`0.4.0`](http://hackage.haskell.org/package/call-stack-0.4.0) | [`MIT`](http://hackage.haskell.org/package/call-stack-0.4.0/src/LICENSE) | Use GHC call-stacks in a backward compatible way | `lens` | 31 | | `comonad` | [`5.0.9`](http://hackage.haskell.org/package/comonad-5.0.9) | [`BSD-3-Clause`](http://hackage.haskell.org/package/comonad-5.0.9/src/LICENSE) | Comonads | `adjunctions`, `bifunctors`, `free`, `invariant`, `kan-extensions`, `lens`, `profunctors`, `semigroupoids` | 32 | | `contravariant` | [`1.5.5`](http://hackage.haskell.org/package/contravariant-1.5.5) | [`BSD-3-Clause`](http://hackage.haskell.org/package/contravariant-1.5.5/src/LICENSE) | Contravariant functors | `adjunctions`, `invariant`, `kan-extensions`, `lens`, `profunctors`, `semigroupoids` | 33 | | **`deepseq`** | [`1.5.0.0`](http://hackage.haskell.org/package/deepseq-1.5.0.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/deepseq-1.5.0.0/src/LICENSE) | Deep evaluation of data structures | `bytestring`, `containers`, `filepath`, `hashable`, `os-string`, `parallel`, `pretty`, `primitive`, `strict`, `tagged`, `text`, `these`, `unordered-containers`, `vector` | 34 | | `distributive` | [`0.6.2.1`](http://hackage.haskell.org/package/distributive-0.6.2.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/distributive-0.6.2.1/src/LICENSE) | Distributive functors -- Dual to Traversable | `adjunctions`, `comonad`, `free`, `kan-extensions`, `lens`, `profunctors`, `semigroupoids` | 35 | | **`exceptions`** | [`0.10.7`](http://hackage.haskell.org/package/exceptions-0.10.7) | [`BSD-3-Clause`](http://hackage.haskell.org/package/exceptions-0.10.7/src/LICENSE) | Extensible optionally-pure exceptions | `filepath`, `free`, `lens`, `os-string` | 36 | | **`filepath`** | [`1.5.2.0`](http://hackage.haskell.org/package/filepath-1.5.2.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/filepath-1.5.2.0/src/LICENSE) | Library for manipulating FilePaths in a cross platform way. | `hashable`, `lens` | 37 | | `free` | [`5.2`](http://hackage.haskell.org/package/free-5.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/free-5.2/src/LICENSE) | Monads for free | `adjunctions`, `kan-extensions`, `lens` | 38 | | **`ghc-bignum`** | [`1.3`](http://hackage.haskell.org/package/ghc-bignum-1.3) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-bignum-1.3/src/LICENSE) | GHC BigNum library | `ghc-internal`, `hashable` | 39 | | **`ghc-boot-th`** | [`9.10.1`](http://hackage.haskell.org/package/ghc-boot-th-9.10.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-boot-th-9.10.1/src/LICENSE) | Shared functionality between GHC and the @template-haskell@ library | `template-haskell` | 40 | | **`ghc-internal`** | [`9.1001.0`](http://hackage.haskell.org/package/ghc-internal-9.1001.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-internal-9.1001.0/src/LICENSE) | Basic libraries | `base` | 41 | | **`ghc-prim`** | [`0.11.0`](http://hackage.haskell.org/package/ghc-prim-0.11.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-prim-0.11.0/src/LICENSE) | GHC primitives | *(core library)* | 42 | | `hashable` | [`1.5.0.0`](http://hackage.haskell.org/package/hashable-1.5.0.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/hashable-1.5.0.0/src/LICENSE) | A class for types that can be converted to a hash value | `lens`, `semigroupoids`, `strict`, `these`, `unordered-containers` | 43 | | `indexed-traversable` | [`0.1.4`](http://hackage.haskell.org/package/indexed-traversable-0.1.4) | [`BSD-2-Clause`](http://hackage.haskell.org/package/indexed-traversable-0.1.4/src/LICENSE) | FunctorWithIndex, FoldableWithIndex, TraversableWithIndex | `comonad`, `free`, `indexed-traversable-instances`, `lens` | 44 | | `indexed-traversable-instances` | [`0.1.2`](http://hackage.haskell.org/package/indexed-traversable-instances-0.1.2) | [`BSD-2-Clause`](http://hackage.haskell.org/package/indexed-traversable-instances-0.1.2/src/LICENSE) | More instances of FunctorWithIndex, FoldableWithIndex, TraversableWithIndex | `lens` | 45 | | `invariant` | [`0.6.4`](http://hackage.haskell.org/package/invariant-0.6.4) | [`BSD-2-Clause`](http://hackage.haskell.org/package/invariant-0.6.4/src/LICENSE) | Haskell98 invariant functors | `kan-extensions` | 46 | | `kan-extensions` | [`5.2.6`](http://hackage.haskell.org/package/kan-extensions-5.2.6) | [`BSD-3-Clause`](http://hackage.haskell.org/package/kan-extensions-5.2.6/src/LICENSE) | Kan extensions, Kan lifts, the Yoneda lemma, and (co)density (co)monads | `lens` | 47 | | **`mtl`** | [`2.3.1`](http://hackage.haskell.org/package/mtl-2.3.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/mtl-2.3.1/src/LICENSE) | Monad classes for transformers, using functional dependencies | `adjunctions`, `exceptions`, `free`, `kan-extensions`, `lens` | 48 | | **`os-string`** | [`2.0.2`](http://hackage.haskell.org/package/os-string-2.0.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/os-string-2.0.2/src/LICENSE) | Library for manipulating Operating system strings. | `filepath`, `hashable` | 49 | | `parallel` | [`3.2.2.0`](http://hackage.haskell.org/package/parallel-3.2.2.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/parallel-3.2.2.0/src/LICENSE) | Parallel programming library | `lens` | 50 | | **`pretty`** | [`1.1.3.6`](http://hackage.haskell.org/package/pretty-1.1.3.6) | [`BSD-3-Clause`](http://hackage.haskell.org/package/pretty-1.1.3.6/src/LICENSE) | Pretty-printing library | `template-haskell` | 51 | | `primitive` | [`0.9.1.0`](http://hackage.haskell.org/package/primitive-0.9.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/primitive-0.9.1.0/src/LICENSE) | Primitive memory-related operations | `vector` | 52 | | `profunctors` | [`5.6.2`](http://hackage.haskell.org/package/profunctors-5.6.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/profunctors-5.6.2/src/LICENSE) | Profunctors | `adjunctions`, `free`, `invariant`, `kan-extensions`, `lens` | 53 | | `reflection` | [`2.1.9`](http://hackage.haskell.org/package/reflection-2.1.9) | [`BSD-3-Clause`](http://hackage.haskell.org/package/reflection-2.1.9/src/LICENSE) | Reifies arbitrary terms into types that can be reflected back into terms | `lens` | 54 | | `semigroupoids` | [`6.0.1`](http://hackage.haskell.org/package/semigroupoids-6.0.1) | [`BSD-2-Clause`](http://hackage.haskell.org/package/semigroupoids-6.0.1/src/LICENSE) | Semigroupoids: Category sans id | `adjunctions`, `free`, `kan-extensions`, `lens` | 55 | | `semigroups` | [`0.20`](http://hackage.haskell.org/package/semigroups-0.20) | [`BSD-3-Clause`](http://hackage.haskell.org/package/semigroups-0.20/src/LICENSE) | Anything that associates | `adjunctions` | 56 | | **`stm`** | [`2.5.3.1`](http://hackage.haskell.org/package/stm-2.5.3.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/stm-2.5.3.1/src/LICENSE) | Software Transactional Memory | `StateVar`, `exceptions`, `invariant`, `transformers-base` | 57 | | `strict` | [`0.5.1`](http://hackage.haskell.org/package/strict-0.5.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/strict-0.5.1/src/LICENSE) | Strict data types and String IO. | `lens` | 58 | | `tagged` | [`0.8.9`](http://hackage.haskell.org/package/tagged-0.8.9) | [`BSD-3-Clause`](http://hackage.haskell.org/package/tagged-0.8.9/src/LICENSE) | Haskell 98 phantom types to avoid unsafely passing dummy arguments | `adjunctions`, `bifunctors`, `comonad`, `distributive`, `indexed-traversable-instances`, `invariant`, `kan-extensions`, `lens`, `profunctors`, `semigroupoids` | 59 | | **`text`** | [`2.1.1`](http://hackage.haskell.org/package/text-2.1.1) | [`BSD-2-Clause`](http://hackage.haskell.org/package/text-2.1.1/src/LICENSE) | An efficient packed Unicode text type. | `hashable`, `lens`, `strict` | 60 | | `th-abstraction` | [`0.7.1.0`](http://hackage.haskell.org/package/th-abstraction-0.7.1.0) | [`ISC`](http://hackage.haskell.org/package/th-abstraction-0.7.1.0/src/LICENSE) | Nicer interface for reified information about data types | `bifunctors`, `free`, `invariant`, `lens` | 61 | | `these` | [`1.2.1`](http://hackage.haskell.org/package/these-1.2.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/these-1.2.1/src/LICENSE) | An either-or-both data type. | `lens`, `strict` | 62 | | `transformers-base` | [`0.4.6`](http://hackage.haskell.org/package/transformers-base-0.4.6) | [`BSD-3-Clause`](http://hackage.haskell.org/package/transformers-base-0.4.6/src/LICENSE) | Lift computations from the bottom of a transformer stack | `free` | 63 | | `transformers-compat` | [`0.7.2`](http://hackage.haskell.org/package/transformers-compat-0.7.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/transformers-compat-0.7.2/src/LICENSE) | A small compatibility shim for the transformers library | `adjunctions`, `comonad`, `invariant`, `lens`, `semigroupoids`, `transformers-base` | 64 | | `unordered-containers` | [`0.2.20`](http://hackage.haskell.org/package/unordered-containers-0.2.20) | [`BSD-3-Clause`](http://hackage.haskell.org/package/unordered-containers-0.2.20/src/LICENSE) | Efficient hashing-based container types | `indexed-traversable-instances`, `invariant`, `lens`, `semigroupoids` | 65 | | `vector` | [`0.13.2.0`](http://hackage.haskell.org/package/vector-0.13.2.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/vector-0.13.2.0/src/LICENSE) | Efficient Arrays | `indexed-traversable-instances`, `lens` | 66 | | `vector-stream` | [`0.1.0.1`](http://hackage.haskell.org/package/vector-stream-0.1.0.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/vector-stream-0.1.0.1/src/LICENSE) | Efficient Streams | `vector` | 67 | | `void` | [`0.7.3`](http://hackage.haskell.org/package/void-0.7.3) | [`BSD-3-Clause`](http://hackage.haskell.org/package/void-0.7.3/src/LICENSE) | A Haskell 98 logically uninhabited data type | `adjunctions` | 68 | 69 | -------------------------------------------------------------------------------- /resources/duality-nescala.org: -------------------------------------------------------------------------------- 1 | #+title: Duality and How to Delete Half (minus ε) of Your Code 2 | #+author: Greg Pfeil 3 | #+OPTIONS: d:(not LOGBOOK SPEAKERNOTES) 4 | #+DRAWERS: SPEAKERNOTES 5 | #+epresent_frame_level: 3 6 | #+epresent_mode_line: (" @sellout" " Duality and How to Delete Half (minus ε) of Your Code " (:eval (int-to-string epresent-page-number))) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | :speakernotes: 18 | I’ll apologize in advance, but there’s going to be some Haskell on these slides. Also Scala, but some Haskell, too. 19 | 20 | Hopefully the title is motivating – I mean, it’s a lie, but it’s /aspirational/. I’m going to build up a few things, then get to a point where it seems like there might be a lot of code we can delete. Then, I’m going to show you how I managed to actually delete that code … in Haskell. Then there are a couple paths from there 21 | 22 | - how do we get to define something similar in Scala? 23 | - what more general/unified thing are we reaching toward? 24 | :END: 25 | 26 | ↓ Twitter slide ↓ 27 | 28 | * What is a category? 29 | 30 | - objects 31 | - arrows between objects 32 | 33 | - arrows compose 34 | - an identity arrow for each object 35 | 36 | - Scala 37 | - types – functions 38 | - Kleisli 39 | - types – ~A => M[B]~ 40 | - (endo-)functors (in Scala) 41 | - type constructors – ~F[_] ↝ G[_]~ 42 | 43 | * What is duality? 44 | 45 | :speakernotes: 46 | A little CT to start with: for any category /C/, there is an opposite (or dual) category C^op with all the arrows reversed (but the same objects). It follows from reversing the arrows again, that the dual category of C^op is C. 47 | 48 | So far, so easy, right? That’s fundamentally all there is to it, but there are some complications 49 | :END: 50 | 51 | - C 52 | - C ↦ C^op 53 | - (C^op)^op = C 54 | 55 | - C and C^op have the same objects 56 | - every arrow /A → B/ in C has a corresponding arrow /B → A/ in C^op 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ** a tuple 68 | 69 | :speakernotes: 70 | #+begin_src dot :file product.png 71 | digraph { 72 | "(A, B)" -> A [label="fst"] 73 | "(A, B)" -> B [label="snd"] 74 | } 75 | #+end_src 76 | :END: 77 | 78 | #+RESULTS: 79 | [[file:product.png]] 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | *** What’s the dual? 101 | 102 | :speakernotes: 103 | #+begin_src dot :file unknown.png 104 | digraph { 105 | rankdir=BT 106 | A -> "?" 107 | B -> "?" 108 | } 109 | #+end_src 110 | :END: 111 | 112 | #+RESULTS: 113 | [[file:unknown.png]] 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | *** Either! 125 | 126 | :speakernotes: 127 | #+begin_src dot :file coproduct.png 128 | digraph { 129 | rankdir=BT 130 | A -> "Either[A, B]" [label="Left"] 131 | B -> "Either[A, B]" [label="Right"] 132 | } 133 | #+end_src 134 | :END: 135 | 136 | #+RESULTS: 137 | [[file:coproduct.png]] 138 | 139 | 140 | *** /Types/ not /Values/ 141 | 142 | #+begin_src scala 143 | (Int, Char) <-> Either[Int, Char] 144 | (3, 'a') <-🚫-> Left 3 145 | #+end_src 146 | 147 | ** What does it mean to “reverse the arrows”? 148 | 149 | #+begin_src scala 150 | @typeclass trait Functor[F[_]] { 151 | def map[A, B](fa: F[A])(f: A ⇒ B): F[B] = lift(f)(fa) 152 | 153 | def lift[A, B](f: A ⇒ B): F[A] ⇒ F[B] = map(_)(f) 154 | } 155 | #+end_src 156 | 157 | #+begin_src scala 158 | @typeclass trait CoFunctor[F[_]] { 159 | def colift[A, B](f: A ⇐ B): F[A] ⇐ F[B] 160 | } 161 | #+end_src 162 | 163 | #+begin_src scala 164 | @typeclass trait Functor[F[_]] { 165 | def lift[A, B](f: B ⇒ A): F[B] ⇒ F[A] 166 | } 167 | #+end_src 168 | 169 | *This is /not/ variance* 170 | 171 | 172 | 173 | 174 | 175 | *** Composition dual 176 | 177 | #+begin_src scala 178 | def compose[A, B, C](f: B ⇒ C, g: A ⇒ B): A ⇒ C 179 | #+end_src 180 | 181 | 182 | #+begin_src scala 183 | def co_pose[A, B, C](f: B ⇐ C, g: A ⇐ B): A ⇐ C 184 | #+end_src 185 | 186 | 187 | #+begin_src scala 188 | def andThen[A, B, C](f: C ⇒ B, g: B ⇒ A): C ⇒ A = 189 | compose(g, f) 190 | #+end_src 191 | 192 | 193 | 194 | #+begin_src scala 195 | dual(f ∘ g) === dual(g) ∘ dual(f) 196 | #+end_src 197 | 198 | ** other duals 199 | 200 | - ~Monad~ ↔ ~Comonad~ 201 | - ~Reader~ ↔ ~Env~ (~Writer~ without a ~Monoid~) 202 | - ~Writer~ ↔ ~Traced~ (~Reader~ /with/ a ~Monoid~) 203 | - ~State~ ↔ ~Store~ 204 | - ~Free~ ↔ ~Cofree~ 205 | - ~Traverse~ ↔ ~Distributive~ 206 | - ~Recursive~ ↔ ~Corecursive~ 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | *** Monad ↔ Comonad 225 | 226 | #+begin_src scala 227 | @typeclass trait Monad[M[_]] { 228 | def pure[A]: A => M[A] 229 | 230 | def flatMap[A, B](f: A => M[B]): M[A] => M[B] 231 | } 232 | 233 | @typeclass trait Comonad[W[_]] { 234 | def extract[A]: W[A] => A 235 | 236 | def coflatMap[B, A](f: W[B] => A): W[B] => W[A] // extend 237 | } 238 | #+end_src 239 | 240 | ** duals compose 241 | 242 | #+begin_src haskell 243 | gcata 244 | :: (Functor f, Comonad w) 245 | => (forall b. f (w b) -> w (f b)) 246 | -> (f (w a) -> a) 247 | -> Fix f -> a 248 | gcata k φ = extract . cata (fmap φ . k . fmap duplicate) 249 | 250 | gana 251 | :: (Functor f, Monad m) 252 | => (forall b. m (f b) -> f (m b)) 253 | -> (a -> f (m a)) 254 | -> a -> Fix f 255 | gana k ψ = ana (fmap join . k . fmap ψ) . pure 256 | #+end_src 257 | 258 | ** even in Scala 259 | 260 | #+begin_src scala 261 | def gcata[F[_]: Functor, W[_]: Comonad, A] 262 | (t: Fix[F]) 263 | (k: (F ∘ W) ~> (W ∘ F), φ: F[W[A]] => A) 264 | : A = 265 | t.cata(fa => k(fa.map(_.cojoin)).map(φ)).extract 266 | 267 | def gana[F[_]: Functor, M[_]: Monad, A] 268 | (a: A) 269 | (k: (M ∘ F) ~> (F ∘ M), ψ: A => F[M[A]]) 270 | : Fix[F] = 271 | a.pure[M].ana(ma => k(ma.map(ψ)).map(_.join)) 272 | #+end_src 273 | 274 | ~(F ∘ W)~ is shorthard for ~λ[A => F[W[A]]]~ 275 | 276 | * Dualizer 277 | 278 | Automatically Derive Duals 279 | (but not in Scala yet) 280 | 281 | - https://github.com/sellout/dualizer 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | ** Types 297 | 298 | #+begin_src haskell 299 | testT :: $(dualType =<< [t|Either Int Char|]) 300 | testT = (3, 'a') 301 | #+end_src 302 | 303 | :speakernotes: 304 | Yes, this compiles. Just as we talked about earlier, the dual of Either is a tuple. And we can do the opposite, too, since C^op^op 305 | :END: 306 | 307 | 308 | #+begin_src haskell 309 | testE :: $(dualType =<< [t|forall a. (Int, a)|]) 310 | testE = Left 3 311 | #+end_src 312 | 313 | ** Expressions 314 | 315 | #+begin_src haskell 316 | snd :: (a, b) -> b 317 | snd = $(dualExp =<< [e|Right|]) 318 | #+end_src 319 | 320 | #+begin_src haskell 321 | right :: b -> Either a b 322 | right = $(dualExp =<< [e|snd|]) 323 | #+end_src 324 | 325 | ** Ergonomics 326 | 327 | :speakernotes: 328 | I’m going to jump right to the fun part here. 329 | :END: 330 | 331 | 332 | #+begin_src haskell 333 | data Fix f = Fix { project :: f (Fix f) } 334 | labelSelfDual ''Fix -- Not actually true 335 | labelDual 'Fix 'project 336 | 337 | makeDualDec 338 | [d| cata :: (f a -> a) -> Fix f -> a 339 | cata f = f . fmap (cata f) . project |] 340 | "ana" 341 | #+end_src 342 | 343 | 344 | :speakernotes: 345 | So, we declare a new data type representing the fixed-point of functors. We don’t need to care about the specific data type so much, other than to note that it is its own dual, and that the constructor for it is dual to its sole accessor. Then we define a function that uses this type, wrapping it in a bit of magic that says “also define the dual of this function, and give it the name ~ana~.” The generated dual looks like: 346 | :END: 347 | 348 | #+begin_src haskell 349 | ana :: (a -> f a) -> a -> Fix f 350 | ana f = Fix . fmap (ana f) . f 351 | #+end_src 352 | 353 | 354 | :speakernotes: 355 | So, let’s walk through this a bit … the first argument is a function (and the dual of a function just inverts the arrows, as we’ve seen). And then we hit the same issue we did with ~Functor~ – rather than this being a function of two arguments, it’s really a function that takes a function and returns a function. So, we flip the arrow of the returned function as well. So, we now have the correct type for ~ana~. 356 | 357 | Now, the value … first, the dual of ~f . g~ is ~g . f~, so all the compositions should be flipped, which means we compare ~project~ to ~Fix~, ~fmap~ to ~fmap~, and ~f~ to ~f~. We’ve just declared that ~Fix~ and ~project~ are duals, so that works. Variables are sort of self-dual (their types have been dualized, so it really represents a different expression), so ~f~ works as well. Finally, we have ~fmap~. We know that that is self-dual already, but we have to take the dual of the function we pass to it, and as we’ve just defined here, ~ana~ is (going to be) the dual of ~cata~, so we swap that in there, and great – it all checks out! 358 | :END: 359 | 360 | ** A lot of other stuff … 361 | 362 | #+begin_src haskell 363 | makeDualDec [d|type Algebra f a = f a -> a|] "Coalgebra" 364 | 365 | makeDualDec [d|newtype Either' a b = Either' (Either a b)|] 366 | "Tuple'" 367 | makeDualDec [d|data Either'' a b = Either'' (Either a b)|] 368 | "Tuple''" 369 | 370 | makeDualDec [d|data family TestA a|] "DualA" 371 | makeDualDec [d|type family TestB a|] "DualB" 372 | makeDualDec 373 | [d| type family TestC a where 374 | TestC (Either b c) = b 375 | TestC Int = Char |] 376 | "DualC" 377 | #+end_src 378 | 379 | ** to help avoid 380 | 381 | #+begin_src haskell 382 | gcata 383 | :: (Functor f, Comonad w) 384 | => (forall b. f (w b) -> w (f b)) 385 | -> (f (w a) -> a) 386 | -> Fix f -> a 387 | gcata k φ = extract . cata (fmap φ . k . fmap duplicate) 388 | 389 | gana 390 | :: (Functor f, Monad m) 391 | => (forall b. m (f b) -> f (m b)) 392 | -> (a -> f (m a)) 393 | -> a -> Fix f 394 | gana k ψ = ana (fmap join . k . fmap ψ) . pure 395 | #+end_src 396 | 397 | * curryhoward 398 | 399 | Automatically Derive Implementations 400 | 401 | - https://github.com/Chymyst/curryhoward 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | ** wat? 418 | 419 | #+begin_src scala 420 | def pure[E, A]: A ⇒ (E ⇒ A) = implement 421 | 422 | def map[E, A, B]: (E ⇒ A) ⇒ (A ⇒ B) ⇒ (E ⇒ B) = implement 423 | 424 | def flatMap[E, A, B]: (E ⇒ A) ⇒ (A ⇒ E ⇒ B) ⇒ (E ⇒ B) = 425 | implement 426 | #+end_src 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | ** commutative diagram 440 | 441 | :speakernotes: 442 | #+begin_src dot :file curry.png 443 | digraph commutative { 444 | Type [shape=none] 445 | Cotype [shape=none] 446 | Impl [shape=none] 447 | Coimpl [shape=none] 448 | 449 | subgraph { 450 | rank=same 451 | Type -> Cotype [label=dualType] 452 | } 453 | Type -> Impl [label=implement] 454 | Cotype -> Coimpl [label=implement] 455 | subgraph { 456 | rank=same 457 | Impl -> Coimpl [label=dualExp] 458 | } 459 | } 460 | #+end_src 461 | :END: 462 | 463 | #+RESULTS: 464 | [[file:curry.png]] 465 | 466 | * it’s not that easy, though 467 | 468 | #+begin_src scala 469 | @typeclass trait Functor[F[_]] { 470 | def map[A, B](fa: F[A])(f: A ⇒ B): F[B] 471 | } 472 | #+end_src 473 | 474 | :speakernotes: 475 | We lied about ~Functor~ earlier, right? ~lift~ is useful, and but there’s this other function you may have heard of – ~map~. So, what does it mean to reverse the arrows here? 476 | :END: 477 | 478 | 479 | #+begin_src scala 480 | @typeclass trait Contravariant[F[_]] { 481 | def contramap[A, B](fa: F[A])(f: B ⇒ A): F[B] 482 | } 483 | #+end_src 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | :speakernotes: 494 | But we showed earlier that the dual of a ~Functor~ is still ~Functor~ … so how can it be ~Contravariant~ here? 495 | 496 | It can’t. This is wrong. Our “reversing the arrows” was too naive. (BTW, Haskell runs into this, too – with ~>>=~ for example. 497 | 498 | And there are other problems – 499 | :END: 500 | 501 | ** Too Many Arrows? 502 | 503 | #+begin_src scala 504 | @typeclass trait Functor[F[_]] { 505 | def lift[A, B](f: A ⇒ B): F[A] ⇒ F[B] 506 | } 507 | #+end_src 508 | 509 | #+begin_src scala 510 | @typeclass trait Functor[F[_]] { 511 | def lift[A, B]: (A ⇒ B) ⇒ F[A] ⇒ F[B] 512 | } 513 | #+end_src 514 | 515 | #+begin_src scala 516 | @typeclass trait Nope[F[_]] { 517 | def unhUh[A, B]: F[B] ⇒ F[A] ⇒ B ⇒ A 518 | } 519 | #+end_src 520 | 521 | 522 | 523 | :speakernotes: 524 | So … how do we know /which/ arrows to reverse? 525 | 526 | Practically? Some guessing. 527 | 528 | More generally? 529 | :END: 530 | 531 | * Back to Categories 532 | 533 | #+begin_src scala 534 | @typeclass trait Category[Arr[_, _]] { 535 | 536 | def compose[A, B, C](f: Arr[B, C], g: Arr[A, B]): Arr[A, C] 537 | 538 | def id[A]: Arr[A, A] 539 | } 540 | 541 | implicit val function1Category = new Category[Function1] { 542 | def compose[A, B, C](f: B => C, g: A => B): A => C = 543 | x => f(g(x)) 544 | 545 | def id[A]: A => A = 546 | x => x 547 | } 548 | #+end_src 549 | 550 | ** instances 551 | 552 | - ~Function1[?, ?]~ 553 | - ~Kleisli[M[_], ?, ?]~ 554 | - ~Cokleisli[W[_], ?, ?]~ 555 | 556 | 557 | - ~Iso[?, ?]~ 558 | - ~Lens[?, ?]~ 559 | - ~Prism[?, ?]~ 560 | 561 | ** a new ~Functor~ rises 562 | 563 | #+begin_src scala 564 | @typeclass trait Functor[F[_]] { 565 | type J[_, _] 566 | type K[_, _] 567 | 568 | def lift[A, B]: J[A, B] => K[F[A], F[B]] 569 | } 570 | 571 | implicit def optionFunctor = new Functor[Option] { 572 | type J[A, B] = A => B 573 | type K[A, B] = A => B 574 | 575 | def lift[A, B](f: A => B): Option[A] => Option[B] = { 576 | Some(a) => Some(f(a)) 577 | Nothing => Nothing 578 | } 579 | } 580 | #+end_src 581 | 582 | ** a new ~Functor~ rises 583 | 584 | #+begin_src scala 585 | @typeclass trait Functor[F[_]] { 586 | 587 | 588 | 589 | def lift[A, B]: (A => B) => F[A] => F[B] 590 | } 591 | 592 | implicit def optionFunctor = new Functor[Option] { 593 | 594 | 595 | 596 | def lift[A, B](f: A => B): Option[A] => Option[B] = { 597 | Some(a) => Some(f(a)) 598 | Nothing => Nothing 599 | } 600 | } 601 | #+end_src 602 | 603 | ** A New ~Category~! 604 | 605 | #+begin_src scala 606 | final case class Op[Arr[_, _], A, B](dual: Arr[B, A]) 607 | 608 | 609 | implicit def opCategory[Arr[_, _]] 610 | (implicit Arr: Category[Arr]) = 611 | 612 | new Category[Op[Arr, ?, ?]] { 613 | def compose[A, B, C](f: Op[Arr, B, C], g: Op[Arr, A, B]) 614 | : Op[Arr, A, C] = 615 | Arr.compose(g.dual, f.dual) 616 | 617 | def id[A]: Op[Arr, A, A] = Op(Arr.id) 618 | } 619 | #+end_src 620 | 621 | #+begin_src scala 622 | dual(f ∘ g) === dual(g) ∘ dual(f) 623 | #+end_src 624 | 625 | ** instances 626 | 627 | - ~Function1[?, ?]~ 628 | - ~Kleisli[M[_], ?, ?]~ 629 | - ~Cokleisli[W[_], ?, ?]~ 630 | - ~Op[Arr[_, _], ?, ?]~ 631 | 632 | ** Functor now subsumes Contravariant 633 | 634 | #+begin_src scala 635 | @typeclass trait Contravariant[F[_]] { 636 | def contramap[A, B](f: B => A): F[A] => F[B] 637 | } 638 | #+end_src 639 | 640 | #+begin_src scala 641 | implicit val eqFunctor = new Functor[Eq] { 642 | type J[A, B] = Op[Function1, A, B] 643 | type K[A, B] = A => B 644 | 645 | // f: B => A 646 | def lift[A, B](f: Op[Function1, A, B]): Eq[A] => Eq[B] = 647 | Eq.by(f.run)(_) 648 | } 649 | #+end_src 650 | 651 | ** And we can now distinguish arrows 652 | 653 | #+begin_src scala 654 | @typeclass trait Functor[F[_]] { 655 | def lift[A, B]: (A => B) => F[A] => F[B] 656 | } 657 | #+end_src 658 | 659 | #+begin_src scala 660 | @typeclass trait Functor[F[_]] { 661 | J[_, _] 662 | K[_, _] 663 | 664 | def lift[A, B]: J[A, B] => K[F[A], F[B]] 665 | } 666 | #+end_src 667 | 668 | # ** other instances? 669 | 670 | # - ~FunctionK[?[_], ?[_]]~ 671 | 672 | # #+begin_src scala 673 | # List ~> Option 674 | # #+end_src 675 | 676 | # #+begin_src scala 677 | # List[Int] ~> Option[Char] 🚫 678 | # #+end_src 679 | 680 | # ** How? 681 | 682 | # - ~FunctionK[?[_], ?[_]]~ 683 | 684 | # #+begin_src scala 685 | # @typeclass trait Category[Arr[_, _]] { ... } 686 | # #+end_src 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | # *** Kind Polymorphism 704 | 705 | # #+begin_src idris 706 | # interface Category (arr : k -> k -> Type) where 707 | # id : arr a a 708 | # (.) : arr b c -> arr a b -> arr a c 709 | # #+end_src 710 | 711 | # :speakernotes: 712 | # - Mandubian for kind-polymorphism work 713 | # - Miles at ScalaDays(?) 714 | # :END: 715 | 716 | # #+begin_src scala 717 | # trait Category[Arr[_ <: AnyKind, _ <: AnyKind]] { 718 | 719 | # def compose 720 | # [A <: AnyKind, B <: AnyKind, C <: AnyKind] 721 | # (f: Arr[B, C], g: Arr[A, B]) 722 | # : Arr[A, C] 723 | 724 | # def id[A <: AnyKind]: Arr[A, A] 725 | # } 726 | # #+end_src 727 | 728 | 729 | 730 | # :speakernotes: 731 | # These are similar, /except/ that the Scala case doesn’t require the ~AnyKind~ variables to have the /same/ kind, whereas the Haskell version uses a /kind variable/ ~k~ to say “whatever kind ~k~ is, both parameters to ~arr~ must be of that kind.” 732 | # :END: 733 | 734 | # *** And a semi-poly-kinded ~Functor~ 735 | 736 | # #+begin_src scala 737 | # // F[_ <: AnyKind] <: AnyKind // 🚫 738 | # @typeclass trait Functor[F[_ <: AnyKind]] { 739 | # type J[_ <: AnyKind, _ <: AnyKind] 740 | # type K[_, _] 741 | 742 | # def lift[A <: AnyKind, B <: AnyKind](f: J[A, B]) 743 | # : K[F[A], F[B]] 744 | # } 745 | # #+end_src 746 | 747 | ** Quote! 748 | 749 | “Most if not all constructions in category theory are parametric in the underlying category, resulting in a remarkable economy of expression. […] This possibly leads to a new style of programming, which could be loosely dubbed as /category-parametric programming/.” 750 | #+begin_justifyright 751 | ———Ralf Hinze, [[http://www.cs.ox.ac.uk/ralf.hinze/SSGIP10/AdjointFolds.pdf][Adjoint Folds and Unfolds]] 752 | #+end_justifyright 753 | 754 | :speakernotes: 755 | So, all of these things 756 | - the ~Category~ type class 757 | - Template Haskell / macros 758 | - kind-polymorphism 759 | are helping us approach a more unified idea – category-parametric programming. We still have a way to go, even in Haskell, Idris, whatever. But it’s really fun to think about. 760 | :END: 761 | 762 | 763 | ** MEME! 764 | [[file:expanding-brain-category.jpg]] 765 | 766 | * Questions? 767 | 768 | [[file:FormationLogo_320x132.png]] 769 | 770 | - Formation is hiring! – https://formation.ai/ 771 | - *Typelevel* Scala – Cats, http4s, Shapeless Circe, and soon … Turtles (I hope) 772 | - Haskell 773 | 774 | - Greg Pfeil (aka, Sellout) 775 | - greg@technomadic.org 776 | - https://github.com/sellout 777 | -------------------------------------------------------------------------------- /dualizer/src/Categorical/Dual.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | {-# LANGUAGE UndecidableInstances #-} 5 | {-# LANGUAGE Unsafe #-} 6 | 7 | -- | Operations to connect dual constructions. 8 | module Categorical.Dual 9 | ( importDuals, 10 | exportDuals, 11 | emptyDuals, -- shouldn’t export this 12 | shareDuals, 13 | dualType, 14 | dualExp, 15 | makeDualClass, 16 | makeDualDec, 17 | makeDualExp, 18 | labelDual, 19 | labelSelfDual, 20 | labelSemiDual, 21 | ) 22 | where 23 | 24 | import safe Control.Applicative (pure, (<*>)) 25 | import safe Control.Arrow ((***)) 26 | import safe Control.Category (id, (.)) 27 | import safe Control.Lens (makeLenses, (%~), (&)) 28 | import safe Control.Monad (Monad, fail, join, (<=<), (=<<)) 29 | import safe Control.Monad.Trans.Class (lift) 30 | import safe Control.Monad.Trans.Except 31 | ( ExceptT (ExceptT), 32 | runExceptT, 33 | throwE, 34 | withExceptT, 35 | ) 36 | import safe Data.Bitraversable (bisequence) 37 | import safe Data.Data (Data) 38 | import safe Data.Either (Either (Left, Right), either) 39 | import safe Data.Eq (Eq) 40 | import safe Data.Function (const, flip, ($)) 41 | import safe Data.Functor (fmap, (<$), (<$>)) 42 | import safe Data.List (nub) 43 | import safe Data.Map (Map) 44 | import safe Data.Map qualified as Map 45 | import safe Data.Maybe (maybe) 46 | import safe Data.Monoid (Monoid, mappend, mempty) 47 | import safe Data.Semigroup (Semigroup, (<>)) 48 | import safe Data.String (String) 49 | import safe Data.Traversable (sequenceA, traverse) 50 | import safe Data.Tuple (swap, uncurry) 51 | import safe Data.Void (Void) 52 | import safe Language.Haskell.TH qualified as TH 53 | import safe Language.Haskell.TH.Syntax 54 | ( Body (GuardedB, NormalB), 55 | Clause (Clause), 56 | Con (ForallC, GadtC, InfixC, NormalC, RecC, RecGadtC), 57 | Guard (NormalG, PatG), 58 | Match (Match), 59 | Name, 60 | Q, 61 | TySynEqn (TySynEqn), 62 | TypeFamilyHead (TypeFamilyHead), 63 | getQ, 64 | liftData, 65 | mkName, 66 | putQ, 67 | recover, 68 | reify, 69 | ) 70 | import safe Text.Show (Show, show) 71 | import safe Prelude (undefined) 72 | 73 | data DualMappings = DualMappings 74 | { _dualTypes :: Map Name TH.Type, 75 | _dualValues :: Map Name TH.Exp 76 | } 77 | deriving stock (Data, Eq) 78 | 79 | makeLenses ''DualMappings 80 | 81 | instance Semigroup DualMappings where 82 | DualMappings t v <> DualMappings t' v' = 83 | -- NB: I reversed the order here, because I _think_ this is supposed to be 84 | -- right-biased? 85 | DualMappings (t' `Map.union` t) (v' `Map.union` v) 86 | 87 | instance Monoid DualMappings where 88 | mappend = (<>) 89 | mempty = DualMappings Map.empty Map.empty 90 | 91 | -- | The empty set of duals, should only be used to initalize the duals for 92 | -- `Prelude`. 93 | emptyDuals :: Q DualMappings 94 | emptyDuals = pure $ DualMappings Map.empty Map.empty 95 | 96 | reifyDuals :: DualMappings -> Q TH.Exp 97 | reifyDuals duals = 98 | [e|maybe $(liftData duals) ($(liftData duals) <>) <$> getQ|] 99 | 100 | shareDuals :: DualMappings -> Q TH.Exp 101 | shareDuals duals = 102 | [e|[] <$ (putQ . maybe $(liftData duals) ($(liftData duals) <>) =<< getQ)|] 103 | 104 | -- TODO: Move this somewhere better 105 | data AndMaybe a b = Only a | Indeed a b deriving stock (Eq, Show) 106 | 107 | andMaybe :: (a -> c) -> (a -> b -> c) -> a `AndMaybe` b -> c 108 | andMaybe f g = \case 109 | Only a -> f a 110 | Indeed a b -> g a b 111 | 112 | fromInfo :: TH.Info -> Q (TH.Type `AndMaybe` TH.Exp) 113 | fromInfo = \case 114 | TH.ClassI (TH.ClassD _ n _ _ _) _ -> pure . Only $ TH.ConT n 115 | TH.ClassI d _ -> fail $ "unknown dec to extract name from: " <> show d 116 | TH.ClassOpI n t _ -> pure . Indeed t $ TH.VarE n 117 | TH.TyConI (TH.DataD _ n _ _ _ _) -> pure . Only $ TH.ConT n 118 | TH.TyConI (TH.TySynD n _ _) -> pure . Only $ TH.ConT n 119 | TH.TyConI (TH.NewtypeD _ n _ _ _ _) -> pure . Only $ TH.ConT n 120 | TH.TyConI d -> fail $ "unknown dec to extract name from: " <> show d 121 | TH.FamilyI d _ -> fail $ "not yet getting type families – " <> show d -- FIXME 122 | TH.PrimTyConI n _ _ -> pure . Only $ TH.ConT n 123 | TH.DataConI n t _ -> pure . Indeed t $ TH.ConE n 124 | TH.PatSynI _ _ -> fail "pattern synonym is not a type" 125 | TH.VarI n t _ -> pure . Indeed t $ TH.VarE n 126 | TH.TyVarI _ t -> pure $ Only t 127 | 128 | fromName :: Name -> Q (TH.Type `AndMaybe` TH.Exp) 129 | fromName = fromInfo <=< reify 130 | 131 | typeFromName :: Name -> Q TH.Type 132 | typeFromName = fmap (andMaybe id const) . fromName 133 | 134 | expFromName :: Name -> Q TH.Exp 135 | expFromName = 136 | andMaybe (\t -> fail $ show t <> " is not a value") (\_ e -> pure e) 137 | <=< fromName 138 | 139 | -- | Returns a `TH.Type` that is the dual of the named type. 140 | dualTypeName :: Map Name TH.Type -> Name -> ExceptT TH.Type Q TH.Type 141 | dualTypeName db name = 142 | maybe (dualType' db <=< lift $ typeFromName name) pure $ Map.lookup name db 143 | 144 | dualExpName :: DualMappings -> Name -> ExceptT (Either TH.Type TH.Exp) Q TH.Exp 145 | dualExpName db name = 146 | maybe (dualExp' db <=< lift $ expFromName name) pure $ 147 | Map.lookup name (_dualValues db) 148 | 149 | retrieveDuals :: Q DualMappings 150 | retrieveDuals = maybe (fail "no duals imported") pure =<< getQ 151 | 152 | -- FIXME: This can get into an infinite loop in the case of missing duals. 153 | dualType' :: Map Name TH.Type -> TH.Type -> ExceptT TH.Type Q TH.Type 154 | dualType' db = \case 155 | TH.ForallT vs c t -> 156 | TH.ForallT vs <$> traverse (dualType' db) c <*> dualType' db t 157 | TH.ForallVisT vs t -> TH.ForallVisT vs <$> dualType' db t 158 | TH.AppT (TH.AppT TH.ArrowT t) inner@(TH.AppT (TH.AppT TH.ArrowT _) _) -> do 159 | t' <- dualType' db t 160 | TH.AppT (TH.AppT TH.ArrowT t') <$> dualType' db inner 161 | TH.AppT (TH.AppT TH.ArrowT t) t' -> 162 | TH.AppT <$> (TH.AppT TH.ArrowT <$> dualType' db t') <*> dualType' db t 163 | TH.AppT t t' -> TH.AppT <$> dualType' db t <*> dualType' db t' 164 | TH.AppKindT t k -> TH.AppKindT <$> dualType' db t <*> pure k 165 | TH.SigT t k -> flip TH.SigT k <$> dualType' db t 166 | TH.VarT n -> pure $ TH.VarT n 167 | TH.ConT n -> dualTypeName db n 168 | TH.PromotedT n -> pure $ TH.PromotedT n 169 | TH.InfixT _t n _t' -> dualTypeName db n -- t t' 170 | TH.UInfixT _t n _t' -> dualTypeName db n -- t t' 171 | TH.ParensT t -> pure $ TH.ParensT t 172 | TH.TupleT 0 -> pure $ TH.ConT ''Void 173 | TH.TupleT 1 -> pure $ TH.TupleT 1 174 | TH.TupleT 2 -> pure $ TH.ConT ''Either 175 | f@(TH.TupleT _) -> throwE f 176 | TH.UnboxedTupleT i -> pure $ TH.UnboxedSumT i 177 | TH.UnboxedSumT a -> pure $ TH.UnboxedTupleT a 178 | TH.ArrowT -> pure TH.ArrowT 179 | TH.EqualityT -> pure TH.EqualityT 180 | TH.ListT -> pure TH.ListT 181 | TH.PromotedTupleT 0 -> pure $ TH.ConT ''Void 182 | TH.PromotedTupleT 1 -> pure $ TH.PromotedTupleT 1 183 | TH.PromotedTupleT 2 -> pure $ TH.ConT ''Either 184 | f@(TH.PromotedTupleT _) -> throwE f 185 | TH.PromotedNilT -> pure TH.PromotedNilT 186 | TH.PromotedConsT -> pure TH.PromotedConsT 187 | TH.StarT -> pure TH.StarT 188 | TH.ConstraintT -> pure TH.ConstraintT 189 | TH.LitT l -> pure $ TH.LitT l 190 | TH.WildCardT -> pure TH.WildCardT 191 | TH.ImplicitParamT n t -> TH.ImplicitParamT n <$> dualType' db t 192 | #if MIN_VERSION_template_haskell(2, 19, 0) 193 | TH.PromotedInfixT _t n _t' -> dualTypeName db n -- t t' 194 | TH.PromotedUInfixT _t n _t' -> dualTypeName db n -- t t' 195 | #endif 196 | #if MIN_VERSION_template_haskell(2, 17, 0) 197 | TH.MulArrowT -> pure TH.MulArrowT 198 | #endif 199 | 200 | exceptT :: (Monad m) => (t1 -> m c) -> (t2 -> m c) -> ExceptT t1 m t2 -> m c 201 | exceptT f g = 202 | ( \case 203 | Left a -> f a 204 | Right a -> g a 205 | ) 206 | <=< runExceptT 207 | 208 | -- | Returns a type that is the dual of the input type. 209 | dualType :: TH.Type -> Q TH.Type 210 | dualType type' = do 211 | duals <- _dualTypes <$> retrieveDuals 212 | exceptT (\t -> fail $ "no dual for type " <> show t) pure $ 213 | dualType' duals type' 214 | 215 | dualGuard' :: DualMappings -> Guard -> ExceptT (Either TH.Type TH.Exp) Q Guard 216 | dualGuard' db = \case 217 | NormalG e -> NormalG <$> dualExp' db e 218 | PatG ss -> PatG <$> traverse (dualStmt' db) ss 219 | 220 | dualDec' :: DualMappings -> TH.Dec -> ExceptT (Either TH.Type TH.Exp) Q TH.Dec 221 | dualDec' _db = pure 222 | 223 | dualPat' :: DualMappings -> TH.Pat -> ExceptT (Either TH.Type TH.Exp) Q TH.Pat 224 | dualPat' db = \case 225 | TH.LitP l -> pure $ TH.LitP l 226 | TH.VarP n -> pure $ TH.VarP n 227 | TH.TupP ps -> TH.TupP <$> traverse (dualPat' db) ps -- FIXME: should also Either? 228 | p@(TH.UnboxedTupP _ps) -> lift . fail $ "unhandled pattern " <> show p 229 | p@(TH.UnboxedSumP _p _a _a') -> lift . fail $ "unhandled pattern " <> show p 230 | p@(TH.InfixP _p _n _p') -> lift . fail $ "unhandled pattern " <> show p 231 | p@(TH.UInfixP _p _n _p') -> lift . fail $ "unhandled pattern " <> show p 232 | p@(TH.ParensP _p) -> lift . fail $ "unhandled pattern " <> show p 233 | p@(TH.TildeP _p) -> lift . fail $ "unhandled pattern " <> show p 234 | p@(TH.BangP _p) -> lift . fail $ "unhandled pattern " <> show p 235 | p@(TH.AsP _n _p) -> lift . fail $ "unhandled pattern " <> show p 236 | p@TH.WildP -> lift . fail $ "unhandled pattern " <> show p 237 | p@(TH.RecP _n _fps) -> lift . fail $ "unhandled pattern " <> show p 238 | p@(TH.ListP _ps) -> lift . fail $ "unhandled pattern " <> show p 239 | p@(TH.SigP _p _t) -> lift . fail $ "unhandled pattern " <> show p 240 | p@(TH.ViewP _e _p) -> lift . fail $ "unhandled pattern " <> show p 241 | #if MIN_VERSION_template_haskell(2, 22, 0) 242 | p@(TH.TypeP _t) -> lift . fail $ "unhandled pattern " <> show p 243 | p@(TH.InvisP _t) -> lift . fail $ "unhandled pattern " <> show p 244 | #endif 245 | #if MIN_VERSION_template_haskell(2, 18, 0) 246 | p@(TH.ConP _n _ts _ps) -> lift . fail $ "unhandled pattern " <> show p 247 | #else 248 | p@(TH.ConP _n _ps) -> lift . fail $ "unhandled pattern " <> show p 249 | #endif 250 | 251 | dualBody' :: DualMappings -> Body -> ExceptT (Either TH.Type TH.Exp) Q Body 252 | dualBody' db = \case 253 | GuardedB xs -> 254 | GuardedB <$> traverse (bisequence . (dualGuard' db *** dualExp' db)) xs 255 | NormalB e -> NormalB <$> dualExp' db e 256 | 257 | dualMatch' :: DualMappings -> Match -> ExceptT (Either TH.Type TH.Exp) Q Match 258 | dualMatch' db (Match p b ds) = 259 | Match <$> dualPat' db p <*> dualBody' db b <*> traverse (dualDec' db) ds 260 | 261 | dualExp' :: DualMappings -> TH.Exp -> ExceptT (Either TH.Type TH.Exp) Q TH.Exp 262 | dualExp' db = \case 263 | v@(TH.VarE n) -> 264 | ExceptT . recover (pure $ pure v) . runExceptT $ dualExpName db n 265 | TH.ConE n -> dualExpName db n 266 | l@(TH.LitE _) -> pure l 267 | TH.AppE a b -> TH.AppE <$> dualExp' db a <*> dualExp' db b 268 | TH.AppTypeE e t -> 269 | TH.AppTypeE 270 | <$> dualExp' db e 271 | <*> withExceptT Left (dualType' (_dualTypes db) t) 272 | TH.InfixE a o b -> 273 | TH.InfixE 274 | <$> traverse (dualExp' db) a 275 | <*> dualExp' db o 276 | <*> traverse (dualExp' db) b 277 | TH.UInfixE a o b -> 278 | TH.UInfixE <$> dualExp' db a <*> dualExp' db o <*> dualExp' db b 279 | TH.ParensE e -> TH.ParensE <$> dualExp' db e 280 | TH.LamE p e -> TH.LamE p <$> dualExp' db e 281 | TH.LamCaseE matches -> TH.LamCaseE <$> traverse (dualMatch' db) matches 282 | TH.TupE es -> TH.TupE <$> traverse (traverse $ dualExp' db) es -- FIXME: Doesn’t seem right. 283 | TH.UnboxedTupE es -> TH.UnboxedTupE <$> traverse (traverse $ dualExp' db) es 284 | TH.UnboxedSumE e alt ar -> 285 | TH.UnboxedSumE <$> dualExp' db e <*> pure alt <*> pure ar 286 | TH.CondE t c a -> 287 | TH.CondE <$> dualExp' db t <*> dualExp' db c <*> dualExp' db a 288 | TH.MultiIfE cases -> 289 | TH.MultiIfE 290 | <$> traverse (bisequence . (dualGuard' db *** dualExp' db)) cases 291 | TH.LetE ds e -> TH.LetE <$> traverse (dualDec' db) ds <*> dualExp' db e 292 | TH.CaseE e ms -> TH.CaseE <$> dualExp' db e <*> traverse (dualMatch' db) ms 293 | TH.CompE ss -> TH.CompE <$> traverse (dualStmt' db) ss 294 | TH.ArithSeqE r -> pure $ TH.ArithSeqE r 295 | TH.ListE es -> TH.ListE <$> traverse (dualExp' db) es 296 | TH.SigE e t -> 297 | TH.SigE <$> dualExp' db e <*> withExceptT Left (dualType' (_dualTypes db) t) 298 | e@(TH.RecConE _ _) -> throwE $ Right e 299 | e@(TH.RecUpdE _ _) -> throwE $ Right e 300 | TH.StaticE e -> TH.StaticE <$> dualExp' db e 301 | TH.UnboundVarE n -> pure $ TH.UnboundVarE n 302 | TH.LabelE l -> pure $ TH.LabelE l 303 | TH.ImplicitParamVarE n -> pure $ TH.ImplicitParamVarE n 304 | #if MIN_VERSION_template_haskell(2, 22, 0) 305 | e@(TH.TypeE _) -> throwE $ Right e 306 | #endif 307 | #if MIN_VERSION_template_haskell(2, 21, 0) 308 | e@(TH.TypedBracketE _) -> throwE $ Right e 309 | e@(TH.TypedSpliceE _) -> throwE $ Right e 310 | #endif 311 | #if MIN_VERSION_template_haskell(2, 19, 0) 312 | TH.LamCasesE cs -> TH.LamCasesE <$> traverse (dualClause' db) cs 313 | #endif 314 | #if MIN_VERSION_template_haskell(2, 18, 0) 315 | TH.GetFieldE e f -> TH.GetFieldE <$> dualExp' db e <*> pure f 316 | TH.ProjectionE fs -> pure $ TH.ProjectionE fs 317 | #endif 318 | #if MIN_VERSION_template_haskell(2, 17, 0) 319 | TH.DoE m ss -> TH.DoE m <$> traverse (dualStmt' db) ss 320 | TH.MDoE m ss -> TH.MDoE m <$> traverse (dualStmt' db) ss 321 | #else 322 | TH.DoE ss -> TH.DoE <$> traverse (dualStmt' db) ss 323 | TH.MDoE ss -> TH.MDoE <$> traverse (dualStmt' db) ss 324 | #endif 325 | 326 | dualClause' :: DualMappings -> Clause -> ExceptT (Either TH.Type TH.Exp) Q Clause 327 | dualClause' db (Clause ps b ds) = 328 | Clause 329 | <$> traverse (dualPat' db) ps 330 | <*> dualBody' db b 331 | <*> traverse (dualDec' db) ds 332 | 333 | dualStmt' :: DualMappings -> TH.Stmt -> ExceptT (Either TH.Type TH.Exp) Q TH.Stmt 334 | dualStmt' db = \case 335 | TH.BindS p e -> TH.BindS <$> dualPat' db p <*> dualExp' db e 336 | TH.LetS ds -> TH.LetS <$> traverse (dualDec' db) ds 337 | TH.NoBindS e -> TH.NoBindS <$> dualExp' db e 338 | TH.ParS sss -> TH.ParS <$> traverse (traverse (dualStmt' db)) sss 339 | TH.RecS ss -> TH.RecS <$> traverse (dualStmt' db) ss 340 | 341 | handleMissingDual :: ExceptT (Either TH.Type TH.Exp) Q a -> Q a 342 | handleMissingDual = 343 | exceptT 344 | ( fail 345 | . ("no dual for " <>) 346 | . either (("type " <>) . show) (("expression " <>) . show) 347 | ) 348 | pure 349 | 350 | -- | Convert an expression to its dual (i.e., an implementation for the dual 351 | -- of the input expression’s type) 352 | dualExp :: TH.Exp -> Q TH.Exp 353 | dualExp exp = do 354 | duals <- retrieveDuals 355 | handleMissingDual $ dualExp' duals exp 356 | 357 | -- | Indicates that some name represents the dual of itself (e.g., `Functor`). 358 | labelSelfDual :: Name -> Q [a] 359 | labelSelfDual name = do 360 | duals <- retrieveDuals 361 | a <- fromName name 362 | [] 363 | <$ putQ 364 | ( andMaybe 365 | (\t -> duals & dualTypes %~ Map.insert name t) 366 | (\_ e -> duals & dualValues %~ Map.insert name e) 367 | a 368 | ) 369 | 370 | -- | This provides a mapping one way, but not the other. Useful for aliased 371 | -- functions (`return`) and overconstrained versions (e.g., mapping 372 | -- `traverse ↔ distribute` but also `mapM → distribute`). 373 | labelSemiDual :: Name -> Name -> Q [a] 374 | labelSemiDual name coname = do 375 | duals <- retrieveDuals 376 | a <- fromName name 377 | b <- fromName coname 378 | [] <$ case (a, b) of 379 | (Only _, Only t) -> putQ $ duals & dualTypes %~ Map.insert name t 380 | (Indeed _ _, Indeed _ e) -> putQ $ duals & dualValues %~ Map.insert name e 381 | (_, _) -> 382 | fail $ 383 | show name 384 | <> " and " 385 | <> show coname 386 | <> "are not in the same namespace: " 387 | <> show a 388 | <> " " 389 | <> show b 390 | 391 | labelDualDataT :: Name -> Name -> TH.Type -> TH.Type -> Q [a] 392 | labelDualDataT name coname type' cotype' = do 393 | duals <- retrieveDuals 394 | [] <$ putQ (duals & dualTypes %~ (Map.insert coname type' . Map.insert name cotype')) 395 | 396 | addDualExp :: Name -> Name -> TH.Exp -> TH.Exp -> Q DualMappings 397 | addDualExp name coname exp' coexp' = do 398 | duals <- retrieveDuals 399 | pure $ duals & dualValues %~ (Map.insert coname exp' . Map.insert name coexp') 400 | 401 | labelDualExpT :: Name -> Name -> TH.Exp -> TH.Exp -> Q [a] 402 | labelDualExpT name coname exp' coexp' = do 403 | duals <- retrieveDuals 404 | [] <$ putQ (duals & dualValues %~ (Map.insert coname exp' . Map.insert name coexp')) 405 | 406 | -- | Indicate that two names are duals of each other. 407 | labelDual :: Name -> Name -> Q [TH.Dec] 408 | labelDual name coname = do 409 | a <- fromName name 410 | b <- fromName coname 411 | case (a, b) of 412 | (Only a', Only b') -> labelDualDataT name coname a' b' 413 | (Indeed _ a', Indeed _ b') -> labelDualExpT name coname a' b' 414 | (_, _) -> 415 | fail $ 416 | show name 417 | <> " and " 418 | <> show coname 419 | <> "are not in the same namespace: " 420 | <> show a 421 | <> " " 422 | <> show b 423 | 424 | stripForall :: TH.Type -> TH.Type 425 | stripForall (TH.ForallT _ _ t) = t 426 | stripForall t = t 427 | 428 | -- | Given a class, creates a new class that represents its dual, with the list 429 | -- containing name mappings of methods to their duals. 430 | makeDualClass :: Name -> String -> [(Name, String)] -> Q [TH.Dec] 431 | makeDualClass name co methods = do 432 | let coname = mkName co 433 | info <- reify name 434 | type' <- typeFromName name 435 | case info of 436 | TH.ClassI (TH.ClassD ctx _ tVars fds _) _ -> do 437 | ctx' <- nub <$> traverse dualType ctx 438 | meths' <- 439 | traverse 440 | ( sequenceA 441 | . (mkName *** ((dualType . stripForall) <=< typeFromName)) 442 | . swap 443 | ) 444 | methods 445 | (TH.ClassD ctx' coname tVars fds (fmap (uncurry TH.SigD) meths') :) 446 | <$> labelDualDataT name coname type' (TH.ConT coname) 447 | _ -> fail "not a type class" 448 | 449 | makeDualExp :: String -> Q TH.Type -> Q TH.Exp -> String -> Q [TH.Dec] 450 | makeDualExp str type' exp' costr = do 451 | let name = mkName str 452 | coname = mkName costr 453 | 454 | sequenceA 455 | [ TH.SigD name <$> type', 456 | TH.ValD (TH.VarP name) <$> (NormalB <$> exp') <*> pure [], 457 | TH.SigD coname <$> (dualType =<< type'), 458 | TH.ValD (TH.VarP coname) <$> (NormalB <$> (dualExp =<< exp')) <*> pure [] 459 | ] 460 | 461 | -- | Creates a value that can be referenced in other modules to load the duals 462 | -- defined in this module. Should be used at the bottom of any module that 463 | -- uses this module. 464 | exportDuals :: String -> Q [TH.Dec] 465 | exportDuals name = do 466 | typ <- [t|Q DualMappings|] 467 | exp <- reifyDuals =<< retrieveDuals 468 | let name' = mkName name 469 | pure 470 | [ TH.SigD name' typ, 471 | TH.ValD (TH.VarP name') (NormalB exp) [] 472 | ] 473 | 474 | -- | Imports duals from other modules via the var created by `exportDuals` in 475 | -- that other module. 476 | importDuals :: Q DualMappings -> Q [a] 477 | importDuals duals = do 478 | oldDuals <- getQ 479 | newDuals <- duals 480 | [] <$ putQ (maybe newDuals (newDuals <>) oldDuals) 481 | 482 | errorMultipleNewNames :: Name -> Q a 483 | errorMultipleNewNames n = 484 | fail $ "declaration introduces multiple new names: " <> show n 485 | 486 | errorNoNewName :: Q a 487 | errorNoNewName = fail "declaration doesn’t introduce a new name" 488 | 489 | dualCon' :: Map Name TH.Type -> Name -> Con -> ExceptT TH.Type Q Con 490 | dualCon' db coname = \case 491 | NormalC _ bts -> NormalC coname <$> traverse (traverse $ dualType' db) bts 492 | -- TODO: Probably want to dualize field names, too. 493 | RecC _ vbts -> RecC coname <$> traverse (\(a, b, c) -> fmap (a,b,) $ dualType' db c) vbts 494 | InfixC bt _ bt' -> 495 | InfixC 496 | <$> traverse (dualType' db) bt 497 | <*> pure coname 498 | <*> traverse (dualType' db) bt' 499 | ForallC tvbs cx cn -> 500 | ForallC tvbs <$> traverse (dualType' db) cx <*> dualCon' db coname cn 501 | GadtC _ns _bts _t -> undefined -- how do we handle the multiple names here 502 | RecGadtC _ns _vbts _t -> undefined -- and here? 503 | 504 | dualTySynEqn' :: Map Name TH.Type -> TySynEqn -> ExceptT TH.Type Q TySynEqn 505 | dualTySynEqn' db (TySynEqn bs t t') = 506 | TySynEqn bs <$> dualType' db t <*> dualType' db t' 507 | 508 | dualizeDec :: DualMappings -> Name -> TH.Dec -> Q [TH.Dec] 509 | dualizeDec db coname d = 510 | handleMissingDual $ (d :) . pure <$> dualizeDec' db coname d 511 | 512 | dualizeDec' :: 513 | DualMappings -> Name -> TH.Dec -> ExceptT (Either TH.Type TH.Exp) Q TH.Dec 514 | dualizeDec' db coname = \case 515 | TH.FunD n cs -> do 516 | newMap <- lift $ addDualExp n coname (TH.VarE n) (TH.VarE coname) 517 | TH.FunD coname <$> traverse (dualClause' newMap) cs 518 | -- TODO: Handle other vals 519 | TH.ValD (TH.VarP n) b ds -> do 520 | newMap <- lift $ addDualExp n coname (TH.VarE n) (TH.VarE coname) 521 | TH.ValD (TH.VarP coname) 522 | <$> dualBody' newMap b 523 | <*> traverse (dualDec' newMap) ds 524 | TH.ValD {} -> lift errorNoNewName 525 | TH.DataD cx _n tvbs k [cn] dcs -> 526 | withExceptT Left $ 527 | TH.DataD 528 | <$> traverse (dualType' (_dualTypes db)) cx 529 | <*> pure coname 530 | <*> pure tvbs 531 | <*> pure k 532 | <*> ((: []) <$> dualCon' (_dualTypes db) coname cn) 533 | <*> pure dcs -- Should actually dualize this 534 | TH.DataD _ n _ _ _ _ -> lift $ errorMultipleNewNames n 535 | TH.NewtypeD cx _n tvbs k cn dcs -> 536 | withExceptT Left $ 537 | TH.NewtypeD 538 | <$> traverse (dualType' (_dualTypes db)) cx 539 | <*> pure coname 540 | <*> pure tvbs 541 | <*> pure k 542 | <*> dualCon' (_dualTypes db) coname cn 543 | <*> pure dcs -- Should actually dualize this 544 | TH.TySynD _ tvbs t -> 545 | TH.TySynD coname tvbs <$> withExceptT Left (dualType' (_dualTypes db) t) 546 | TH.ClassD {} -> lift errorNoNewName 547 | TH.InstanceD {} -> lift errorNoNewName 548 | TH.SigD _ t -> TH.SigD coname <$> withExceptT Left (dualType' (_dualTypes db) t) 549 | TH.KiSigD {} -> lift errorNoNewName 550 | TH.ForeignD {} -> lift errorNoNewName 551 | TH.InfixD {} -> lift errorNoNewName 552 | TH.PragmaD {} -> lift errorNoNewName 553 | TH.DataFamilyD _ tvbs k -> pure $ TH.DataFamilyD coname tvbs k 554 | TH.DataInstD {} -> lift errorNoNewName 555 | TH.NewtypeInstD {} -> lift errorNoNewName 556 | TH.TySynInstD {} -> lift errorNoNewName 557 | TH.OpenTypeFamilyD (TypeFamilyHead _n tvbs frs ia) -> 558 | pure . TH.OpenTypeFamilyD $ TypeFamilyHead coname tvbs frs ia 559 | TH.ClosedTypeFamilyD (TypeFamilyHead _n tvbs frs ia) tses -> 560 | TH.ClosedTypeFamilyD (TypeFamilyHead coname tvbs frs ia) 561 | <$> withExceptT Left (traverse (dualTySynEqn' $ _dualTypes db) tses) 562 | TH.RoleAnnotD {} -> lift errorNoNewName 563 | TH.StandaloneDerivD {} -> lift errorNoNewName 564 | TH.DefaultSigD {} -> lift errorNoNewName 565 | TH.PatSynD {} -> lift errorNoNewName 566 | TH.PatSynSigD {} -> lift errorNoNewName 567 | TH.ImplicitParamBindD {} -> lift errorNoNewName 568 | #if MIN_VERSION_template_haskell(2, 20, 0) 569 | TH.TypeDataD {} -> lift errorNoNewName 570 | #endif 571 | #if MIN_VERSION_template_haskell(2, 19, 0) 572 | TH.DefaultD {} -> lift errorNoNewName 573 | #endif 574 | 575 | -- | Creates both the original declaration and its dual. Should only work for 576 | -- declarations that introduce exactly one top-level name. 577 | makeDualDec :: Q [TH.Dec] -> String -> Q [TH.Dec] 578 | makeDualDec decs co = do 579 | let coname = mkName co 580 | db <- retrieveDuals 581 | fmap join . traverse (dualizeDec db coname) =<< decs 582 | -------------------------------------------------------------------------------- /resources/duality-boulder-haskell.org: -------------------------------------------------------------------------------- 1 | #+title: Duality and How to Delete Half (minus ε) of Your Code 2 | #+author: Greg Pfeil 3 | #+email: greg@technomadic.org 4 | #+date: Wednesday, 2018 January 10 5 | #+description: There’s a prefix that shows up a lot in Haskell: “co-”. There are “comonads” and “coalgebras” and “covariant functors” … wait a second, that last one means something different than the others. But what, and how? I’ll explain the concept of duality (at least in category theory, you’re on your own for metaphysics) and distinguish it from often confused concepts like variance and isomorphisms. I will also introduce a tool, [[https://github.com/sellout/dualizer][Dualizer]], for programmatically generating dual constructions, which can hopefully both reduce the amount of code you write and give you a new way to explore category theory. 6 | #+options: d:(not LOGBOOK SPEAKERNOTES) 7 | #+drawers: SPEAKERNOTES 8 | #+epresent_frame_level: 3 9 | #+epresent_mode_line: (" @sellout" " Duality and How to Delete Half (minus ε) of Your Code " (:eval (int-to-string epresent-page-number))) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ↓ Twitter slide ↓ 20 | 21 | * Haskell is /not/ Category Theory 22 | 23 | :speakernotes: 24 | Before we get too far, I want to make it clear – Haskell is /not/ category theory. We steal a lot of concepts and they generally apply to things pretty well, but a lot of the relationships are hand-wavy, and as soon as you start trying to implement CT concepts, you run into a few walls. 25 | 26 | For example, 27 | :END: 28 | 29 | #+begin_src haskell 30 | class Functor f where 31 | fmap :: (a -> b) -> f a -> f b 32 | #+end_src 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ** generalizing ~Functor~ 48 | 49 | :speakernotes: 50 | isn’t really an arbitrary “functor”, but rather an endofunctor in the “category” of Hask (where “Hask” means “Haskell types”, and is Hask even a category? It’s hotly debated.) 51 | 52 | We /can/ move things closer to CT, perhaps with 53 | :END: 54 | 55 | #+begin_src haskell 56 | type family Cat k :: k -> k -> * 57 | type instance Cat Type = (->) 58 | 59 | class (Category (Cat j), Category (Cat k)) => 60 | Functor (f :: j -> k) where 61 | fmap :: Cat j a b -> Cat k (f a) (f b) 62 | 63 | newtype Natural (cat :: k -> k -> *) 64 | (f :: j -> k) 65 | (g :: j -> k) = 66 | Natural { runNatural :: forall x. cat (f x) (g x) } 67 | 68 | type instance Cat (j -> k) = Natural (Cat k) 69 | #+end_src 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | *** Wait … what’s a category? 79 | 80 | - objects 81 | - arrows 82 | 83 | “Hask” 84 | - ~Int -> Char~ 85 | 86 | “A monad is just a monoid in /the category of endofunctors/.” 87 | #+begin_src haskell 88 | mempty :: Id ~> F -- pure 89 | mappend :: (F ∘ F) ~> F -- join 90 | #+end_src 91 | 92 | So is an ~Applicative~! 93 | #+begin_src haskell 94 | mempty :: Id ~> F -- pure 95 | mappend :: (F, F) ~> F -- ap 96 | #+end_src 97 | 98 | ** generalizing ~Functor~ 99 | 100 | :speakernotes: 101 | isn’t really an arbitrary “functor”, but rather an endofunctor in the “category” of Hask (where “Hask” means “Haskell types”, and is Hask even a category? It’s hotly debated.) 102 | 103 | We /can/ move things closer to CT, perhaps with 104 | :END: 105 | 106 | #+begin_src haskell 107 | type family Cat k :: k -> k -> * 108 | type instance Cat Type = (->) 109 | 110 | class (Category (Cat j), Category (Cat k)) => 111 | Functor (f :: j -> k) where 112 | fmap :: Cat j a b -> Cat k (f a) (f b) 113 | 114 | newtype Natural (cat :: k -> k -> *) 115 | (f :: j -> k) 116 | (g :: j -> k) = 117 | Natural { runNatural :: forall x. cat (f x) (g x) } 118 | 119 | type instance Cat (j -> k) = Natural (Cat k) 120 | #+end_src 121 | :speakernotes: 122 | so now we have a “true” functor between categories ~j~ and ~k~ where ~Cat~ represents the type of the arrows in the category. To get back the traditional ~Functor~, both categories are ~*~, which means the arrowss are ~->~. Oh, don’t worry, this is going to come back into play with duality … 123 | 124 | Duality definitely falls into that trap – and I’m not going to distinguish things too much in this talk. I’m going to be talking about CT and showing Haskell, and only sometimes going to point out where they don’t align so well. 125 | 126 | But fast & loose reasoning is morally correct … right? 127 | :END: 128 | ** specializing ~Functor~ 129 | 130 | #+begin_src haskell 131 | class (Category (Cat j), Category (Cat k)) => 132 | Functor (f :: j -> k) where 133 | fmap :: Cat j a b -> Cat k (f a) (f b) 134 | #+end_src 135 | 136 | #+begin_src haskell 137 | class (Category (->), Category (->)) => 138 | Functor (f :: Type -> Type) where 139 | fmap :: (->) a b -> (->) (f a) (f b) 140 | #+end_src 141 | 142 | 143 | * So, what’s duality? 144 | 145 | :speakernotes: 146 | A little CT to start with: for any category /C/, there is an opposite (or dual) category C^op with all the arrows reversed (but the same objects). It follows from reversing the arrows again, that the dual category of C^op is C. 147 | 148 | So far, so easy, right? That’s fundamentally all there is to it, but there are some complications 149 | :END: 150 | 151 | - C 152 | - C ↦ C^op 153 | - (C^op)^op = C 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | ** a tuple 168 | 169 | :speakernotes: 170 | #+begin_src dot :file product-haskell.png 171 | digraph { 172 | "(a, b)" -> a [label="fst"] 173 | "(a, b)" -> b [label="snd"] 174 | } 175 | #+end_src 176 | :END: 177 | 178 | #+RESULTS: 179 | [[file:product-haskell.png]] 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | *** What’s the dual? 197 | 198 | :speakernotes: 199 | #+begin_src dot :file unknown-haskell.png 200 | digraph { 201 | rankdir=BT 202 | a -> "?" 203 | b -> "?" 204 | } 205 | #+end_src 206 | :END: 207 | 208 | #+RESULTS: 209 | [[file:unknown-haskell.png]] 210 | 211 | *** Either! 212 | 213 | :speakernotes: 214 | #+begin_src dot :file coproduct-haskell.png 215 | digraph { 216 | rankdir=BT 217 | a -> "Either a b" [label="Left"] 218 | b -> "Either a b" [label="Right"] 219 | } 220 | #+end_src 221 | :END: 222 | 223 | #+RESULTS: 224 | [[file:coproduct-haskell.png]] 225 | 226 | ** What does it mean to “reverse the arrows”? 227 | 228 | :speakernotes: 229 | This is when we start to get into practical complications. Let’s look at a simple case ~a -> b~. An arrow is /sort of/ another name for function, so the dual of ~a -> b~ is ~b -> a~. Ok, great. Now let’s look at something a bit more complicated: 230 | :END: 231 | 232 | #+begin_src haskell 233 | class Functor f where 234 | fmap :: (a -> b) -> f a -> f b 235 | #+end_src 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | *** Reversed! 252 | 253 | :speakernotes: 254 | What? Didn’t we just look at this? Yeah, ok, but it was for a different reason … sort of. So let’s reverse the arrows of ~Functor~. 255 | :END: 256 | 257 | #+begin_src haskell 258 | class Functor f where 259 | fmap :: (a -> b) -> f a -> f b 260 | 261 | 262 | cofmap :: (a <- b) <- f a <- f b 263 | 264 | cofmap :: f b -> f a -> (b -> a) 265 | #+end_src 266 | 267 | :speakernotes: 268 | So, something isn’t right here – there’s no way you can have a function that takes an ~f b~ and and ~f a~ and returns a function of ~b -> a~. It turns out the arrows are /different/. But let’s go back to our “categorical” ~Functor~ from earlier: 269 | :END: 270 | 271 | 272 | *** Categorical 273 | 274 | #+begin_src haskell 275 | class (Category (Cat j), Category (Cat k)) => 276 | Functor (f :: j -> k) where 277 | fmap :: Cat j a b -> Cat k (f a) (f b) 278 | #+end_src 279 | 280 | :speakernotes: 281 | With this definition we can now talk about ~Cat j a b~ “a arrow in category j from a to b”. Well, clearly in this case “reversing the arrows” is obvious: 282 | :END: 283 | 284 | #+begin_src haskell 285 | cofmap :: Cat j b a -> Cat k (f b) (f a) 286 | #+end_src 287 | 288 | #+begin_src haskell 289 | -- Cat Type = (->) 290 | cofmap :: (b -> a) -> f b -> f a 291 | #+end_src 292 | 293 | :speakernotes: 294 | And this leads to another revelation – Functors are self-dual. You can see that just swapping the variables ~a~ and ~b~ gives us a ~cofmap~ that is equvalent to the original ~fmap~. 295 | :END: 296 | 297 | *** Takeaways 298 | 299 | - The objects of a category and its dual are the same 300 | - The arrows of a category ond its dual are the same, but reversed 301 | - ∴ ~Cat j~ is sort-of its own dual, and ~Cat j a b~ is dual to ~Cat j b a~ 302 | - ∴ ~Functor~ is its own dual and ~fmap~ is its own dual 303 | 304 | #+begin_src haskell 305 | type Op c a b = Cat c b a 306 | #+end_src 307 | 308 | :speakernotes: 309 | which lets us refer to the opposite category, and now we can say that the dual of ~Cat~ is ~Op~ … which is great as long as we’re dealing with ~Cat~, but we rarely get to do that in production Haskell code, right? I mean, it kills me, but it’s true. 310 | This is something we end up doing a lot – generalizing some Haskell thing to it’s CT equivalent, figuring out the duals, and then applying them back to the Haskell thing. 311 | :END: 312 | 313 | 314 | 315 | ** Other notions of duality 316 | 317 | :speakernotes: 318 | There are many different notions of duality throughout mathematics, and they don’t necessarily line up. 319 | 320 | E.g., there is a notion of “dual vector spaces”, but then you talk about “double duals“, which are the dual of the dual. In catgegory theory, that’d just be the original category. 321 | :END: 322 | 323 | - set complements 324 | - dual vector space 325 | 326 | ** Notions similar to duality 327 | 328 | *** Variance 329 | 330 | :speakernotes: 331 | One expectation that people often have is that the dual of `Functor` (aka, a “covariant” functor) must be a contravariant functor. 332 | :END: 333 | 334 | 335 | #+begin_src haskell 336 | class Functor f where 337 | fmap :: (a -> b) -> f a -> f b 338 | 339 | class Contravariant f where 340 | contramap :: (b -> a) -> f a -> f b 341 | #+end_src 342 | 343 | :speakernotes: 344 | In other languages you may see variance in different contexts – in Scala, you see it on type parameters, like ~Foo[+A]~ or ~Foo[-A]~ this is the same concept. 345 | 346 | You can see here that the mapping function has been reversed. But to be a dual, what else needs to happen? That’s right – the resulting function also needs to be in its opposite category … which would give us back a regular /covariant/ ~Functor~ again. 347 | 348 | Now … what happens if we do take the dual of ~Contravariant~? 349 | :END: 350 | 351 | 352 | #+begin_src haskell 353 | class Cocontravariant f where 354 | cocontramap :: (a -> b) -> f b -> f a 355 | #+end_src 356 | 357 | :speakernotes: 358 | We’re right back where we started – so ~Contravariant~ is also self-dual. 359 | 360 | How about /invariant/ functors? 361 | :END: 362 | 363 | 364 | #+begin_src haskell 365 | class Invariant f where 366 | imap :: (a -> b) -> (b -> a) -> f a -> f b 367 | 368 | class Coinvariant f where 369 | coimap :: (b -> a) -> (a -> b) -> f b -> f a 370 | #+end_src 371 | 372 | :speakernotes: 373 | Ok, perhaps you’re unsurprised at this point. If so, great! That means you’re getting a bit of a sense of this. 374 | 375 | Looking at this, we can see that variance is independent of duality. 376 | :END: 377 | 378 | . 379 | 380 | *** Isomorphisms 381 | 382 | #+begin_src haskell 383 | spotCheckIsomorphism 384 | :: (a -> b) -> (b -> a) -> a -> b -> Bool 385 | spotCheckIsomorphism f g a b = 386 | g (f a) == a && f (g b) == b 387 | #+end_src 388 | 389 | :speakernotes: 390 | ~Dual~ itself is an isomorphism, and many duals do admit isomorphisms in special cases, but duals aren’t necessarily isomorphic. E.g, ~snd~ and ~Right~ 391 | :END: 392 | 393 | 394 | #+begin_src haskell 395 | snd :: (a, b) -> b 396 | right :: b -> Either a b 397 | 398 | tToE :: (a, b) -> Either a b 399 | tToE = right . snd 400 | 401 | eToT :: _ 402 | eToT = snd . right -- 🚫 403 | #+end_src 404 | 405 | :speakernotes: 406 | You can compose them in one direction, but you can’t even compose them the other way, let alone expect to get the original value back. 407 | :END: 408 | 409 | . 410 | *** Adjunctions 411 | 412 | :speakernotes: 413 | Adjunctions are a somewhat advanced topic, but there are a few things in them that sometimes get confused with duals. In fact, I made the mistake myself just a week ago. 414 | :END: 415 | 416 | 417 | #+begin_src meh 418 | L ⊣ R 419 | #+end_src 420 | 421 | :speakernotes: 422 | ~f~ is /left adjoint/ to ~g~ and ~g~ is right adjoint to ~f~. These may be, but aren’t necessarily, duals. Beyond that, any adjunction induces both a monad (~g · f~) and a comonad (~f · g~). I don’t think there’s a name for the relationship between the monad and comonad, but they are often clearly related (e.g., ~State~ and ~Store~). 423 | 424 | In particular, people often think of ~Writer~ and ~Reader~ as dual, but they really form an adjunction (well, sort of). The dual of the structure underlying ~Writer~ (a tuple) is ~Either~. 425 | 426 | #+begin_src dot :file right.png 427 | digraph { 428 | subgraph "clusterC" { 429 | label="C" 430 | c 431 | } 432 | subgraph "clusterD" { 433 | label="D" 434 | d 435 | "d'" 436 | } 437 | 438 | c -> "d'" [label="R"] 439 | d -> "d'" [label="η"] 440 | d -> c [label="L"] 441 | } 442 | #+end_src 443 | :END: 444 | 445 | [[file:right.png]] 446 | 447 | :speakernotes: 448 | #+begin_src dot :file left.png 449 | digraph { 450 | subgraph "clusterC" { 451 | label="C" 452 | c 453 | "c'" 454 | } 455 | subgraph "clusterD" { 456 | label="D" 457 | d 458 | } 459 | c -> d [label="R"] 460 | d -> "c'" [label="L"] 461 | "c'" -> c [label="ε"] 462 | } 463 | #+end_src 464 | :END: 465 | 466 | [[file:left.png]] 467 | 468 | 469 | * Is it useful to know the dual of something? 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | :speakernotes: 486 | That is a /really/ good question. If we know the dual of something, what can we do with it? Not much, really. 487 | 488 | However, it is useful to be able to /construct/ the dual of something. There are non-zero instances where I have “simply” reversed the arrows for something and discovered some other structure that was also useful to me. 489 | 490 | This actually happened last weekend. On Friday, [[https://www.reddit.com/r/haskell/comments/7oav51/i_made_a_monad_that_i_havent_seen_before_and_i/][someone discovered a new monad]]. Without getting into the details, if you constrain the ~Store~ comonad with a monoid, you can get a monad instance out of it. That’s already pretty cool. But then Ed Kmett said “if you can get a monad from ~Store~, then you can probably get a comonad (the dual of a monad) from ~State~ (the dual of ~Store~).” Then [[http://comonad.com/reader/2018/the-state-comonad/][he did it]]. And he noticed that this was already something he was using concretely: ~extract = flip evalState mempty~ is something that comes up a lot when dealing with state already, but Ed also had a more constrained comonad defined for streaming. 491 | 492 | As an aside, Kmett has a ~newtype Co w a~ that converts any comonad into its dual monad, but the reverse isn’t possible within Haskell, so he had to manually construct ~Comonad State~ and from there can /hopefully/ verify that ~Co (State s)~ is equivalent to ~Store s~. 493 | 494 | And, it turns out that even though /Haskell isn’t category theory/, we can still encode the rules of duality well enough to have Haskell do the work of finding a dual for us (at least in a lot of cases). Which means we can now just /ask/ for the dual of something, and Haskell will tell us. And then we have a new structure … which /may/ be useful. 495 | 496 | Not to mention that if you’re working in a domain that deals with duals a lot (as I do, with recursion schemes), you can avoid writing a lot of code in this way. 497 | :END: 498 | 499 | *** So, Store is a comonad 500 | 501 | :speakernotes: 502 | If you’re not familiar with ~Store~, it’s basically a place in a larger structure, you can see the current value, and you can provide a new value, getting the updated larger structure as a result. The most well-known instance of this is probably lens, where ~type Lens' a s = a -> Store s a~. 503 | :END: 504 | 505 | 506 | #+begin_src haskell 507 | newtype Store s a = Store (s, s -> a) 508 | 509 | 510 | instance Comonad (Store s) where 511 | extract (Store (s, f)) = f s 512 | #+end_src 513 | 514 | *** Someone figured out a monad for Store 515 | 516 | #+begin_src haskell 517 | newtype Store s a = Store (s, s -> a) 518 | 519 | 520 | instance Comonad (Store s) where 521 | extract (Store (s, f)) = f s 522 | 523 | 524 | 525 | 526 | instance Monoid s => Monad (Store s) where 527 | pure a = Store (mempty, const a) 528 | #+end_src 529 | 530 | *** State is the dual of Store 531 | 532 | #+begin_src haskell 533 | newtype Store s a = Store (s, s -> a) 534 | newtype State s a = State (s -> (s, a)) 535 | 536 | instance Comonad (Store s) where 537 | extract (Store (s, f)) = f s 538 | 539 | instance Monad (State s) where 540 | pure a = (\s -> (s, a)) 541 | 542 | instance Monoid s => Monad (Store s) where 543 | pure a = Store (mempty, const a) 544 | #+end_src 545 | 546 | *** And the duals! 547 | 548 | #+begin_src haskell 549 | newtype Store s a = Store (s, s -> a) 550 | newtype State s a = State (s -> (s, a)) 551 | 552 | instance Comonad (Store s) where 553 | extract (Store (s, f)) = f s 554 | 555 | instance Monad (State s) where 556 | pure a = (\s -> (s, a)) 557 | 558 | instance Monoid s => Monad (Store s) where 559 | pure a = Store (mempty, const a) 560 | 561 | instance Monoid s => Comonad (State s) where 562 | extract (State f) = fst $ f mempty 563 | #+end_src 564 | 565 | * → Dualizer ← 566 | 567 | :speakernotes: 568 | Dualizer is a tool I wrote to help myself 569 | 1. understand duality a bit better (and it has certainly uncovered some unknown unknowns for me) and 570 | 2. have an excuse to finally learn Template Haskell. 571 | 572 | As a result of #2, I feel like the ergonomics of the library are really lacking, but we can at least see what it does, and hopefully the ergonomics can be improved down the line. 573 | :END: 574 | 575 | 1. duality 576 | 2. Template Haskell 577 | 3. recursion schemes 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | ** Types 593 | 594 | :speakernotes: 595 | The most direct thing you can do is ask for the dual of a type: 596 | :END: 597 | 598 | 599 | #+begin_src haskell 600 | testT :: $(dualType =<< [t|Either Int Char|]) 601 | testT = (3, 'a') 602 | #+end_src 603 | 604 | :speakernotes: 605 | Yes, this compiles. Just as we talked about earlier, the dual of Either is a tuple. And we can do the opposite, too, since C^op^op 606 | :END: 607 | 608 | 609 | #+begin_src haskell 610 | testE :: $(dualType =<< [t|forall a. (Int, a)|]) 611 | testE = Left 3 612 | #+end_src 613 | 614 | #+begin_src haskell 615 | testT :: Dual Either Int Char -- fragile 616 | testE :: Dual (forall a. (Int, a)) -- 🚫 617 | #+end_src 618 | 619 | 620 | ** Expressions 621 | 622 | #+begin_src haskell 623 | snd :: (a, b) -> b 624 | snd = $(dualExp =<< [e|Right|]) 625 | #+end_src 626 | 627 | #+begin_src haskell 628 | right :: b -> Either a b 629 | right = $(dualExp =<< [e|snd|]) 630 | #+end_src 631 | 632 | ** Ergonomics 633 | 634 | :speakernotes: 635 | I’m going to jump right to the fun part here. 636 | :END: 637 | 638 | 639 | #+begin_src haskell 640 | data Fix f = Fix { project :: f (Fix f) } 641 | labelSelfDual ''Fix -- Not actually true 642 | labelDual 'Fix 'project 643 | 644 | makeDualDec 645 | [|d cata :: (f a -> a) -> Fix f -> a 646 | cata f = f . fmap (cata f) . project |] 647 | "ana" 648 | #+end_src 649 | 650 | :speakernotes: 651 | So, we declare a new data type representing the fixed-point of functors. We don’t need to care about the specific data type so much, other than to note that it is its own dual, and that the constructor for it is dual to its sole accessor. Then we define a function that uses this type, wrapping it in a bit of magic that says “also define the dual of this function, and give it the name ~ana~.” The generated dual looks like: 652 | :END: 653 | 654 | 655 | #+begin_src haskell 656 | ana :: (a -> f a) -> a -> Fix f 657 | ana f = Fix . fmap (ana f) . f 658 | #+end_src 659 | 660 | :speakernotes: 661 | So, let’s walk through this a bit … the first argument is a function (and the dual of a function just inverts the arrows, as we’ve seen). And then we hit the same issue we did with ~Functor~ – rather than this being a function of two arguments, it’s really a function that takes a function and returns a function. So, we flip the arrow of the returned function as well. So, we now have the correct type for ~ana~. 662 | 663 | Now, the value … first, the dual of ~f . g~ is ~g . f~, so all the compositions should be flipped, which means we compare ~project~ to ~Fix~, ~fmap~ to ~fmap~, and ~f~ to ~f~. We’ve just declared that ~Fix~ and ~project~ are duals, so that works. Variables are sort of self-dual (their types have been dualized, so it really represents a different expression), so ~f~ works as well. Finally, we have ~fmap~. We know that that is self-dual already, but we have to take the dual of the function we pass to it, and as we’ve just defined here, ~ana~ is (going to be) the dual of ~cata~, so we swap that in there, and great – it all checks out! 664 | :END: 665 | 666 | ** A lot of stuff already works … 667 | 668 | #+begin_src haskell 669 | makeDualDec [d|type Algebra f a = f a -> a|] "Coalgebra" 670 | 671 | makeDualDec [d|newtype Either' a b = Either' (Either a b)|] 672 | "Tuple'" 673 | makeDualDec [d|data Either'' a b = Either'' (Either a b)|] 674 | "Tuple''" 675 | 676 | makeDualDec [d|data family TestA a|] "DualA" 677 | makeDualDec [d|type family TestB a|] "DualB" 678 | makeDualDec 679 | [d| type family TestC a where 680 | TestC (Either b c) = b 681 | TestC Int = Char |] 682 | "DualC" 683 | #+end_src 684 | * More about Adjunctions 685 | 686 | :speakernotes: 687 | This is only tangentially about duality, but adjunctions are /really/ cool. First, if there exists an adjunction between two functors, 688 | :END: 689 | 690 | #+begin_src haskell 691 | instance f ⊣ g => Monad (g ∘ f) 692 | instance f ⊣ g => Comonad (f ∘ g) 693 | #+end_src 694 | 695 | :speakernotes: 696 | And /all/ monads and comonads can be decomposed into an adjoint pair. 697 | 698 | Then there are adjoint triples (and quadruples …) which similarly give adjoint pairs of monads and comonads. 699 | :END: 700 | 701 | ** triples, quadruples, … 702 | 703 | #+begin_src haskell 704 | -- f ⊣ g ⊣ h 705 | -- (m) gf ⊣ gh (co) 706 | -- (co) fg ⊣ hg (m) -- this one can be even _more_ general 707 | instance (f ⊣ g, g ⊣ h) => (g ∘ f) ⊣ (g ∘ h) 708 | instance (f ⊣ g, h ⊣ i) => (f ∘ h) ⊣ (i ∘ g) 709 | #+end_src haskell 710 | 711 | :speakernotes: 712 | You can see how this works with some specific examples. 713 | :END: 714 | 715 | ** a concrete example 716 | 717 | #+begin_src haskell 718 | (a, _) ⊣ (a -> _) -- (almost) Writer a ⊣ Reader a 719 | a -> (a, _) -- State a 720 | (a, a -> _) -- Store a 721 | #+end_src 722 | 723 | :speakernotes: 724 | And since the initial adjunction there is a Comonad/Monad pair, it means that they can further be broken down into an adjoint triple. Unfortunately, that level isn’t easily representable in Haskell, because they’re not ~Functor~, but we could probably use the same ~Cat~ trick to get them. 725 | 726 | Another simple example. 727 | :END: 728 | 729 | #+begin_src haskell 730 | (_ -> a) ⊣ (_ -> a) -- Contravariant, not Functor 731 | (_ -> a) -> a -- Cont 732 | (_ -> a) -> a -- also Cont 733 | #+end_src 734 | 735 | :speakernotes: 736 | #+begin_src haskell 737 | data Δ :: c -> (c, c) 738 | 739 | Σ ⊣ Δ ⊣ Π 740 | Env A ⊣ Reader A (can we generalize these to polykinds, rather than also 741 | Store A having some transformer version?) 742 | State A 743 | Possibility ⊣ Necessity 744 | 745 | Writer A ⊣ Traced A (like above, but with a monoid on `A`) 746 | (A, A -> _) 747 | A -> (A, _) 748 | 749 | (_ -> A) ⊣ (_ -> A) 750 | Cont 751 | Cont 752 | 753 | Free F ⊣ Cofree G (where F ⊣ G) … is this right? Seems like there should be a U of some sort in the middle. 754 | 755 | F ⊣ U ⊣ C (coyoneda / forgetful / yoneda) 756 | Id ⊣ Id 757 | Id 758 | Id 759 | 760 | −×P ⊣ (−)^P 761 | 762 | -- These overlap in self-dual cases, like `Id` and `Cont`. 763 | type (Monad f, Comonad g, f ⊣ g) => Dual f = g 764 | type (Comonad f, Monad g, f ⊣ g) => Dual g = f 765 | 766 | 767 | π0 ⊣ D ⊣ U ⊣ A 768 | 769 | Lan L ⊣ (−)∘L ⊣ (−)∘R ⊣ Ran R (where L ⊣ R) 770 | 771 | F ⊣ G ⊣ H ⊣ I 772 | (F ⊣ G) ⊣ (G ⊣ H) ⊣ (H ⊣ I) 773 | ((F ⊣ G) ⊣ (G ⊣ H)) ⊣ ((G ⊣ H) ⊣ (H ⊣ I)) 774 | 775 | 776 | ((A, _) -> B) ⊣ (A -> _ -> B) 777 | (A, A -> _ -> B) -> B 778 | 779 | A -> ((A, _) -> B) -> B 780 | pure x = My 781 | 782 | (A, _) \/ (_ -> B) ⊣ (A -> _, _ -> B) 783 | (A, (A -> _, _ -> B)) \/ ((A -> _, _ -> B) -> B) 784 | copure (Left x) = fst x 785 | copure (Right x) = ??? 786 | (A -> ((A, _) \/ _ -> B), ((A, _) \/ (_ -> B)) -> B) 787 | pure = x -> (\a -> Left (a, x), ???) 788 | #+end_src 789 | :END: 790 | 791 | * Questions? 792 | 793 | - https://github.com/sellout/dualizer 794 | - greg@technomadic.org 795 | 796 | - come work at [[https://takt.com/][Takt]] (we don’t use dualizer … yet) 797 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------