RecordWildCards for localised module imports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Rank2Types #-}
-- This line isn't required unless you want “import” punning:
{-# LANGUAGE NamedFieldPuns #-}

-- This can be declared in your API module:
import qualified Data.Map as M
-- This is used in client code:
import Data.Map (Map)

-- For convenience.
class Default a where def :: a

-- To declare the “module”:
data DataMap = M -- Choose your module selector… I'll mirror the usual “qualified as M” style for Data.Map.
  { lookup :: Ord k => k -> Map k a -> Maybe a
  , insert :: Ord k => k -> a -> Map k a -> Map k a
  , empty  :: forall k a. Map k a
  }
-- This makes using it a lot shorter. Otherwise you can have “dataMap = M { … }”.
instance Default DataMap where
  def = M { lookup = M.lookup
          , insert = M.insert
          , empty  = M.empty
          }

-- Use the “module”:
-- So “let” is OK:
foo x y z = let M{..} = def in lookup x $ insert x y z
-- I prefer “where”:
foo x y z = lookup x $ insert x y z where M{..} = def
-- Import punning:
foo x y z = lookup x $ insert x y z where M{insert,lookup} = def
-- Import renaming:
foo x y z = l x $ i x y z where M{insert=i,lookup=l,..} = def
-- Exports aren't monomorphic:
bar = let M{..} = def in (empty :: Map Int Char,empty :: Map Char Int)

-- I think it's quite concise. It doesn't work for types but I think
-- types are a rare source of clashes compared to values/functions.
--
-- Looking into it further, I see that there doesn't appear to be an
-- overhead for this, either:
--
--     main = print (lookup 1 (empty :: Map Int Char)) where M{..} = def
-- 
-- In the core -O2 output becomes:
--
--   main:main4 :: Int =
--     Izh (1::Intzh);
--   main:main3 :: (base:DataziMaybe.Maybe
--                       Char) =
--     containerszm0zi3zi0zi0:DataziMap.lookup @ Int
--     @ Char base:zdfOrdInt
--     main:main4
--     (containerszm0zi3zi0zi0:DataziMap.Tip @ Int
--      @ Char);
--
-- Or more cleanly, in essence:
--
--     main3 = Data.Map.lookup 1 Data.Map.Tip

For ByteString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-- Here's an example that deals with ByteString clashes


module LS where

import           Default

import qualified Data.ByteString.Lazy as L
import           GHC.Word

data ByteStringLazy = L { pack :: [Word8] -> L.ByteString }
instance Default ByteStringLazy where def = L { pack = L.pack}

---

module BS where

import           Default

import qualified Data.ByteString as B
import           GHC.Word

data ByteStringStrict = B { pack :: [Word8] -> B.ByteString }
instance Default ByteStringStrict where def = B { pack = B.pack}

---

module Default where

class Default a where def :: a

---

{-# LANGUAGE RecordWildCards #-}

import BS
import Default
import LS

-- lazy
foo = pack [1,2,3] where L{..} = def

-- strict
bar = pack [1,2,3] where B{..} = def

-- mixed
zot = (a,b) where
  a = pack [1,2,3] where L{..} = def
  b = pack [1,2,3] where B{..} = def

With renaming

1
2
3
4
-- renaming, if really needed
renaming = (packL [1,2,3],packS [4,5,6]) where
  L{pack=packL} = def
  B{pack=packS} = def