4 ms·
I understand that, but at some point someone will call toRGB. It seems like for any function that calls toRGB, we have a choice of using the existential type fo
by smegma2 5y ago
I understand that, but at some point someone will call toRGB. It seems like for any function that calls toRGB, we have a choice of using the existential type for this e.g.
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)
- chrischen 5y agoIt's likely that in this simple example the practical differences may be nonexistent. But if you think about the types represented and what they mean, RGB is not a color, but an encoding of a color. So if some function is supposed to take a color and RGB is used as a proxy for a color, you may very well be able to skip the abstraction but it would need to be refactored if you ever evolve into using the concept of a color and its presentation form in different ways. 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.
- rebeccaskinner 5y agoOne of the reasons we might want to pick an existential type is that it lets us (or the users of our library) compose a theme using a number of different colors of different types. 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.
- smegma2 5y agoAh, that all makes sense. Thanks for the answer, and great article!