Skip to content
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

[RFC] Find best solution for dealing with (phantom) type variables #45

Open
chshersh opened this issue Feb 22, 2019 · 3 comments
Open
Labels
help wanted Extra attention is needed question Further information is requested

Comments

@chshersh
Copy link
Contributor

chshersh commented Feb 22, 2019

It's highly desired to have datatypes with phantom type variables. Like this one:

newtype Id a = Id { unId :: Text }

But there are problems with using such data types in Elm. For example, if we have the following Haskell data type:

data User = User
    { userId   :: Id User
    , userName :: Text
    }

then elm-street generates the following Elm definition:

type Id a
    = Id String

type alias User =
    { id : Id User
    , name : String
    }

Unfortunately, this is invalid Elm (version 0.19) and produces the following error:

This type alias is recursive, forming an infinite type!

51| type alias User =
               ^^^^
When I expand a recursive type alias, it just keeps getting bigger and bigger.
So dealiasing results in an infinitely large type! Try this instead:

    type User =
        User
            { id : Id User, name : String }

Hint: This is kind of a subtle distinction. I suggested the naive fix, but I
recommend reading <https://elm-lang.org/0.19.0/recursive-alias> for ideas on how
to do better.

This is not restricted only to phantom type variables, we can't have type aliases that reference themselves. So we need to do better. I would like to discuss possible solutions to this problem and choose the most ergonomic one.

Solution 1: Drop phantom type variables on frontend and tell custom compiler error [current solution]

Instead of preserving phantom type variables on the frontend, we just drop them:

type alias Id =
    { id : String
    }
  
type alias User =
    { id : Id
    , name : String
    }

If you derive Elm typeclass for data types with type variables, the following compiler error is shown:

screenshot 2019-02-22 at 12 23 31 pm

Solution 2: Generate fake data type for using in phantom variables

Type aliases can refer to themselves. But they can refer to some other types.

type Id a
    = Id String

type User_ = User_
type alias User =
    { id : Id User_
    , name : String
    }

Solution 3: Always create type instead of type alias

Just don't generate type aliases at all. Always create type:

type Id a
    = Id String

type User = User
    { id : Id User
    , name : String
    }

This might be less convenient to use.

Solution 4: Generate type instead of type alias only when the type alias reference itself

This is hard to implement, because we need to implement algorithms for finding cycles in data type definitions.

@chshersh chshersh added help wanted Extra attention is needed question Further information is requested labels Feb 22, 2019
@turboMaCk
Copy link
Member

turboMaCk commented Apr 11, 2020

I would like to propose another solution. We can take advantage of extensible records to implement this.

See example bellow:

type Id a = Id String

type alias WithId r = { r | id : Id r }

type alias User = WithId
        { name : String
        , age : Int
        }

user : User
user =
    { id = Id "1"
    , name = "Jane Doe"
    , age = 54
    }

in repl:

> user
{ age = 54, id = Id "1", name = "Jane Doe" } 
    : User

How does it work?

With usage of extensible record, we exclude the id filed from the record which is used as a parameter of Id type. Since there is no Id a in a, there is no recursion in type alias.

This illustrates what is going on (repl):

> user.id
Id "1" : Id { age : Int, name : String }

Drawback

  1. By definition 2 aliases with the same structure are just synonyms for the same thing. If 2 records has same fields, their IDs will also be of same type.
  2. possibly hard to implement

@turboMaCk
Copy link
Member

turboMaCk commented Sep 24, 2021

I just come up with another trick using extensible records that might be even more useful:

type Id a
    = Id String


type OwnId
    = OwnId String


type alias User =
    { id : OwnId
    , name : String
    , age : Int
    }


getId : { a | id : OwnId } -> Id { a | id : OwnId }
getId rec =
    case rec.id of
        OwnId str ->
            Id str


user : User
user =
    { id = OwnId "1"
    , name = "Jane Doe"
    , age = 54
    }


userId : Id User
userId =
    getId user

loaded to repl:

> user.id
OwnId "1" : OwnId
> userId
Id "1" : Id User

All we need to do is to generate OwnId by elm street. Then in elm we can use getId to get type safe Id User type to work with.

Update

I also managed to break this:

type Own a
    = Own a


type Tag a t
    = Tag a


type alias OwnId =
    Own String


type alias Id t =
    Tag String t


getOwn : (a -> Own b) -> a -> Tag b a
getOwn get val =
    case get val of
        Own a ->
            Tag a


getId : { a | id : OwnId } -> Id { a | id : OwnId }
getId =
    getOwn .id


type alias Post =
    { id : OwnId
    , text : String
    , userId : Id User
    }


type alias User =
    { id : OwnId
    , name : String
    , age : Int
    , posts : List (Id Post)
    }


post : Post
post =
    { id = Own "1"
    , text = "FooBar"
    , userId = getId user
    }


user : User
user =
    { id = Own "1"
    , name = "Jane Doe"
    , age = 54
    , posts = [ getId post ]
    }

will again result in recursion:

This type alias is part of a mutually recursive set of type aliases.

39| type alias User =
               ^^^^
It is part of this cycle of type aliases:

    ┌─────┐
    │    User
    │     ↓
    │    Post
    └─────┘

You need to convert at least one of these type aliases into a `type`.

Note: Read <https://elm-lang.org/0.19.1/recursive-alias> to learn why this
`type` vs `type alias` distinction matters. It is subtle but important!

@silky
Copy link

silky commented Dec 21, 2023

Did anyone make any progress here?

Especially in the simple case of a datatype like:

data A a = X a | Y a | ...

especially when I can, at compile-time, tell elm-street what concrete a values I want to export:

type Types = '[ A Text, A Int ]

?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants