-- Copyright 2013 Evan Laforge
-- This program is distributed under the terms of the GNU General Public
-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt

{-# LANGUAGE ScopedTypeVariables #-}
-- | Do things with files.
module Util.File (
    -- * read/write
    writeLines
    , writeAtomic
    , symlink
    -- * query
    , writable
    -- * directory
    , list, listRecursive
    , walk
    -- * compression
    , readGz, writeGz
) where
import qualified Codec.Compression.GZip as GZip
import qualified Codec.Compression.Zlib.Internal as Zlib.Internal
import qualified Control.Exception as Exception
import           Control.Monad (forM_, unless, when)
import           Control.Monad.Extra (filterM, ifM, orM, partitionM, whenM)
import           Control.Monad.Trans (liftIO)

import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as Lazy
import           Data.Text (Text)
import qualified Data.Text.IO as Text.IO

import qualified Streaming as S
import qualified Streaming.Prelude as S
import qualified System.Directory as Directory
import           System.FilePath ((</>))
import qualified System.IO as IO
import qualified System.IO.Error as Error
import qualified System.Posix.Files as Posix.Files

import qualified Util.Exceptions as Exceptions


-- * read/write

writeLines :: FilePath -> [Text] -> IO ()
writeLines :: [Char] -> [Text] -> IO ()
writeLines [Char]
fname [Text]
lines = [Char] -> IOMode -> (Handle -> IO ()) -> IO ()
forall r. [Char] -> IOMode -> (Handle -> IO r) -> IO r
IO.withFile [Char]
fname IOMode
IO.WriteMode ((Handle -> IO ()) -> IO ()) -> (Handle -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Handle
hdl ->
    (Text -> IO ()) -> [Text] -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (Handle -> Text -> IO ()
Text.IO.hPutStrLn Handle
hdl) [Text]
lines

writeAtomic :: FilePath -> ByteString.ByteString -> IO ()
writeAtomic :: [Char] -> ByteString -> IO ()
writeAtomic [Char]
fn ByteString
bytes = do
    [Char] -> ByteString -> IO ()
ByteString.writeFile [Char]
tmp ByteString
bytes
    [Char] -> [Char] -> IO ()
Directory.renameFile [Char]
tmp [Char]
fn
    where
    tmp :: [Char]
tmp = [Char]
fn [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
".write.tmp"

-- | Make a symlink atomically.
symlink :: String -> FilePath -> IO ()
symlink :: [Char] -> [Char] -> IO ()
symlink [Char]
dest [Char]
fname = do
    Maybe [Char]
oldDest <- IO [Char] -> IO (Maybe [Char])
forall a. IO a -> IO (Maybe a)
Exceptions.ignoreEnoent (IO [Char] -> IO (Maybe [Char])) -> IO [Char] -> IO (Maybe [Char])
forall a b. (a -> b) -> a -> b
$ [Char] -> IO [Char]
Directory.getSymbolicLinkTarget [Char]
fname
    -- Don't remake if it's already right.  Probably unnecessary, but it
    -- happens a lot and we can avoid touching the filesystem.
    Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Maybe [Char]
oldDest Maybe [Char] -> Maybe [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== [Char] -> Maybe [Char]
forall a. a -> Maybe a
Just [Char]
dest) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
        -- If a previous process got killed, there might be stale .tmp files.
        IO () -> IO (Maybe ())
forall a. IO a -> IO (Maybe a)
Exceptions.ignoreEnoent (IO () -> IO (Maybe ())) -> IO () -> IO (Maybe ())
forall a b. (a -> b) -> a -> b
$ [Char] -> IO ()
Directory.removeFile ([Char]
fname [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
".tmp")
        -- Atomically replace the old link, if any.
        [Char] -> [Char] -> IO ()
Directory.createFileLink [Char]
dest ([Char]
fname [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
".tmp")
        [Char] -> [Char] -> IO ()
Directory.renameFile ([Char]
fname [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
".tmp") [Char]
fname

-- * query

sameContents :: FilePath -> FilePath -> IO Bool
sameContents :: [Char] -> [Char] -> IO Bool
sameContents [Char]
fn1 [Char]
fn2 = do
    Maybe ByteString
c1 <- IO ByteString -> IO (Maybe ByteString)
forall a. IO a -> IO (Maybe a)
Exceptions.ignoreEnoent (IO ByteString -> IO (Maybe ByteString))
-> IO ByteString -> IO (Maybe ByteString)
forall a b. (a -> b) -> a -> b
$ [Char] -> IO ByteString
Lazy.readFile [Char]
fn1
    Maybe ByteString
c2 <- IO ByteString -> IO (Maybe ByteString)
forall a. IO a -> IO (Maybe a)
Exceptions.ignoreEnoent (IO ByteString -> IO (Maybe ByteString))
-> IO ByteString -> IO (Maybe ByteString)
forall a b. (a -> b) -> a -> b
$ [Char] -> IO ByteString
Lazy.readFile [Char]
fn2
    Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return (Bool -> IO Bool) -> Bool -> IO Bool
forall a b. (a -> b) -> a -> b
$ Maybe ByteString
c1 Maybe ByteString -> Maybe ByteString -> Bool
forall a. Eq a => a -> a -> Bool
== Maybe ByteString
c2

-- | Throw if this file exists but isn't writable.
requireWritable :: FilePath -> IO ()
requireWritable :: [Char] -> IO ()
requireWritable [Char]
fn = IO Bool -> IO () -> IO ()
forall (m :: * -> *). Monad m => m Bool -> m () -> m ()
whenM (Bool -> Bool
not (Bool -> Bool) -> IO Bool -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> IO Bool
writable [Char]
fn) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
    IOError -> IO ()
forall e a. Exception e => e -> IO a
Exception.throwIO (IOError -> IO ()) -> IOError -> IO ()
forall a b. (a -> b) -> a -> b
$ IOErrorType -> [Char] -> Maybe Handle -> Maybe [Char] -> IOError
Error.mkIOError IOErrorType
Error.permissionErrorType
        [Char]
"refusing to overwrite a read-only file" Maybe Handle
forall a. Maybe a
Nothing ([Char] -> Maybe [Char]
forall a. a -> Maybe a
Just [Char]
fn)

-- | True if the file doesn't exist, or if it does but is writable.
writable :: FilePath -> IO Bool
writable :: [Char] -> IO Bool
writable [Char]
fn = [IO Bool] -> IO Bool
forall (m :: * -> *). Monad m => [m Bool] -> m Bool
orM
    [ Bool -> Bool
not (Bool -> Bool) -> IO Bool -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [IO Bool] -> IO Bool
forall (m :: * -> *). Monad m => [m Bool] -> m Bool
orM [[Char] -> IO Bool
Directory.doesFileExist [Char]
fn, [Char] -> IO Bool
Directory.doesDirectoryExist [Char]
fn]
    , Permissions -> Bool
Directory.writable (Permissions -> Bool) -> IO Permissions -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> IO Permissions
Directory.getPermissions [Char]
fn
    ]

-- * directory

-- | Like 'Directory.listDirectory' except prepend the directory.
list :: FilePath -> IO [FilePath]
list :: [Char] -> IO [[Char]]
list [Char]
dir = do
    [[Char]]
fns <- [Char] -> IO [[Char]]
Directory.listDirectory [Char]
dir
    [[Char]] -> IO [[Char]]
forall (m :: * -> *) a. Monad m => a -> m a
return ([[Char]] -> IO [[Char]]) -> [[Char]] -> IO [[Char]]
forall a b. (a -> b) -> a -> b
$ ([Char] -> [Char]) -> [[Char]] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map ([Char] -> [Char]
strip ([Char] -> [Char]) -> ([Char] -> [Char]) -> [Char] -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([Char]
dir </>)) ([[Char]] -> [[Char]]) -> [[Char]] -> [[Char]]
forall a b. (a -> b) -> a -> b
$ ([Char] -> Bool) -> [[Char]] -> [[Char]]
forall a. (a -> Bool) -> [a] -> [a]
filter (([Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
/=[Char]
".") ([Char] -> Bool) -> ([Char] -> [Char]) -> [Char] -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> [Char] -> [Char]
forall a. Int -> [a] -> [a]
take Int
1) [[Char]]
fns
    where
    strip :: [Char] -> [Char]
strip (Char
'.' : Char
'/' : [Char]
path) = [Char]
path
    strip [Char]
path = [Char]
path

listRecursive :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
listRecursive :: ([Char] -> Bool) -> [Char] -> IO [[Char]]
listRecursive [Char] -> Bool
descend [Char]
dir = do
    Bool
is_file <- [Char] -> IO Bool
Directory.doesFileExist [Char]
dir
    if Bool
is_file then [[Char]] -> IO [[Char]]
forall (m :: * -> *) a. Monad m => a -> m a
return [[Char]
dir]
        else Bool -> ([Char] -> Bool) -> [Char] -> IO [[Char]]
maybeDescend ([Char]
dir [Char] -> [Char] -> Bool
forall a. Eq a => a -> a -> Bool
== [Char]
"." Bool -> Bool -> Bool
|| [Char] -> Bool
descend [Char]
dir) [Char] -> Bool
descend [Char]
dir
    where
    maybeDescend :: Bool -> ([Char] -> Bool) -> [Char] -> IO [[Char]]
maybeDescend Bool
True [Char] -> Bool
descend [Char]
dir = do
        [[Char]]
fns <- [Char] -> IO [[Char]]
list [Char]
dir
        ([[[Char]]] -> [[Char]]) -> IO [[[Char]]] -> IO [[Char]]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap [[[Char]]] -> [[Char]]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat (IO [[[Char]]] -> IO [[Char]]) -> IO [[[Char]]] -> IO [[Char]]
forall a b. (a -> b) -> a -> b
$ ([Char] -> IO [[Char]]) -> [[Char]] -> IO [[[Char]]]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
mapM (([Char] -> Bool) -> [Char] -> IO [[Char]]
listRecursive [Char] -> Bool
descend) [[Char]]
fns
    maybeDescend Bool
False [Char] -> Bool
_ [Char]
_ = [[Char]] -> IO [[Char]]
forall (m :: * -> *) a. Monad m => a -> m a
return []

-- | Walk the filesystem and stream (dir, fname).
walk :: (FilePath -> Bool) -> FilePath
    -> S.Stream (S.Of (FilePath, [FilePath])) IO ()
walk :: ([Char] -> Bool) -> [Char] -> Stream (Of ([Char], [[Char]])) IO ()
walk [Char] -> Bool
wantDir = [Char] -> Stream (Of ([Char], [[Char]])) IO ()
forall {m :: * -> *}.
MonadIO m =>
[Char] -> Stream (Of ([Char], [[Char]])) m ()
go
    where
    go :: [Char] -> Stream (Of ([Char], [[Char]])) m ()
go [Char]
dir = do
        ([[Char]]
dirs, [[Char]]
fnames) <- IO ([[Char]], [[Char]])
-> Stream (Of ([Char], [[Char]])) m ([[Char]], [[Char]])
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO ([[Char]], [[Char]])
 -> Stream (Of ([Char], [[Char]])) m ([[Char]], [[Char]]))
-> IO ([[Char]], [[Char]])
-> Stream (Of ([Char], [[Char]])) m ([[Char]], [[Char]])
forall a b. (a -> b) -> a -> b
$
            ([Char] -> IO Bool) -> [[Char]] -> IO ([[Char]], [[Char]])
forall (m :: * -> *) a.
Monad m =>
(a -> m Bool) -> [a] -> m ([a], [a])
partitionM ([Char] -> IO Bool
Directory.doesDirectoryExist ([Char] -> IO Bool) -> ([Char] -> [Char]) -> [Char] -> IO Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([Char]
dir</>))
                ([[Char]] -> IO ([[Char]], [[Char]]))
-> IO [[Char]] -> IO ([[Char]], [[Char]])
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< [Char] -> IO [[Char]]
Directory.listDirectory [Char]
dir
        ([Char], [[Char]]) -> Stream (Of ([Char], [[Char]])) m ()
forall (m :: * -> *) a. Monad m => a -> Stream (Of a) m ()
S.yield ([Char]
dir, [[Char]]
fnames)
        [[Char]]
dirs <- [[Char]] -> Stream (Of ([Char], [[Char]])) m [[Char]]
forall (m :: * -> *) a. Monad m => a -> m a
return ([[Char]] -> Stream (Of ([Char], [[Char]])) m [[Char]])
-> [[Char]] -> Stream (Of ([Char], [[Char]])) m [[Char]]
forall a b. (a -> b) -> a -> b
$ ([Char] -> [Char]) -> [[Char]] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map ([Char]
dir</>) ([[Char]] -> [[Char]]) -> [[Char]] -> [[Char]]
forall a b. (a -> b) -> a -> b
$ ([Char] -> Bool) -> [[Char]] -> [[Char]]
forall a. (a -> Bool) -> [a] -> [a]
filter [Char] -> Bool
wantDir [[Char]]
dirs
        [[Char]]
dirs <- IO [[Char]] -> Stream (Of ([Char], [[Char]])) m [[Char]]
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO [[Char]] -> Stream (Of ([Char], [[Char]])) m [[Char]])
-> IO [[Char]] -> Stream (Of ([Char], [[Char]])) m [[Char]]
forall a b. (a -> b) -> a -> b
$ if Bool
followLinks then [[Char]] -> IO [[Char]]
forall (m :: * -> *) a. Monad m => a -> m a
return [[Char]]
dirs
            else ([Char] -> IO Bool) -> [[Char]] -> IO [[Char]]
forall (m :: * -> *) a.
Applicative m =>
(a -> m Bool) -> [a] -> m [a]
filterM ((Bool -> Bool) -> IO Bool -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Bool -> Bool
not (IO Bool -> IO Bool) -> ([Char] -> IO Bool) -> [Char] -> IO Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> IO Bool
Directory.pathIsSymbolicLink) [[Char]]
dirs
        ([Char] -> Stream (Of ([Char], [[Char]])) m ())
-> [[Char]] -> Stream (Of ([Char], [[Char]])) m ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ [Char] -> Stream (Of ([Char], [[Char]])) m ()
go [[Char]]
dirs
    followLinks :: Bool
followLinks = Bool
False

-- * compression

-- | Read and decompress a gzipped file.
readGz :: FilePath -> IO (Either String ByteString.ByteString)
readGz :: [Char] -> IO (Either [Char] ByteString)
readGz [Char]
fn = ByteString -> IO (Either [Char] ByteString)
decompress (ByteString -> IO (Either [Char] ByteString))
-> IO ByteString -> IO (Either [Char] ByteString)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< [Char] -> IO ByteString
Lazy.readFile [Char]
fn

decompress :: Lazy.ByteString -> IO (Either String ByteString.ByteString)
decompress :: ByteString -> IO (Either [Char] ByteString)
decompress ByteString
bytes =
    (DecompressError -> IO (Either [Char] ByteString))
-> IO (Either [Char] ByteString) -> IO (Either [Char] ByteString)
forall e a. Exception e => (e -> IO a) -> IO a -> IO a
Exception.handle (Either [Char] ByteString -> IO (Either [Char] ByteString)
forall (m :: * -> *) a. Monad m => a -> m a
return (Either [Char] ByteString -> IO (Either [Char] ByteString))
-> (DecompressError -> Either [Char] ByteString)
-> DecompressError
-> IO (Either [Char] ByteString)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. DecompressError -> Either [Char] ByteString
forall {b}. DecompressError -> Either [Char] b
handle) (IO (Either [Char] ByteString) -> IO (Either [Char] ByteString))
-> IO (Either [Char] ByteString) -> IO (Either [Char] ByteString)
forall a b. (a -> b) -> a -> b
$
        ByteString -> Either [Char] ByteString
forall a b. b -> Either a b
Right (ByteString -> Either [Char] ByteString)
-> IO ByteString -> IO (Either [Char] ByteString)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ByteString -> IO ByteString
forall a. a -> IO a
Exception.evaluate (ByteString -> ByteString
Lazy.toStrict (ByteString -> ByteString
GZip.decompress ByteString
bytes))
    where handle :: DecompressError -> Either [Char] b
handle (DecompressError
exc :: Zlib.Internal.DecompressError) = [Char] -> Either [Char] b
forall a b. a -> Either a b
Left (DecompressError -> [Char]
forall a. Show a => a -> [Char]
show DecompressError
exc)

-- | Write a gzipped file.  Try to do so atomically by writing to a tmp file
-- first and renaming it.
--
-- Like @mv@, this will refuse to overwrite a file if it isn't writable.  If
-- the file wouldn't have changed, abort the write and delete the tmp file.
-- The mtime won't change, and the caller gets a False, which can be used to
-- avoid rebuilds.
writeGz :: Int -- ^ save this many previous versions of the file
    -> FilePath -> ByteString.ByteString -> IO Bool
    -- ^ False if the file wasn't written because it wouldn't have changed.
writeGz :: Int -> [Char] -> ByteString -> IO Bool
writeGz Int
rotations [Char]
fn ByteString
bytes = do
    [Char] -> IO ()
requireWritable [Char]
fn
    [Int] -> (Int -> IO ()) -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ [Int
0 .. Int
rotationsInt -> Int -> Int
forall a. Num a => a -> a -> a
-Int
1] ((Int -> IO ()) -> IO ()) -> (Int -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ [Char] -> IO ()
requireWritable ([Char] -> IO ()) -> (Int -> [Char]) -> Int -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> [Char]
forall a. Show a => a -> [Char]
rotation
    let tmp :: [Char]
tmp = [Char]
fn [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
".write.tmp"
    [Char] -> ByteString -> IO ()
Lazy.writeFile [Char]
tmp (ByteString -> IO ()) -> ByteString -> IO ()
forall a b. (a -> b) -> a -> b
$ ByteString -> ByteString
GZip.compress (ByteString -> ByteString) -> ByteString -> ByteString
forall a b. (a -> b) -> a -> b
$ ByteString -> ByteString
Lazy.fromStrict ByteString
bytes
    IO Bool -> IO Bool -> IO Bool -> IO Bool
forall (m :: * -> *) a. Monad m => m Bool -> m a -> m a -> m a
ifM ([Char] -> [Char] -> IO Bool
sameContents [Char]
fn [Char]
tmp)
        ([Char] -> IO ()
Directory.removeFile [Char]
tmp IO () -> IO Bool -> IO Bool
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
False) (IO Bool -> IO Bool) -> IO Bool -> IO Bool
forall a b. (a -> b) -> a -> b
$
        do
            [Int] -> (Int -> IO ()) -> IO ()
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ [Int
rotationsInt -> Int -> Int
forall a. Num a => a -> a -> a
-Int
1, Int
rotationsInt -> Int -> Int
forall a. Num a => a -> a -> a
-Int
2 .. Int
1] ((Int -> IO ()) -> IO ()) -> (Int -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Int
n ->
                IO () -> IO ()
forall a. IO a -> IO ()
Exceptions.ignoreEnoent_ (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
                    [Char] -> [Char] -> IO ()
Directory.renameFile (Int -> [Char]
forall a. Show a => a -> [Char]
rotation (Int
nInt -> Int -> Int
forall a. Num a => a -> a -> a
-Int
1)) (Int -> [Char]
forall a. Show a => a -> [Char]
rotation Int
n)
            -- Go to some hassle to ensure files are replaced atomically.
            Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Int
rotations Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
0) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> IO ()
forall a. IO a -> IO ()
Exceptions.ignoreEnoent_ (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
                [Char] -> [Char] -> IO ()
Posix.Files.createLink [Char]
fn (Integer -> [Char]
forall a. Show a => a -> [Char]
rotation Integer
0 [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
".tmp")
                [Char] -> [Char] -> IO ()
Directory.renameFile (Integer -> [Char]
forall a. Show a => a -> [Char]
rotation Integer
0 [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
".tmp") (Integer -> [Char]
forall a. Show a => a -> [Char]
rotation Integer
0)
            [Char] -> [Char] -> IO ()
Directory.renameFile [Char]
tmp [Char]
fn
            Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
    where
    rotation :: a -> [Char]
rotation a
n = [Char]
fn [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
"." [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> a -> [Char]
forall a. Show a => a -> [Char]
show a
n