--------------------------------------------------------------------------------
-- | A store for storing and retreiving items
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables       #-}
module Hakyll.Core.Store
    ( Store
    , Result (..)
    , toMaybe
    , new
    , set
    , get
    , isMember
    , delete
    , hash
    ) where


--------------------------------------------------------------------------------
import           Control.Monad        (when)
import           Data.Binary          (Binary, decode, encodeFile)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Cache.LRU.IO    as Lru
import qualified Data.Hashable        as DH
import qualified Data.IORef           as IORef
import           Data.List            (intercalate)
import qualified Data.Map             as Map
import           Data.Maybe           (isJust)
import           Data.Typeable        (TypeRep, Typeable, cast, typeOf)
import           System.Directory     (createDirectoryIfMissing, doesFileExist,
                                       removeFile)
import           System.FilePath      ((</>))
import           System.IO            (IOMode (..), hClose, openFile)
import           System.IO.Error      (catchIOError, ioeSetFileName,
                                       ioeSetLocation, modifyIOError)


--------------------------------------------------------------------------------
-- | Simple wrapper type
data Box = forall a. Typeable a => Box a


--------------------------------------------------------------------------------
data Store = Store
    { -- | All items are stored on the filesystem
      Store -> [Char]
storeDirectory  :: FilePath
    , -- | See 'set'
      Store -> IORef (Map [Char] Box)
storeWriteAhead :: IORef.IORef (Map.Map String Box)
      -- | Optionally, items are also kept in-memory
    , Store -> Maybe (AtomicLRU [Char] Box)
storeMap        :: Maybe (Lru.AtomicLRU FilePath Box)
    }


--------------------------------------------------------------------------------
instance Show Store where
    show :: Store -> [Char]
show Store
_ = [Char]
"<Store>"


--------------------------------------------------------------------------------
-- | Result of a store query
data Result a
    = Found a                    -- ^ Found, result
    | NotFound                   -- ^ Not found
    | WrongType TypeRep TypeRep  -- ^ Expected, true type
    deriving (Int -> Result a -> ShowS
[Result a] -> ShowS
Result a -> [Char]
(Int -> Result a -> ShowS)
-> (Result a -> [Char]) -> ([Result a] -> ShowS) -> Show (Result a)
forall a. Show a => Int -> Result a -> ShowS
forall a. Show a => [Result a] -> ShowS
forall a. Show a => Result a -> [Char]
forall a.
(Int -> a -> ShowS) -> (a -> [Char]) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: forall a. Show a => Int -> Result a -> ShowS
showsPrec :: Int -> Result a -> ShowS
$cshow :: forall a. Show a => Result a -> [Char]
show :: Result a -> [Char]
$cshowList :: forall a. Show a => [Result a] -> ShowS
showList :: [Result a] -> ShowS
Show, Result a -> Result a -> Bool
(Result a -> Result a -> Bool)
-> (Result a -> Result a -> Bool) -> Eq (Result a)
forall a. Eq a => Result a -> Result a -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: forall a. Eq a => Result a -> Result a -> Bool
== :: Result a -> Result a -> Bool
$c/= :: forall a. Eq a => Result a -> Result a -> Bool
/= :: Result a -> Result a -> Bool
Eq)


--------------------------------------------------------------------------------
-- | Convert result to 'Maybe'
toMaybe :: Result a -> Maybe a
toMaybe :: forall a. Result a -> Maybe a
toMaybe (Found a
x) = a -> Maybe a
forall a. a -> Maybe a
Just a
x
toMaybe Result a
_         = Maybe a
forall a. Maybe a
Nothing


--------------------------------------------------------------------------------
-- | Initialize the store
new :: Bool      -- ^ Use in-memory caching
    -> FilePath  -- ^ Directory to use for hard disk storage
    -> IO Store  -- ^ Store
new :: Bool -> [Char] -> IO Store
new Bool
inMemory [Char]
directory = do
    Bool -> [Char] -> IO ()
createDirectoryIfMissing Bool
True [Char]
directory
    IORef (Map [Char] Box)
writeAhead <- Map [Char] Box -> IO (IORef (Map [Char] Box))
forall a. a -> IO (IORef a)
IORef.newIORef Map [Char] Box
forall k a. Map k a
Map.empty
    Maybe (AtomicLRU [Char] Box)
