elm-css
elm-css copied to clipboard
Phantom Types (contributors, please take note!)
I'm working on having Css.Value
use phantom types. (More on what this means below.)
If you're using elm-css
, you may not even notice this change when it comes out (except that you may notice things running faster), but if you're contributing to current master
, this will almost certainly lead to merge conflicts - so watch out! Any large PR will likely need numerous changes once this is finished, and I would strongly recommend opening an issue before making a PR so we can coordinate.
You can follow progress on the phantom-types
branch. This one is gonna take awhile to finish. 😅
The Change
On current master
, Css.Value
is defined like this:
type alias Value compatible =
{ compatible | value : String }
On the phantom-values
branch it's instead defined like this:
type Value a
= Value String
A union type with a type variable which doesn't appear in any constructors is known as a phantom type. I'm somewhat annoyed that they have such a cool name, because it makes me feel like I should use them more often...when in reality, they are useful under very rare circumstances.
However, elm-css
happens to be one of them!
What the phantom type lets us do is move our compatibility information from runtime values to compile-time values. For example:
type alias ColorValue compatible =
{ compatible | value : String, color : Compatible }
This can now become:
type alias ColorValue compatible =
Value { compatible | value : String, color : Compatible }
The difference is subtle, but impactful. We still get all the same compile-time verification as before, but whereas before we actually had to instantiate all those fields in real objects, the runtime representation of Value
is now always a single String
constructor: type Value a = Value String
. All the compatibility checking information now exists at compile time only.
If that were the only part of the refactor, though, that wouldn't be a terribly big change. The big change is shifting around how the extensible records work.
Much Nicer Error Messages
Credit to @ianmackenzie for showing me this. Switching around which records are extensible and which ones are not can result in much nicer documentation and compile-time error messages for elm-css
!
For example, let's consider how color
and rgb
interact.
Status Quo
Here's how these are defined right now.
color : ColorValue compatible -> Style
rgb : Int -> Int -> Int -> Color
type alias Color =
ColorValue { red : Int, green : Int, blue : Int, alpha : Float }
type alias ColorValue compatible =
{ compatible | value : String, color : Compatible }
After the Change
Here's how they're defined on the phantom-values
branch.
color :
Value
{ rgb : Supported
, rgba : Supported
, hsl : Supported
, hsla : Supported
, hex : Supported
}
-> Style
rgb : Int -> Int -> Int -> Value { provides | rgb : Supported }
Yep,
rgb
returns aValue
parameterized on an extensible record. I didn't realize you could do this, but you totally can! This meanscolor
will accept it, even thoughcolor
accepts a non-extensible record, becauseValue { provides | rgb : Supported }
unifies withValue { rgb : Supported, rgba : Supported, ...etc }
in the type checker.mindblown.gif
The first benefit of this is that its type signature is much more useful than before.
What can I pass to the color
function? Values returned by rgb
, rgba
, hsl
, hsla
, and hex
, just like it says in color
's type signature. I can instantly go look up the docs for any of those if I want to know what they do.
The second benefit is that we get much more helpful error messages. Before, if we tried to do color (px 10)
here's what we'd get:
-- TYPE MISMATCH ---------------------------------------------
The argument to function `color` is causing a mismatch.
4| color (px 10)
^^^^^
Function `color` is expecting the argument to be:
Css.ColorValue compatible
But it is:
Css.Px
Hint: The record fields do not match up. One has color. The other has
absoluteLength, calc, flexBasis, fontSize, length, lengthOrAuto,
lengthOrAutoOrCoverOrContain, lengthOrMinMaxDimension, lengthOrNone,
lengthOrNoneOrMinMaxDimension, lengthOrNumber,
lengthOrNumberOrAutoOrNoneOrContent, numericValue, textIndent, unitLabel, and
units.
On the phantom-values
branch, here's what we get instead:
-- TYPE MISMATCH ---------------------------------------------
The argument to function `color` is causing a mismatch.
4| color (px 10)
^^^^^
Function `color` is expecting the argument to be:
Css.Value { hex : ..., hsl : ..., hsla : ..., rgb : ..., rgba : ... }
But it is:
Css.Value { provides | px : ... }
Hint: The record fields do not match up. One has hex, hsl, hsla, rgb, and rgba.
The other has px.
With a small bit of learning, we can get a ton more out of this error message. It's telling us that the value we used was constructed with the px
function, and that it was expecting a value constructed using hex
, hsl
, hsla
, rgb
, or rgba
instead.
That's way more directly useful than seeing stuff like lengthOrNumberOrAutoOrNoneOrContent
with the status quo.
The Plan
I'm gonna switch everything in the Css
module to use both phantom types as well as this new style of extensible records vs. non-extensible records.
Even though things will fit together the same way when it's done—so glad we have an extensive test suite to guard against regressions!—this is not a direct one-to-one transformation. I can't write a script to automate it. It's just gonna take time. 😄
Since I have to touch so many functions by hand anyway, while I'm at it, I'm also making sure everything has real documentation (too many {-| -}
docs in the current release), and I'm also knocking out some easy performance optimizations along the way.
It's gonna be sweet! 😸
@rtfeldman Doesn't this commit https://github.com/elm-lang/elm-compiler/commit/5fb82e2d3fd6c62b92b5548cdf119d6f24787a5f "break" phantom types?
type Value a -- `a` is now an unbound type variable error?
= Value String
according to Evan:
It’s the same as 0.18
type F = A a | B b
is the bad thing buttype F a b = A | B
is still allowed
@rtfeldman could you add link to the issue at README? Maybe on the top that contributors see it first.
@owanturist Added to ISSUE_TEMPLATE.md
and PULL_REQUEST_TEMPLATE.md
in https://github.com/rtfeldman/elm-css/commit/3fd114227797f1dfb76bb49ff0ae27aa010fc1cf