Search code examples
haskellvariadic-functions

How to concatenate variable arguments in Haskell?


Shell-monad supports variable arguments, however I couldn't find a way to pass a list of such arguments to append. It might be possible to workaround with a function construct present in that library, but I'd like to ask about the general problem.

I have vaguely understood that the "varargs" mechanism is implemented by function composition and recursion is terminated though use of type class inference.

Using that library as an example, I'm wondering if it's possible to treat arguments as "first class" such as assigning two arguments to a variable.

Here's an (incorrect) example that shows my intent.

Prelude Control.Monad.Shell Data.Text> f xs = cmd "cat" xs
Prelude Control.Monad.Shell Data.Text> let a = static ("a" :: Text)
Prelude Control.Monad.Shell Data.Text> let a2 = [a,a]
Prelude Control.Monad.Shell Data.Text> f a2

<interactive>:42:1: error:
    • Could not deduce (Param [Term Static Text])
        arising from a use of ‘f’
      from the context: CmdParams t2
        bound by the inferred type of it :: CmdParams t2 => t2
        at <interactive>:42:1-4
    • In the expression: f a2
      In an equation for ‘it’: it = f a2

Solution

  • Nothing a little polymorphic recursion can't fix:

    cmdList :: (Param command, Param arg, CmdParams result) =>
        command -> [arg] -> result
    cmdList command = go . reverse where
        go :: (Param arg, CmdParams result) => [arg] -> result
        go [] = cmd command
        go (arg:args) = go args arg
    

    Try it in ghci:

    > Data.Text.Lazy.IO.putStr . script $ cmdList "cat" ["dog", "fish"]
    #!/bin/sh
    cat dog fish
    

    It requires all the arguments given to cmdList have the same type, though it does still accept additional arguments of other types not in list form afterwards.

    If you're willing to turn on extensions, you can even have it accept lists in multiple positions, each of potentially different types.

    list :: (Param arg, CmdParams result) =>
        (forall result'. CmdParams result => result') ->
        [arg] -> result
    list f [] = f
    list f (arg:args) = list (f arg) args
    

    An example of using it:

    > T.putStr . script $ list (list (cmd "cat") ["dog", "fish"] "bug") ["turtle", "spider"]
    #!/bin/sh
    cat dog fish bug turtle spider
    

    The previous cmdList can be defined in terms of it as cmdList command = list (cmd command). (N.B. cmdList = list . cmd does not work!)

    Accepting lists that contain different types is noisier, but possible with existential types.

    data Exists c where Exists :: c a => a -> Exists c
    
    elist :: CmdParams result =>
        (forall result. CmdParams result => result) ->
        [Exists Param] -> result
    elist f [] = f
    elist f (Exists arg:args) = elist (f arg) args
    

    But look how annoying it is to use:

    > T.putStr . script $ elist (cmd "cat") [Exists "dog", Exists "fish"]
    #!/bin/sh
    cat dog fish
    

    The previous list can be defined in terms of it via list f = elist f . map Exists.