packages feed

named (empty) → 0.1.0.0

raw patch · 6 files changed

+410/−0 lines, 6 filesdep +basedep +namedsetup-changed

Dependencies added: base, named

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+## 0.1.0.0
+
+* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2018, Vladislav Zavialov
+
+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 Vladislav Zavialov 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,2 @@+import Distribution.Simple
+main = defaultMain
+ named.cabal view
@@ -0,0 +1,57 @@+name:                named
+version:             0.1.0.0
+synopsis:            Named parameters (keyword arguments) for Haskell
+description:
+    `named` is a lightweight library for named function parameters (keyword
+    arguments) based on overloaded labels. Keyword arguments have several
+    advantages over positional arguments:
+    .
+    * they can be supplied in arbitrary order
+    * their names serve as documentation at call site
+    * it is impossible to accidentally mix them up
+    .
+    Unlike @newtype@ wrappers, keyword arguments don't pollute the global
+    namespace, don't require top-level definitions, and don't need to be
+    exported.
+    .
+    This implementation of named parameters is typesafe, provides good type
+    inference, descriptive type errors, and has no runtime overhead.
+    .
+    Example usage:
+    .
+    > import Named
+    >
+    > createSymLink :: FilePath `Named` "from" -> FilePath `Named` "to" -> IO ()
+    > createSymLink (Named from) (Named to) = ...
+    >
+    > main = createSymLink ! #from "/path/to/source"
+    >                      ! #to "/target/path"
+
+
+license:             BSD3
+license-file:        LICENSE
+author:              Vladislav Zavialov
+maintainer:          Vladislav Zavialov <vlad.z.4096@gmail.com>
+category:            Control
+build-type:          Simple
+extra-source-files:  ChangeLog.md
+tested-with:         GHC ==8.0.2, GHC ==8.2.2, GHC ==8.4.1
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Named
+  build-depends:       base >=4.9 && <4.12
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+                       -fno-warn-unticked-promoted-constructors
+
+test-suite regression
+  type:                exitcode-stdio-1.0
+  main-is:             Test.hs
+  build-depends:       base >=4.9 && <4.12,
+                       named
+  hs-source-dirs:      test
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+                       -fno-warn-missing-signatures
+ src/Named.hs view
@@ -0,0 +1,255 @@+{-# LANGUAGE KindSignatures, DataKinds, FlexibleInstances, FlexibleContexts,
+             FunctionalDependencies, TypeFamilies, TypeOperators,
+             PatternSynonyms, UndecidableInstances, ConstraintKinds,
+             TypeApplications, ScopedTypeVariables, CPP #-}
+
+{- |
+
+Named parameters, also known as keyword arguments, have several advantages
+over positional arguments:
+
+* convenience: they can be supplied in arbitrary order
+* readability: their names serve as documentation at call site
+* safety: it is impossible to accidentally mix them up
+
+Consider a function to replace a substring with another string:
+
+@
+Text.replace path "$HOME" "\/home\/username\/"
+@
+
+We want to replace references to the @$HOME@ environment variable with a
+concrete directory. There is but one problem – we have supplied the text
+arguments in the wrong order.
+
+Compare that to a newtype-based solution:
+
+@
+Text.replace
+  (Needle "$HOME")
+  (Replacement "\/home\/username\/")
+  (Haystack path)
+@
+
+Now that the function requires each argument to be wrapped in a newtype, we
+cannot mix them up – the compiler will report an error, and newtype constructors
+serve as documentation.
+
+The problem with newtypes is that it is bothersome to create them for each
+parameter, they pollute the global namespace, and we still cannot supply wrapped
+arguments in arbitrary order.
+
+With keyword arguments, none of that is a problem:
+
+@
+Text.replace '!' #haystack path
+             '!' #needle "$HOME"
+             '!' #replacement "\/home\/username\/"
+@
+
+Functions must declare their parameter names in the type signature:
+
+@
+replace ::
+  Text \``Named`\` "needle" ->
+  Text \``Named`\` "replacement" ->
+  Text \``Named`\` "haystack" ->
+  Text
+replace (Named needle) (Named replacement) (Named haystack) =
+  ...
+@
+
+Keyword arguments have seamless interoperability with positional arguments when
+the function takes them last. Consider this function:
+
+@
+foo :: A -> B -> C \``Named`\` "x" -> IO ()
+@
+
+There are several ways to invoke it:
+
+@
+(foo a b) '!' #x c     -- parentheses for clarity
+(foo a '!' #x c) b     -- parentheses required
+(foo '!' #x c) a b     -- parentheses required
+@
+
+We can also supply keyword arguments using the 'with' combinator instead of
+the '!' operator:
+
+@
+('with' #x c foo) a b  -- parentheses for clarity
+'with' #x c (foo a b)  -- has the same effect
+@
+
+Both '!' and 'with' work in a similar manner: they traverse the spine of
+the function and supply the first keyword argument with a matching name.
+
+For example:
+
+@
+bar           :: A \``Named`\` "x" -> B \``Named`\` "y" -> IO ()
+bar '!' #y b    :: A \``Named`\` "x"                  -> IO ()
+'with' #y b bar :: A \``Named`\` "x"                  -> IO ()
+@
+
+-}
+module Named
+  (
+    -- * Core interface
+    Named(..),
+    (!),
+    Name(..),
+    with,
+
+    -- * Specialized synonyms
+    Flag,
+    pattern Flag,
+
+    -- * Internal
+    Apply,
+    apply,
+    named
+  ) where
+
+import Prelude (Bool, id)
+import Data.Kind (Type)
+import GHC.TypeLits (Symbol, TypeError, ErrorMessage(..))
+import GHC.OverloadedLabels (IsLabel(..))
+
+{- |
+
+Assign a name to a value of type @a@. This is a simple wrapper intended
+for use with @-XOverloadedLabels@:
+
+@
+#verbose True :: Named Bool "verbose"
+@
+
+-}
+newtype Named a (name :: Symbol) = Named { unnamed :: a }
+
+instance (name ~ name', a ~ a') => IsLabel name (a -> Named a' name') where
+#if MIN_VERSION_base(4,10,0)
+  fromLabel = Named
+#else
+  fromLabel _ = Named
+#endif
+  {-# INLINE fromLabel #-}
+
+-- | Snake oil to cure boolean blindness.
+type Flag = Named Bool
+
+-- | Match on a flag, a version of 'Named' specialized to 'Bool'.
+pattern Flag :: Bool -> Flag name
+pattern Flag a = Named a
+
+#if MIN_VERSION_base(4,10,0)
+{-# COMPLETE Flag #-}
+#endif
+
+{- | Supply a keyword argument to a function:
+
+@
+function ! #param_name value
+@
+-}
+
+(!) :: Apply name a fn fn' => fn -> Named a name -> fn'
+(!) = apply
+{-# INLINE (!) #-}
+
+infixl 9 !
+
+{- |
+
+A proxy for a name, intended for use with @-XOverloadedLabels@:
+
+@
+#verbose :: Name "verbose"
+@
+
+-}
+data Name (name :: Symbol) = Name
+
+instance name ~ name' => IsLabel name' (Name name) where
+#if MIN_VERSION_base(4,10,0)
+  fromLabel = Name
+#else
+  fromLabel _ = Name
+#endif
+  {-# INLINE fromLabel #-}
+
+{- | Supply a keyword argument to a function:
+
+@
+with #param_name value function
+@
+-}
+with :: Apply name a fn fn' => Name name -> a -> fn -> fn'
+with name a fn = fn ! named name a
+{-# INLINE with #-}
+
+-- | Annotate a value with a name.
+named :: Name name -> a -> Named a name
+named _ = Named
+{-# INLINE named #-}
+
+--------------------------------------------------------------------------------
+--  Do not read further to avoid emotional trauma.
+--------------------------------------------------------------------------------
+
+data Decision = Done | Skip Decision
+
+type family Decide (name :: Symbol) (fn :: Type) :: Decision where
+  Decide name (Named a name -> r) = Done
+  Decide name (x -> r) = Skip (Decide name r)
+  Decide name t =
+    TypeError (Text "Named parameter '" :<>: Text name :<>:
+               Text "' was supplied, but not expected")
+
+class
+  ( decision ~ Decide name fn
+  ) => Apply' decision name a fn fn' | name fn -> fn'
+  where
+    -- | Apply a function to a keyword argument.
+    apply :: fn -> Named a name -> fn'
+
+instance
+  ( fn ~ (Named a name -> r),
+    fn' ~ r,
+    Decide name fn ~ Done
+  ) => Apply' Done name a fn fn'
+  where
+    apply = id
+    {-# INLINE apply #-}
+
+instance
+  ( Apply' decision name a r r',
+    Decide name fn ~ Skip decision,
+    fn ~ (x -> r),
+    fn' ~ (x -> r')
+  ) => Apply' (Skip decision) name a fn fn'
+  where
+    apply fn a = \x -> apply (fn x) a
+    {-# INLINE apply #-}
+
+{- |
+
+Supply a parameter of type @a@ named @name@ to a function @fn@, resulting in
+@fn'@.
+
+For example:
+
+@
+Apply "x" Char
+  (Named Bool "b" -> Named Char "x" -> r)
+  (Named Bool "b" -> r)
+@
+
+In case the parameter cannot be supplied, this constraint will become a type
+error.
+
+-}
+type Apply (name :: Symbol) (a :: Type) (fn :: Type) (fn' :: Type) =
+  Apply' (Decide name fn) name a fn fn'
+ test/Test.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedLabels, DataKinds, TypeOperators #-}
+
+module Main where
+
+import Data.Function ((&))
+import Named
+
+test1 ::
+  String ->
+  Flag "a" ->
+  Bool `Named` "b" ->
+  IO ()
+test1 "str" (Flag True) (Flag False) = return ()
+test1 _ _ _ = error "unexpected flags or str"
+
+test1_1 =
+  test1 "str"
+    ! #a True
+    ! #b False
+
+test1_2 =
+  test1 "str"
+    ! #b False
+    ! #a True
+
+test1_3 =
+  test1 "str"
+    & with #b False
+    & with #a True
+
+test1_4 =
+  with #a True $
+  with #b False $
+    test1 "str"
+
+test1_5 =
+  with #a True $
+  test1 "str"! #b False
+
+test1_6 =
+  test1 "str"! #a True
+    & with #b False
+
+test2 :: Named Int "x" -> Int
+test2 x = unnamed x * 2
+
+test2_1 :: Int
+test2_1 = test2 ! #x 5 + test2 ! #x 3
+
+main :: IO ()
+main = do
+  test1_1
+  test1_2
+  test1_3
+  test1_4
+  test1_5
+  test1_6
+  test2_1 `mustBe` 16
+
+mustBe :: (Eq a, Show a) => a -> a -> IO ()
+mustBe a b
+  | a == b = return ()
+  | otherwise = error $ show a ++ " must be " ++ show b