(I'm totally rewriting this question to give it a better focus; you can see the history of changes if you want to see the original.)
Let's say I have two modules:
- One module defines the function
inverseAndSqrt
. What this function actually does is not important; what is important is that it returns none, one, or both of two things in a way that the client can distinguish which one is which;
module Module1 (inverseAndSqrt) where
type TwoOpts a = (Maybe a, Maybe a)
inverseAndSqrt :: Int -> TwoOpts Float
inverseAndSqrt x = (if x /= 0 then Just (1.0/(fromIntegral x)) else Nothing,
if x >= 0 then Just (sqrt $ fromIntegral x) else Nothing)
- another module defines other functions depending on
inverseAndSqrt
and on its type
module Module2 where
import Module1
fun :: (Maybe Float, Maybe Float) -> Float
fun (Just x, Just y) = x + y
fun (Just x, Nothing) = x
fun (Nothing, Just y) = y
exportedFun :: Int -> Float
exportedFun = fun . inverseAndSqrt
What I want to understand from the perspective of design principle is: how should I interface Module1
with other modules (e.g. Module2
) in a way that makes it well encapsulated, reusable, etc?
The problems I see are
- I could one day decide that I don't want to use a pair to return the two results anymore; I could decide to use a 2 elements list; or another type which is isomorphic (I think this is the right adjective, isn't it?) to a pair; if I do this, all client code will break
- Exporting the
TwoOpts
type synonym doesn't solve anything, asModule1
could still change its implementation thus breaking client code. Module1
is also forcing the type of the two optionals to be the same, but I'm not sure this is really relevant to this question...
How should I design Module1
(and thus edit Module2
as well) such that the two are not tightly coupled?
One thing I can think of is that maybe I should define a typeclass
expressing what "a box with two optional things in it" is, and then Module1
and Module2
would use that as a common interface. But should that be in both module? In either of them? Or in none of them, in a third module? Or maybe such a class
/concept is not needed?
I'm not a computer scientist so I'm sure that this question highlights some misunderstanding of mine due to lack of experience and theoretical background. Any help filling the gaps is welcome.
Possible modifications I'd like to support
- Related to what chepner suggested in a comment to his answer, at some point I might want to extend the support from 2-tuple things to both 2- and 3-tuple things, having different accessor names for them, suche as
get1of2
/get2of2
(let's say these are the name we use when we first designModule1
) vsget1of3
/get2of3
/get3of3
. - At some point I would also be able to complement this 2-tuple-like type with something else, for instance an optional containing
Just
the sum¹ of the two main contents only if they are bothJust
s, or aNothing
if at least one of the two main contents is aNothing
. I guess in this case the internal representation of this class would be something like((Maybe a, Maybe a), Maybe b)
(¹ The sum is really a stupid example, so I've usedb
here instead ofa
to be more general than the sum would require).
Don't define a simple type alias; this exposes the details of how you implement
TwoOpts
.Instead, define a new type, but don't export the data constructor, but rather functions for accessing the two components. Then you are free to change the implementation of the type all you like without changing the interface, because the user can't pattern-match on a value of type
TwoOpts a
.and
Later, when you realize that you've reimplemented the type product, you can change your definitions without affecting any user code.