ref <- if Bool
inMemory then AtomicLRU [Char] Box -> Maybe (AtomicLRU [Char] Box)
forall a. a -> Maybe a
Just (AtomicLRU [Char] Box -> Maybe (AtomicLRU [Char] Box))
-> IO (AtomicLRU [Char] Box) -> IO (Maybe (AtomicLRU [Char] Box))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Integer -> IO (AtomicLRU [Char] Box)
forall key val. Ord key => Maybe Integer -> IO (AtomicLRU key val)
Lru.newAtomicLRU Maybe Integer
csize else Maybe (AtomicLRU [Char] Box) -> IO (Maybe (AtomicLRU [Char] Box))
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe (AtomicLRU [Char] Box)
forall a. Maybe a
Nothing
    Store -> IO Store
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Store
        { storeDirectory :: [Char]
storeDirectory  = [Char]
directory
        , storeWriteAhead :: IORef (Map [Char] Box)
storeWriteAhead = IORef (Map [Char] Box)
writeAhead
        , storeMap :: Maybe (AtomicLRU [Char] Box)
storeMap        = Maybe (AtomicLRU [Char] Box)
ref
        }
  where
    csize :: Maybe Integer
csize = Integer -> Maybe Integer
forall a. a -> Maybe a
Just Integer
500

--------------------------------------------------------------------------------
withStore :: Store -> String -> (String -> FilePath -> IO a) -> [String] -> IO a
withStore :: forall a.
Store -> [Char] -> ([Char] -> [Char] -> IO a) -> [[Char]] -> IO a
withStore Store
store [Char]
loc [Char] -> [Char] -> IO a
run [[Char]]
identifier = (IOError -> IOError) -> IO a -> IO a
forall a. (IOError -> IOError) -> IO a -> IO a
modifyIOError IOError -> IOError
handle (IO a -> IO a) -> IO a -> IO a
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char] -> IO a
run [Char]
key [Char]
path
  where
    key :: [Char]
key = [[Char]] -> [Char]
hash [[Char]]
identifier
    path :: [Char]
path = Store -> [Char]
storeDirectory Store
store [Char] -> ShowS
</> [Char]
key
    handle :: IOError -> IOError
handle IOError
e = IOError
e IOError -> [Char] -> IOError
`ioeSetFileName` ([Char]
path [Char] -> ShowS
forall a. [a] -> [a] -> [a]
++ [Char]
" for " [Char] -> ShowS
forall a. [a] -> [a] -> [a]
++ [Char] -> [[Char]] -> [Char]
forall a. [a] -> [[a]] -> [a]
intercalate [Char]
"/" [[Char]]
identifier)
                 IOError -> [Char] -> IOError
`ioeSetLocation` ([Char]
"Store." [Char] -> ShowS
forall a. [a] -> [a] -> [a]
++ [Char]
loc)

--------------------------------------------------------------------------------
-- | Auxiliary: add an item to the in-memory cache
cacheInsert :: Typeable a => Store -> String -> a -> IO ()
cacheInsert :: forall a. Typeable a => Store -> [Char] -> a -> IO ()
cacheInsert (Store [Char]
_ IORef (Map [Char] Box)
_ Maybe (AtomicLRU [Char] Box)
Nothing)    [Char]
_   a
_     = () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
cacheInsert (Store [Char]
_ IORef (Map [Char] Box)
_ (Just AtomicLRU [Char] Box
lru)) [Char]
key a
x =
    [Char] -> Box -> AtomicLRU [Char] Box -> IO ()
forall key val. Ord key => key -> val -> AtomicLRU key val -> IO ()
Lru.insert [Char]
key (a -> Box
forall a. Typeable a => a -> Box
Box a
x) AtomicLRU [Char] Box
lru


--------------------------------------------------------------------------------
-- | Auxiliary: get an item from the in-memory cache
cacheLookup :: forall a. Typeable a => Store -> String -> IO (Result a)
cacheLookup :: forall a. Typeable a => Store -> [Char] -> IO (Result a)
cacheLookup (Store [Char]
_ IORef (Map [Char] Box)
_ Maybe (AtomicLRU [Char] Box)
Nothing)    [Char]
_   = Result a -> IO (Result a)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Result a
forall a. Result a
NotFound
cacheLookup (Store [Char]
_ IORef (Map [Char] Box)
_ (Just AtomicLRU [Char] Box
lru)) [Char]
key = do
    Maybe Box
res <- [Char] -> AtomicLRU [Char] Box -> IO (Maybe Box)
forall key val.
Ord key =>
key -> AtomicLRU key val -> IO (Maybe val)
Lru.lookup [Char]
key AtomicLRU [Char] Box
lru
    Result a -> IO (Result a)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Result a -> IO (Result a)) -> Result a -> IO (Result a)
