New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phantom Types (contributors, please take note!) #375

Open
rtfeldman opened this Issue Dec 25, 2017 · 4 comments

Comments

Projects
None yet
3 participants
@rtfeldman
Copy link
Owner

rtfeldman commented Dec 25, 2017

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 a Value parameterized on an extensible record. I didn't realize you could do this, but you totally can! This means color will accept it, even though color accepts a non-extensible record, because Value { provides | rgb : Supported } unifies with Value { 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! 😸

@tolgap

This comment has been minimized.

Copy link
Collaborator

tolgap commented Feb 20, 2018

@rtfeldman Doesn't this commit elm/compiler@5fb82e2 "break" phantom types?

type Value a -- `a` is now an unbound type variable error?
    = Value String
@rtfeldman

This comment has been minimized.

Copy link
Owner Author

rtfeldman commented Feb 20, 2018

according to Evan:

It’s the same as 0.18
type F = A a | B b is the bad thing
but type F a b = A | B is still allowed

@rtfeldman rtfeldman changed the title Phantom Values (contributors, please take note!) Phantom Types (contributors, please take note!) Mar 22, 2018

@owanturist

This comment has been minimized.

Copy link

owanturist commented Apr 22, 2018

@rtfeldman could you add link to the issue at README? Maybe on the top that contributors see it first.

@rtfeldman

This comment has been minimized.

Copy link
Owner Author

rtfeldman commented Apr 22, 2018

@owanturist Added to ISSUE_TEMPLATE.md and PULL_REQUEST_TEMPLATE.md in 3fd1142

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment