<!-- language-all: haskell -->

My suggestion would be to declare an opaque `newtype` that hides the actual implementation and implement type classes that could be useful to your clients and that you can guarantee to be stable in the future. And you can also export functions that allow conversion of your data type to some standard ones.

This forces clients to use only functions or instances you provide and document, and also allows clients to use standard API such as `Foldable`:

    module MyMod
        ( MyResult()
        , resultToSet   -- by hiding the internals and exporting the accessor
                        -- explicitly, you can always change the internal
                        -- representation and replace the accessor a function
                        -- with the same signature
        ) where

    import Data.Monoid
    import qualified Data.Foldable as F
    import qualified Data.Set as S

    newtype MyResult a = MyResult { resultToSet :: S.Set a }

    -- For example, your clients could use `Foldable` and `Monoid` instances:

    instance F.Foldable MyResult where
        foldMap f = F.foldMap f . resultToSet

    instance (Ord a) => Monoid (MyResult a) where
        mempty = MyResult mempty
        mappend (MyResult x) (MyResult y) = MyResult (x `mappend` y)

**Update:** If you later decide to change the internal representation to `HashSet`, the module would look like this:

    module MyMod
        ( MyResult()
        , resultToSet   -- by hiding the internals and exporting the accessor
                        -- explicitly, you can always change the internal
                        -- representation and replace the accessor a function
                        -- with the same signature
        ) where

    import Data.Monoid
    import qualified Data.Foldable as F
    import Data.Hashable
    import qualified Data.HashSet as HS
    import qualified Data.Set as S

    newtype MyResult a = MyResult { resultToHashSet :: HS.HashSet a }

    -- For example, your clients could use `Foldable` and `Monoid` instances:

    instance F.Foldable MyResult where
        foldMap f = F.foldMap f . resultToHashSet

    instance (Eq a, Hashable a) => Monoid (MyResult a) where
        mempty = MyResult mempty
        mappend (MyResult x) (MyResult y) = MyResult (x `mappend` y)

    -- Reimplementation of functions from the previous version

    resultToSet :: (Ord a) => MyResult a -> S.Set a
    resultToSet = S.fromList . HS.toList . resultToHashSet

The external interface is almost the same (the only change is that the `Monoid` instance now has slightly different constraints).

----

Another way that would give clients much flexibility is to let the collection be fully abstract. This can be nicely expressed using [monoids](https://en.wikipedia.org/wiki/Monoid). The output of your function will be a polymorphic monoid, we just need a way how to create singletons. For example, if we want to generalize `enumFromTo`, we could write

    enumCol :: (Enum a, Monoid c) => a -> a -> ((a -> c) -> c)
    enumCol f t singleton = mconcat . map singleton $ enumFromTo f t

It's then the client responsibility to use a monoid with effective `mappend` operation.

With `RankNTypes` the return type could be also encapsulated using `type` as

    type OutCol e c = (Monoid c) => (e -> c) -> c

The additional benefit is that in _some_ cases this can be very effective thanks to laziness. For example, the following returns immediately:

    enumCol 0 (10^20) (First . Just)