packages feed

autoapply (empty) → 0.1.0.0

raw patch · 8 files changed

+572/−0 lines, 8 filesdep +basedep +doctestdep +logictbuild-type:Customsetup-changed

Dependencies added: base, doctest, logict, mtl, template-haskell, th-desugar, transformers, unification-fd

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Joe Hermaszewski (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Joe Hermaszewski nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main where++import Distribution.Extra.Doctest (defaultMainWithDoctests)++main :: IO ()+main = defaultMainWithDoctests "doctests"
+ autoapply.cabal view
@@ -0,0 +1,67 @@+cabal-version: 1.24++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 7d16c8a0b05df49d6afc85393edc6b82c2482761dbc1c85a66ec61675dba7497++name:           autoapply+version:        0.1.0.0+synopsis:       Template Haskell to automatically pass values to functions+description:    See readme.md+category:       Template Haskell+homepage:       https://github.com/expipiplus1/autoapply#readme+bug-reports:    https://github.com/expipiplus1/autoapply/issues+maintainer:     Joe Hermaszewski <if.it.fits.i.sits@monoid.al>+copyright:      (c) 2020 Joe Hermaszewski+license:        BSD3+license-file:   LICENSE+build-type:     Custom+extra-source-files:+    readme.md+    default.nix+    changelog.md++source-repository head+  type: git+  location: https://github.com/expipiplus1/autoapply++custom-setup+  setup-depends:+      Cabal+    , base+    , cabal-doctest >=1 && <1.1++library+  exposed-modules:+      AutoApply+  other-modules:+      Paths_autoapply+  hs-source-dirs:+      src+  default-extensions: DeriveFoldable DeriveFunctor DeriveTraversable DerivingStrategies FlexibleContexts KindSignatures LambdaCase PatternSynonyms RankNTypes RecordWildCards ScopedTypeVariables TemplateHaskellQuotes TupleSections TypeFamilies ViewPatterns+  ghc-options: -Wall+  build-depends:+      base >=4.13 && <5+    , logict+    , mtl+    , template-haskell+    , th-desugar >=1.10+    , transformers+    , unification-fd+  default-language: Haskell2010++test-suite doctests+  type: exitcode-stdio-1.0+  main-is: Doctests.hs+  other-modules:+      +  hs-source-dirs:+      test+  default-extensions: DeriveFoldable DeriveFunctor DeriveTraversable DerivingStrategies FlexibleContexts KindSignatures LambdaCase PatternSynonyms RankNTypes RecordWildCards ScopedTypeVariables TemplateHaskellQuotes TupleSections TypeFamilies ViewPatterns+  ghc-options: -Wall+  build-depends:+      base+    , doctest+  default-language: Haskell2010
+ changelog.md view
@@ -0,0 +1,7 @@+# Change Log++## WIP++## [0.1.0.0] - 2020-04-26+  - Initial release+  - `autoapply` and `autoapplyDecs`
+ default.nix view
@@ -0,0 +1,40 @@+{ pkgs ? import <nixpkgs> { }, compiler ? "ghc882", hoogle ? true }:++let+  src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;++  compiler' = if compiler != null then+    compiler+  else+    "ghc" + pkgs.lib.concatStrings+    (pkgs.lib.splitVersion pkgs.haskellPackages.ghc.version);++  # Any overrides we require to the specified haskell package set+  haskellPackages = with pkgs.haskell.lib;+    pkgs.haskell.packages.${compiler'}.override {+      overrides = self: super:+        { } // pkgs.lib.optionalAttrs hoogle {+          ghc = super.ghc // { withPackages = super.ghc.withHoogle; };+          ghcWithPackages = self.ghc.withPackages;+        };+    };++  # Any packages to appear in the environment provisioned by nix-shell+  extraEnvPackages = with haskellPackages; [ ];++  # Generate a haskell derivation using the cabal2nix tool on `package.yaml`+  drv = let old = haskellPackages.callCabal2nix "" src { };+  in old // {+    # Insert the extra environment packages into the environment generated by+    # cabal2nix+    env = pkgs.lib.overrideDerivation old.env (attrs:+      {+        buildInputs = attrs.buildInputs ++ extraEnvPackages;+      } // pkgs.lib.optionalAttrs hoogle {+        shellHook = attrs.shellHook + ''+          export HIE_HOOGLE_DATABASE="$(cat $(${pkgs.which}/bin/which hoogle) | sed -n -e 's|.*--database \(.*\.hoo\).*|\1|p')"+        '';+      });+  };++in if pkgs.lib.inNixShell then drv.env else drv
+ readme.md view
@@ -0,0 +1,141 @@+# autoapply++A Template-Haskell program to automatically pass arguments to functions+wherever the type fits.++## TL;DR++You have the following values and want to stir them together and see what+sticks.++- `foo :: Monad m => A -> B -> C -> m D`+- `getA :: App A`+- `myC :: C`++`$(autoApply ['getA, 'myC] 'foo)` will create+`\b -> getA >>= \a -> foo a b myC` which has type `B -> App D`++or++`autoApplyDecs reverse ['getA, 'myC] ['foo]` will create+`oof :: B -> App D; oof b = do { a <- getA; foo a b myC }`++## Why to use it++One nice use-case is to avoiding writing boilerplate wrappers for using an API+in your Monad stack. For instance imagine the following API.++```haskell+data Instance; data ExtraOpenInfo; data Foo; data Bar; data Handle+openHandle  :: MonadIO m => Instance -> Maybe ExtraOpenInfo -> m Handle+closeHandle :: MonadIO m => Instance -> Handle -> m ()+useHandle   :: MonadIO m => Instance -> Handle -> Foo -> m Bar+```++You'd like to use this in your `polysemy` application, using the `Input` effect+to pass the `Instance` handle around, and always passing `Nothing` for+`ExtraOpenInfo` because you don't use that functionality and getting a `Foo`+from some other constraint `MyConstraint`. You define the following values.++```haskell+myExtraOpenInfo :: Maybe ExtraOpenInfo+myExtraOpenInfo = Nothing+getInstance :: Member (Input Instance) r => Sem r Instance+getInstance = input+getFoo :: MyConstraint m => m Foo+getFoo = ...+```++You then create the wrapped API thusly:++```haskell+autoapplyDecs+  (<> "'") -- Function to transform the names of the wrapped functions+  ['myExtraOpenInfo, 'getInstance, 'getFoo] -- Potential arguments to pass+  ['openHandle, 'closeHandle, 'useHandle] -- Functions to wrap+```++Which creates the following declarations:++```haskell+openHandle'+  :: (Member (Input Instance) r, MonadIO (Sem r)) => Sem r Handle+closeHandle'+  :: (Member (Input Instance) r, MonadIO (Sem r)) => Handle -> Sem r ()+useHandle'+  :: (Member (Input Instance) r, MyConstraint (Sem r), MonadIO (Sem r))+  => Handle -> Sem r Bar+```++Notice:+- `Instance` is supplied with the `Member (Input Instance) r` constraint+- `Foo` is supplied by `MyConstraint (Sem r)`+- `ExtraOpenInfo` is not present at all, being supplied internally by `myExtraOpenInfo`++To see the generated code (it's exactly what you'd expect) compile+`test/Types.hs` with `-ddump-splices`.++## How to use this++To generate a new top-level declaration you'll need:++- The `Name` of a function to apply to some arguments.+- The `Name`s of some values to try and pass as arguments.+- A way of generating a name for this declaration given the wrapped name+  `:: String -> String`.++The new declaration will be generated, equal to the wrapped one but using the+supplied arguments wherever possible.++Arguments can be used in two ways:++- As regular parameters+  - If the type of the argument matches directly+  - An example is applying `takeWhile` to `not`; `not` is passed as the `a -> Bool`+    argument to `takeWhile`. `$(autoapply ['not] 'takeWhile) :: [Bool] -> [Bool]`++- Using a monadic bind+  - If the wrapped function returns a value of type `m a` and there exists an instance `Monad m`+  - If the argument is of type `n a` and there exists an instance `Monad m`+  - If `m` unifies with `n`+  - An example is applying `putStrLn` to `getLine`. The `String` result of `getLine` is passed to `putStrLn`+    `$(autoapply ['getLine] 'putStrLn) :: IO ()`++It's important to note that `Monad` instance checking only goes as far as+`template-haskell`'s `reifyInstances`. i.e. only the instance heads are+checked.++Aside for checking for a `Monad` instance, no constraints are checked. So `autoapply`+will happily pass `reverse` to `(+)` yielding a value of type `Num ([a] -> [a]) => [a] -> [a]`.++Monadic binds are performed in the order of arguments passed to the wrapped+function, and will be performed more than once if the argument is used multiple+times.++You may want to either type your generated declarations manually (putting the+type after the splice) or turn on `-XNoMonomorphismRestriction` if your+arguments have polymorphic constraints.++## Where to use it++- In an expression context:+  - `$(autoApply ['my, 'arguments] 'myFunction)`++- At the top level to generate several declarations+  - `$(autoApplyDecs (funNameToNewFunName :: String -> String) ['my, 'arguments] ['myFunction, 'anotherFunction])`++## See also++This has a similar feel to some other programs which also generate Haskell+expressions based on types.++- [djinn](https://hackage.haskell.org/package/djinn)+- [exference](http://hackage.haskell.org/package/exference) ([github](https://github.com/lspitzner/exference))+- [JustDoIt](https://www.joachim-breitner.de/blog/735-The_magic_%E2%80%9CJust_do_it%E2%80%9D_type_class)++There are a couple of differences here:++- One doesn't need to specify the desired type up front, this tool will just go+  as far as it can.+- This tool isn't doing any interesting proof search instead it's just "if it+  fits, I sits"
+ src/AutoApply.hs view
@@ -0,0 +1,274 @@+module AutoApply+  ( autoapply+  , autoapplyDecs+  ) where++import           Control.Applicative+import           Control.Arrow                  ( (>>>) )+import           Control.Monad+import           Control.Monad.Logic            ( LogicT+                                                , observeManyT+                                                )+import           Control.Monad.Logic.Class      ( ifte )+import           Control.Monad.Trans           as T+import           Control.Monad.Trans.Except+import           Control.Unification+import           Control.Unification.IntVar+import           Control.Unification.Types+import           Data.Foldable+import           Data.Functor+import           Data.Functor.Fixedpoint+import           Data.Maybe+import           Data.Traversable+import           Language.Haskell.TH+import           Language.Haskell.TH.Desugar++-- | @autoapply args fun@ creates an expression which is equal to @fun@ applied+-- to as many of the values in @args@ as possible.+autoapply :: [Name] -> Name -> Q Exp+autoapply givens fun = do+  givenInfos <- for givens $ fmap (uncurry Given) . reifyVal "Argument"+  funInfo    <- uncurry Function <$> reifyVal "Function" fun+  autoapply1 givenInfos funInfo++-- | @autoapplyDecs mkName args funs@ will wrap every function in @funs@ by+-- applying it to as many of the values in @args@ as possible. The new function+-- name will be @mkName@ applied to the wrapped function name.+--+-- Type signatures are not generated, so you may want to add these yourself or+-- turn on @NoMonomorphismRestriction@ if you have polymorphic constraints.+autoapplyDecs :: (String -> String) -> [Name] -> [Name] -> Q [Dec]+autoapplyDecs getNewName givens funs = do+  givenInfos <- for givens $ fmap (uncurry Given) . reifyVal "Argument"+  funInfos   <- for funs $ fmap (uncurry Function) . reifyVal "Function"+  let mkFun fun = do+        exp' <- autoapply1 givenInfos fun+        pure $ FunD (mkName . getNewName . nameBase . fName $ fun)+                    [Clause [] (NormalB exp') []]+  traverse mkFun funInfos++-- | A given is something we can try to pass as an argument+data Given = Given+  { gName :: Name+  , gType :: DType+  }+  deriving (Show)++-- | A function we are wrapping+data Function = Function+  { fName :: Name+  , fType :: DType+  }+  deriving (Show)++autoapply1 :: [Given] -> Function -> Q Exp+autoapply1 givens fun = do+  -- In this function we:+  --+  -- - Instantiate the command type with new unification variables+  -- - Split it into arguments and return type+  -- - Try to unify it with every 'Given' at every argument+  --   - If we can unify the monad of the 'Given' with that of the functions and+  --     unify the argument type, use that.+  --   - If nothing matches we just use an 'Argument'+  -- - Take the first result of all these tries++  let (fmap varBndrName -> cmdVars, _preds, args, ret) = unravel (fType fun)+      defaultMaybe m = ifte m (pure . Just) (pure Nothing)+      liftQ :: Q a -> IntBindingT TypeF (LogicT Q) a+      liftQ = T.lift . T.lift++      -- Use LogicT so we can backtrack on failure+      genProvs :: LogicT Q [ArgProvenance]+      genProvs = evalIntBindingT $ do+        instArgs <- traverse (inst cmdVars . snd <=< liftQ . typeDtoF) args++        -- This is @Just (m, a)@ when m is Applicative+        retMonad <- case ret of+          DAppT m a -> liftQ (isInstance ''Applicative [sweeten m]) >>= \case+            False -> pure Nothing+            True  -> do+              m' <- inst cmdVars . snd <=< liftQ . typeDtoF $ m+              a' <- inst cmdVars . snd <=< liftQ . typeDtoF $ a+              pure $ Just (m', a')+          _ -> pure Nothing++        -- A list of (type to unify, predicate to use this match, the given+        -- providing the value).+        --+        -- The predicate is there to make sure we only match unifiable monads+        instGivens <- fmap concat . for givens $ \g@Given {..} -> do+          -- The Given applied as is+          nonApp <- do+            instTy <- uncurry inst <=< liftQ . typeDtoF $ gType+            v      <- liftQ $ newName "g"+            pure (instTy, pure (), BoundPure v g)+          -- The given, but in an applicative context, only possible if we can+          -- unify the monad and there is a Monad instance+          app <- case stripForall gType of+            (vars, DAppT m a) | Just (cmdM, _) <- retMonad ->+              liftQ (isInstance ''Applicative [sweeten m]) >>= \case+                False -> pure Nothing+                True  -> do+                  m' <- inst vars . snd <=< liftQ . typeDtoF $ m+                  a' <- inst vars . snd <=< liftQ . typeDtoF $ a+                  v  <- liftQ $ newName "g"+                  let predicate = do+                        _ <- unify m' cmdM+                        pure ()+                  pure $ Just (a', predicate, Bound v g)+            _ -> pure Nothing+          pure ([nonApp] <> toList app)++        as <- for instArgs $ \argTy ->+          defaultMaybe . asum $ instGivens <&> \(givenTy, predicate, g) ->+            runExceptT+                (do+                  predicate+                  freshGivenTy <- freshen givenTy+                  unify freshGivenTy argTy+                )+              >>= \case+                    Left  (_ :: UFailure TypeF IntVar) -> empty+                    Right _                            -> pure g+        for (zip args as) $ \case+          (_, Just p ) -> pure p+          (t, Nothing) -> (`Argument` t) <$> liftQ (newName "a")++  argProvenances <-+    note "\"Impossible\" Finding argument provenances failed"+    .   listToMaybe+    =<< observeManyT 1 genProvs+  unless (length argProvenances == length args) $ fail+    "\"Impossible\", incorrect number of argument provenances were found"++  let bindGiven = \case+        BoundPure _ _ -> Nothing+        Bound     n g -> Just $ BindS (VarP n) (VarE (gName g))+        Argument  _ _ -> Nothing+      bs   = catMaybes (bindGiven <$> argProvenances)+      ret' = applyDExp+        (DVarE (fName fun))+        (argProvenances <&> \case+          Bound     n _           -> DVarE n+          BoundPure _ (Given n _) -> DVarE n+          Argument  n _           -> DVarE n+        )+  exp' <- dsDoStmts (bs <> [NoBindS (sweeten ret')])++  -- Typing the arguments here is important, if we don't then some skolems+  -- might escape!+  --+  -- Consider wrapping @f :: (forall a. a) -> ()@ (and supplying no arguments).+  -- We end up with the splice @myF x = f x@, and the @a@ in the argument to+  -- @f@ escapes. We can fix this by typing the pattern explicitly, thusly @myF+  -- (x :: forall a. a) = f x@+  pure $ LamE [ SigP (VarP n) (sweeten t) | Argument n t <- argProvenances ]+              (sweeten exp')++data ArgProvenance+  = Bound Name Given+    -- ^ Comes from a monadic binding+  | BoundPure Name Given+    -- ^ Comes from a pure binding, i.e. let ... in+  | Argument Name DType+    -- ^ Comes from an argument to the wrapped function+  deriving (Show)++----------------------------------------------------------------+-- Haskell types as a fixed point of TypeF+----------------------------------------------------------------++data TypeF a+  = AppF a a+  | VarF Name+  | ConF Name+  | ArrowF+  | LitF TyLit+  deriving (Show, Functor, Foldable, Traversable)++-- TODO: Derive this with generics+instance Unifiable TypeF where+  zipMatch (AppF l1 r1) (AppF l2 r2) =+    Just (AppF (Right (l1, l2)) (Right (r1, r2)))+  zipMatch (VarF n1) (VarF n2) | n1 == n2 = Just (VarF n1)+  zipMatch (ConF n1) (ConF n2) | n1 == n2 = Just (ConF n1)+  zipMatch ArrowF ArrowF                  = Just ArrowF+  zipMatch (LitF l1) (LitF l2) | l1 == l2 = Just (LitF l1)+  zipMatch _ _                            = Nothing++-- | Returns the type as a @Fix TypeF@ along with any quantified names. Drops+-- any context.+typeDtoF :: MonadFail m => DType -> m ([Name], Fix TypeF)+typeDtoF = traverse go . stripForall+ where+  go = \case+    DForallT{} -> fail "TODO: Higher ranked types"+    DAppT l r  -> do+      l' <- go l+      r' <- go r+      pure $ Fix (AppF l' r')+    DAppKindT t _ -> go t+    DSigT     t _ -> go t+    DVarT n       -> pure . Fix $ VarF n+    DConT n       -> pure . Fix $ ConF n+    DArrowT       -> pure . Fix $ ArrowF+    DLitT l       -> pure . Fix $ LitF l+    DWildCardT    -> fail "TODO: Wildcards"++varBndrName :: DTyVarBndr -> Name+varBndrName = \case+  DPlainTV n    -> n+  DKindedTV n _ -> n++-- | Raise foralls on the spine of the function type to the top+--+-- For example @forall a. a -> forall b. b@ becomes @forall a b. a -> b@+raiseForalls :: DType -> DType+raiseForalls = uncurry3 DForallT . go+ where+  go = \case+    DForallT vs ctx t ->+      let (vs', ctx', t') = go t in (vs <> vs', ctx <> ctx', t')+    l :~> r -> let (vs, ctx, r') = go r in (vs, ctx, l :~> r')+    t       -> ([], [], t)++pattern (:~>) :: DType -> DType -> DType+pattern l :~> r = DArrowT `DAppT` l `DAppT` r++-- | Instantiate a type with unification variables+inst+  :: BindingMonad TypeF IntVar m+  => [Name]+  -> Fix TypeF+  -> m (UTerm TypeF IntVar)+inst ns t = do+  vs <- sequence [ (n, ) <$> freeVar | n <- ns ]+  let go (Fix f) = case f of+        AppF l r                       -> UTerm (AppF (go l) (go r))+        VarF n | Just v <- lookup n vs -> UVar v+        VarF n                         -> UTerm (VarF n)+        ConF n                         -> UTerm (ConF n)+        ArrowF                         -> UTerm ArrowF+        LitF l                         -> UTerm (LitF l)+  pure $ go t++----------------------------------------------------------------+-- Utils+----------------------------------------------------------------++reifyVal :: String -> Name -> Q (Name, DType)+reifyVal d n = dsReify n >>= \case+  Just (DVarI name ty _) -> pure (name, ty)+  _                      -> fail $ d <> " " <> show n <> " isn't a value"++stripForall :: DType -> ([Name], DType)+stripForall = raiseForalls >>> \case+  DForallT vs _ ty -> (varBndrName <$> vs, ty)+  ty               -> ([], ty)++uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uncurry3 f (a, b, c) = f a b c++note :: MonadFail m => String -> Maybe a -> m a+note s = maybe (fail s) pure
+ test/Doctests.hs view
@@ -0,0 +1,7 @@+module Main where++import Build_doctests (flags, pkgs)+import Test.DocTest++main :: IO ()+main = doctest $ flags <> pkgs <> ["-fno-print-bind-result", "test/Types.hs"]