[Haskell-cafe] Design your modules for qualified import

Denis Bueno dbueno at gmail.com
Sat Jun 7 10:59:57 EDT 2008


On Fri, Jun 6, 2008 at 2:35 PM, Andrew Coppin
<andrewcoppin at btinternet.com> wrote:
> Johan Tibell wrote:
>> 3. Lack of common interfaces.
>>
>
> Yes.
>
> It's really quite frustrating that it is 100% impossible to write a single
> function that will process lists, arrays, sets, maps, byte strings, etc. You
> have to write several different versions. OK, so some functions really don't
> make sense for a set because it's unordered, and some functions don't make
> sense for a map, and so forth. But for example, if I write some complicated
> algorithm that uses a list to store data and I change that to a set instead,
> I now have to wade through the function changing every operation from a
> list-op into a set-op. It's really very annoying!

I can think of at least two ways I have been able to keep generic code
generic, in the way you describe:

1.  Use the Data.Foldable interface.  Lists, Sets, Sequences and Trees
are all foldable.  This doesn't help with your complaint about arrays
and bytestrings, but it's something.  You can do a lot with Foldable.

2. Factor out the operations *your app uses* into a typeclass.  For
some code I was writing I wrote the following interface

-- | Represents a container of type @t@ storing elements of type @a@ that
-- support membership, insertion, and deletion.
class Ord a => Setlike t a where
    -- | The set-like object with an element removed.
    without  :: t -> a -> t
    -- | The set-like object with an element included.
    with     :: t -> a -> t
    -- | Whether the set-like object contains a certain element.
    contains :: t -> a -> Bool

[Aside: This could be made H98 easily, but I didn't need it to be.]

By implementing this class for various types (sets and lists, as well
as a few others I used) I achieved the implementation-agnosticism you
described at fairly low cost.  If I needed to change out a list for an
array, I only changed the code that creates the initial data
structure, or the signature in the actual record containing the type
--- in other words, I only changed what needed to be changed, and the
rest was inferred.

The reason I think it makes sense for *me* to have created and
implemented this interface, instead of a library writer, is that for
each task that is data-structure agnostic in some respect, you will
need a *different* set of operations.  I know which ones I need, so I
should make only those generic.  The library writer, to be generic,
would have to write interfaces for all combinations of operations one
might care about (too many for this not to be a waste of time) and
that *still* wouldn't be good enough, because certain operations have
slightly differing semantics (or signatures) that make genericity
inadvisable (e.g. Set.map does not preserve the length of the input
list, but List.map does.  If your code relies on this rule about
"map", it is bad to use an interface with a "generic" map).

Perhaps there are certain, small, common interfaces --- like Setlike
--- that could be agreed upon.  But some people will always clamor
that it doesn't fit their application, or doesn't have the right
methods.  Which I think is okay, if we don't ask too much of standard
libs.

-- 
Denis


More information about the Haskell-Cafe mailing list