forall a b. (a -> b) -> a -> b
$ case Maybe Box
res of
        Maybe Box
Nothing      -> Result a
forall a. Result a
NotFound
        Just (Box a
x) -> case a -> Maybe a
forall a b. (Typeable a, Typeable b) => a -> Maybe b
cast a
x of
            Just a
x' -> a -> Result a
forall a. a -> Result a
Found a
x'
            Maybe a
Nothing -> TypeRep -> TypeRep -> Result a
forall a. TypeRep -> TypeRep -> Result a
WrongType (a -> TypeRep
forall a. Typeable a => a -> TypeRep
typeOf (a
forall a. HasCallStack => a
undefined :: a)) (a -> TypeRep
forall a. Typeable a => a -> TypeRep
typeOf a
x)


--------------------------------------------------------------------------------
cacheIsMember :: Store -> String -> IO Bool
cacheIsMember :: Store -> [Char] -> IO Bool
cacheIsMember (Store [Char]
_ IORef (Map [Char] Box)
_ Maybe (AtomicLRU [Char] Box)
Nothing)    [Char]
_   = Bool -> IO Bool
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
False
cacheIsMember (Store [Char]
_ IORef (Map [Char] Box)
_ (Just AtomicLRU [Char] Box
lru)) [Char]
key = Maybe Box -> Bool
forall a. Maybe a -> Bool
isJust (Maybe Box -> Bool) -> IO (Maybe Box) -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> AtomicLRU [Char] Box -> IO (Maybe Box)
forall key val.
Ord key =>
key -> AtomicLRU key val -> IO (Maybe val)
Lru.lookup [Char]
key AtomicLRU [Char] Box
lru


--------------------------------------------------------------------------------
-- | Auxiliary: delete an item from the in-memory cache
cacheDelete :: Store -> String -> IO ()
cacheDelete :: Store -> [Char] -> IO ()
cacheDelete (Store [Char]
_ IORef (Map [Char] Box)
_ Maybe (AtomicLRU [Char] Box)
Nothing)    [Char]
_   = () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
cacheDelete (Store [Char]
_ IORef (Map [Char] Box)
_ (Just AtomicLRU [Char] Box
lru)) [Char]
key = do
    Maybe Box
_ <- [Char] -> AtomicLRU [Char] Box -> IO (Maybe Box)
forall key val.
Ord key =>
key -> AtomicLRU key val -> IO (Maybe val)
Lru.delete [Char]
key AtomicLRU [Char] Box
lru
    () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()


--------------------------------------------------------------------------------
-- | Store an item
set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
set :: forall a. (Binary a, Typeable a) => Store -> [[Char]] -> a -> IO ()
set Store
store [[Char]]
identifier a
value = Store -> [Char] -> ([Char] -> [Char] -> IO ()) -> [[Char]] -> IO ()
forall a.
Store -> [Char] -> ([Char] -> [Char] -> IO a) -> [[Char]] -> IO a
withStore Store
store [Char]
"set" (\[Char]
key [Char]
path -> do
    -- We need to avoid concurrent writes to the filesystem.  Imagine the
    -- follow scenario:
    --
    --  *  We compile multiple posts
    --  *  All of these fetch some common metadata
    --  *  This metadata is missing; we fetch it and then store it.
    --
    -- To solve this, we skip duplicate writes by tracking their status
    -- in 'storeWriteAhead'.  Since this set will usually be small, the
    -- required locking should be fast.  Additionally the actual IO operation
    -- still happens outside of the locking.
    Bool
first <- IORef (Map [Char] Box)
-> (Map [Char] Box -> (Map [Char] Box, Bool)) -> IO Bool
forall a b. IORef a -> (a -> (a, b)) -> IO b
IORef.atomicModifyIORef' (Store -> IORef (Map [Char] Box)
storeWriteAhead Store
store) ((Map [Char] Box -> (Map [Char] Box, Bool)) -> IO Bool)
-> (Map [Char] Box -> (Map [Char] Box, Bool)) -> IO Bool
forall a b. (a -> b) -> a -> b
$
        \Map [Char] Box
wa -> case [Char] -> Map [Char] Box -> Maybe Box
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup [Char]
key Map [Char] Box
wa of
            Maybe Box
Nothing -> ([Char] -> Box -> Map [Char] Box -> Map [Char] Box
forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert [Char]
key (a -> Box
forall a. Typeable a => a -> Box
Box a
value) Map [Char] Box
wa, Bool
True)
            Just Box
_  -> (Map [Char] Box
wa, Bool
False)

    Store -> [Char] -> a -> IO ()
forall a. Typeable a => Store -> [Char] -> a -> IO ()
cacheInsert Store
store [Char]
key a
value

    -- Only the thread that stored the writeAhead should actually write this
    -- file.  That way, only one thread at a time will try to write this.
    -- Release the writeAhead value once we're done.
    Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when Bool
first (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
        [Char] -> a -> IO ()
forall a. Binary a => [Char] -> a -> IO ()
encodeFile [Char]
path a
value
        IORef (Map [Char] Box)
-> (Map [Char] Box -> (Map [Char] Box, ())) -> IO ()
forall a b. IORef a -> (a -> (a, b)) -> IO b
IORef.atomicModifyIORef' (Store -> IORef (Map [Char] Box)
storeWriteAhead Store
store) ((Map [Char] Box -> (Map [Char] Box, ())) -> IO ())
-> (Map [Char] Box -> (Map [Char] Box, ())) -> IO ()
forall a b. (a -> b) -> a -> b
$
            \Map [Char] Box
wa -> ([Char] -> Map [Char] Box -> Map [Char] Box
forall k a. Ord k => k -> Map k a -> Map k a
Map.delete [Char]
key Map [Char] Box
wa, ())
  ) [[Char]]
identifier


--------------------------------------------------------------------------------
-- | Load an item
get :: forall a. (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
get :: forall a.
(Binary a, Typeable a) =>
Store -> [[Char]] -> IO (Result a)
get Store
store = Store
-> [Char]
-> ([Char] -> [Char] -> IO (Result a))
-> [[Char]]
-> IO (Result a)
forall a.
Store -> [Char] -> ([Char] -> [Char] -> IO a) -> [[Char]] -> IO a
withStore Store
store [Char]
"get" (([Char] -> [Char] -> IO (Result a)) -> [[Char]] -> IO (Result a))
-> ([Char] -> [Char] -> IO (Result a)) -> [[Char]] -> IO (Result a)
forall a b. (a -> b) -> a -> b
$ \[Char]
key [Char]
path -> do
    -- Check the writeAhead value
    Map [Char] Box
writeAhead <- IORef (Map [Char] Box) -> IO (Map [Char] Box)
forall a. IORef a -> IO a
IORef.readIORef (IORef (Map [Char] Box) -> IO (Map [Char] Box))
-> IORef (Map [Char] Box) -> IO (Map [Char] Box)
forall a b. (a -> b) -> a -> b
$ Store -> IORef (Map [Char] Box)
storeWriteAhead Store
store
    case [Char] -> Map [Char] Box -> Maybe Box
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup [Char]
key Map [Char] Box
writeAhead of
        Just (Box a
x) -> case a -> Maybe a
forall a b. (Typeable a, Typeable b) => a -> Maybe b
cast a
x of
            Just a
x' -> Result a -> IO (Result a)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Result a -> IO (Result a)) -> Result a -> IO (Result a)
forall a b. (a -> b) -> a -> b
$ a -> Result a
forall a. a -> Result a
Found a
x'
            Maybe a
Nothing -> Result a -> IO (Result a)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Result a -> IO (Result a)) -> Result a -> IO (Result a)
forall a b. (a -> b) -> a -> b
$ TypeRep -> TypeRep -> Result a
forall a. TypeRep -> TypeRep -> Result a
WrongType (a -> TypeRep
forall a. Typeable a => a -> TypeRep
typeOf (a
forall a. HasCallStack => a
undefined :: a)) (a -> TypeRep
forall a. Typeable a => a -> TypeRep
typeOf a
x)
        Maybe Box
Nothing -> do
            -- Check the in-memory map
            Result a
ref <- Store -> [Char] -> IO (Result a)
forall a. Typeable a => Store -> [Char] -> IO (Result a)
cacheLookup Store
store [Char]
key
            case Result a
ref of
                -- Not found in the map, try the filesystem
                Result a
NotFound -> do
                    Bool
exists <- [Char] -> IO Bool
doesFileExist [Char]
path
                    if Bool -> Bool
not Bool
exists
                        -- Not found in the filesystem either
                        then Result a -> IO (Result a)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Result a
forall a. Result a
NotFound
                        -- Found in the filesystem
                        else do
                            a
v <- [Char] -> IO a
forall {b}. Binary b => [Char] -> IO b
decodeClose [Char]
path
                            Store -> [Char] -> a -> IO ()
forall a. Typeable a => Store -> [Char] -> a -> IO ()
cacheInsert Store
store [Char]
key a
v
                            Result a -> IO (Result a)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (Result a -> IO (Result a)) -> Result a -> IO (Result a)
forall a b. (a -> b) -> a -> b
$ a -> Result a
forall a. a -> Result a
Found a
v
                -- Found in the in-memory map (or wrong type), just return
                Result a
s -> Result a -> IO (Result a)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Result a
s
  where
    -- 'decodeFile' from Data.Binary which closes the file ASAP
    decodeClose :: [Char] -> IO b
decodeClose [Char]
path = do
        Handle
h   <- [Char] -> IOMode -> IO Handle
openFile [Char]
path IOMode
ReadMode
        ByteString
lbs <- Handle -> IO ByteString
BL.hGetContents Handle
h
        ByteString -> Int64
BL.length ByteString
lbs Int64 -> IO () -> IO ()
forall a b. a -> b -> b
`seq` Handle -> IO ()
hClose Handle
h
        b -> IO b
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (b -> IO b) -> b -> IO b
forall a b. (a -> b) -> a -> b
$ ByteString -> b
forall a. Binary a => ByteString -> a
decode ByteString
lbs


--------------------------------------------------------------------------------
-- | Strict function
isMember :: Store -> [String] -> IO Bool
isMember :: Store -> [[Char]] -> IO Bool
isMember Store
store = Store
-> [Char] -> ([Char] -> [Char] -> IO Bool) -> [[Char]] -> IO Bool
forall a.
Store -> [Char] -> ([Char] -> [Char] -> IO a) -> [[Char]] -> IO a
withStore Store
store [Char]
"isMember" (([Char] -> [Char] -> IO Bool) -> [[Char]] -> IO Bool)
-> ([Char] -> [Char] -> IO Bool) -> [[Char]] -> IO Bool
forall a b. (a -> b) -> a -> b
$ \[Char]
key [Char]
path -> do
    Map [Char] Box
writeAhead <- IORef (Map [Char] Box) -> IO (Map [Char] Box)
forall a. IORef a -> IO a
IORef.readIORef (IORef (Map [Char] Box) -> IO (Map [Char] Box))
-> IORef (Map [Char] Box) -> IO (Map [Char] Box)
forall a b. (a -> b) -> a -> b
$ Store -> IORef (Map [Char] Box)
storeWriteAhead Store
store
    if [Char] -> Map [Char] Box -> Bool
forall k a. Ord k => k -> Map k a -> Bool
Map.member [Char]
key Map [Char] Box
writeAhead
        then Bool -> IO Bool
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Bool
True
        else do
            Bool
inCache <- Store -> [Char] -> IO Bool
cacheIsMember Store
store [Char]
key
            if Bool
inCache then Bool -> IO Bool
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True else [Char] -> IO Bool
doesFileExist [Char]
path


--------------------------------------------------------------------------------
-- | Delete an item
delete :: Store -> [String] -> IO ()
delete :: Store -> [[Char]] -> IO ()
delete Store
store = Store -> [Char] -> ([Char] -> [Char] -> IO ()) -> [[Char]] -> IO ()
forall a.
Store -> [Char] -> ([Char] -> [Char] -> IO a) -> [[Char]] -> IO a
withStore Store
store [Char]
"delete" (([Char] -> [Char] -> IO ()) -> [[Char]] -> IO ())
-> ([Char] -> [Char] -> IO ()) -> [[Char]] -> IO ()
forall a b. (a -> b) -> a -> b
$ \[Char]
key [Char]
path -> do
    Store -> [Char] -> IO ()
cacheDelete Store
store [Char]
key
    [Char] -> IO ()
deleteFile [Char]
path


--------------------------------------------------------------------------------
-- | Delete a file unless it doesn't exist...
deleteFile :: FilePath -> IO ()
deleteFile :: [Char] -> IO ()
deleteFile = (IO () -> (IOError -> IO ()) -> IO ()
forall a. IO a -> (IOError -> IO a) -> IO a
`catchIOError` \IOError
_ -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()) (IO () -> IO ()) -> ([Char] -> IO ()) -> [Char] -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> IO ()
removeFile


--------------------------------------------------------------------------------
-- | Mostly meant for internal usage
hash :: [String] -> String
hash :: [[Char]] -> [Char]
hash = Int -> [Char]
forall a. Show a => a -> [Char]
show (Int -> [Char]) -> ([[Char]] -> Int) -> [[Char]] -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> Int
forall a. Hashable a => a -> Int
DH.hash ([Char] -> Int) -> ([[Char]] -> [Char]) -> [[Char]] -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> [[Char]] -> [Char]
forall a. [a] -> [[a]] -> [a]
intercalate [Char]
"/"