Thank you! Indeed, I was able to re-implement the library deferring to MonoFoldable and MonoPointed and then collapse the whole thing to a single module with two functions that automatically work with all suitable input and output types.
In terms of the package (and since I'll be bumping the major version) I don't think the specialized modules are needed any more, since they're just a small subset of the combinatorial input/output space. Or is there a tangible benefit to users in providing more concretely typed versions for those (unusual?) cases where the return type is ambiguous? So, given:
type Input i e = (MonoFoldable i, Element i ~ e)
type Output o e = (MonoPointed o, Element o ~ e, Monoid o)
encode :: forall i o. (Input i Word8, Output o Char) => i -> o
decode :: forall i o. (Input i Char, Output o Word8) => i -> o
Does including and exposing the following...
encodeToString :: Input i Word8 => i -> [Char]
decodeToBytes :: Input i Char => i -> [Word8]
encodeBytes :: Output o Char => [Word8] -> o
decodeString :: Output o Word8 => [Char] -> o
encodeBytesToString :: [Word8] -> [Char]
decodeStringToBytes :: [Char] -> [Word8]
...add any value, being trivially defined as equal to the generic version? (Plus, the explosion doubles with another dimension like lazy vs. strict.) I don't see much advantage over e.g. (encode ws :: String) for the first case.