someFunc :: SomeColor -> ...
someFunc (SomeColor color) = ... (toRGB color) ...
vs having these functions accept an RGB instead of a SomeColor e.g. someFunc :: RGB -> ...
someFunc color = ... color ...
Then of course the caller would have to do the conversion someFunc (CMYToRGB color)The class method seems to also allow CMYK and RGB to be defined completely independently of one another for any given color type. Whether that's necessary for the use case is beyond my understanding, but it allows that.
I'd also imagine that giving colors their own types means that in the domain there's a small set of colors that are important, so a type class around this emphasizes the behaviors of these important colors rather than modeling a color system in general.
Let's say one day RGB is superseded... while you can convert RGB values to any new value, you end up with all these functions that want to take a color, but are taking an RGB value instead.
I do think that depending on the problem domain your method of treating colors as directly encoded values is valid, but maybe for a more robust color specific model than a color scheme application.
I think a good analogy would be how to model a generic currency system, rather than the pricing plans of a SaaS app.
The SaaS app will have some
data StartupTier = StartupTier
data ProTier = ProTier
data EnterpriseTier = EnterpriseTier
class IsPrice price where...
instance IsPriceTier StartupTier where
toUSD = const $ USD 5
Here it's not so much the currency system that we focus on than some discrete prices that we care about, that just so happen to convert to currencies. It also gives us the benefit of having tighter control on the different prices in different currencies rather than using a conversion function.Also that toRGB function has signature a -> RGB so it allows for a variable to perhaps dynamically calculate what RGB value it becomes... such as creating an instance of IsColor for something like "FavoriteColor" which could be pulled from the database.
In your example, you have two different calls to `someFunc` that are each eventually converting two different colors, so you might say something like:
let
somethingRed = someFunc (RGB 0xff 0x0 0x0)
somethingBlack = someFunc (cmykToRGB $ CMYK 0x00 0x00 0x00 0xff)
in (somethingRed, somethingBlack)
In this sort of situation there's not a strong difference between the two. There are a few other situations that, if we expect to encounter them, tip us in favor of considering existentials though. The big one from an API design standpoint is that your approach makes it a bit more work to allow the user to mix-and-match colors from different encodings.If you think about the `ThemeInstance` in the article, it's a `Map String SomeColor`. If you wanted to eliminate the existential type you'd probably end up with something like:
newtype ThemeInstance theme encoding = ThemeInstance
{ getThemeInstance :: Map String encoding }
So you can have a `ThemeInstance RGB` or a `ThemeInstance CYMK`, but the user isn't allowed to freely mix-and-match.Of course, you could use an ADT rather than an existential type here:
data SomeColor = SomeRGBColor RGB | SomeCMYKColor CMYK
myThemeInstance = ThemeInstance $ fromList
[ ("red", SomeRGBColor (RGB 255 0 0))
, ("black", SomeCMYKColor (CMYK 0 0 0 255)) ]
Now you can case match against the constructors in `SomeColor` if you want: myFunc someColor = case someColor of
SomeRGBColor rgb -> someFunc rgb
SomeCMYKColor cmyk -> someFun (cmykToRGB cmyk)
But if this is library code, you've now created a closed encoding- meaning that a user who wants to work with YUV or HSL or some esoteric encoding can't do it.At the end of the day, it is a design decision. If you opt not to go with existentials and typeclasses you'll end up with a different sort of API with different benefits and limitations.