Holmusk / elm-street

:deciduous_tree: Crossing the road between Haskell and Elm

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

new type deriving broken

turboMaCk opened this issue · comments

Deriving instances for newtype is broken in cases like:

newtype TopicKey = TopicKey
        { unTopicKey :: Text
        } deriving stock (Show, Eq, Generic)
          deriving (FromJSON, ToJSON, Elm) via ElmStreet TopicKey

Json instances expect the value to be wrapped in tagged object while elm decoders are generated for json string values. Decoding therefore fails (at runtime).

Two other ways to derive the value work

newtype TopicKey = TopicKey
        { unTopicKey :: Text
        } deriving stock (Show, Eq, Generic)
          deriving newtype (FromJSON, ToJSON)
          deriving (Elm) via ElmStreet TopicKey

This generates instances where both expect wrapping object to be present.

newtype TopicKey = TopicKey
        { unTopicKey :: Text
        } deriving stock (Show, Eq, Generic)
          deriving (FromJSON, ToJSON) via Text
          deriving (Elm) via ElmStreet TopicKey

Derives the instance where json values are not boxed in json representation.

I think the later option is better in most cases but there is a disadvantage for types which might start as a newtype and later be changed to full data record. This often happens as hlint complains about data with single field. With 2nd approach the JSON representation in not compatible between newtype and data so changing types from newtype to data is breaking change for the api.

Related Issues

@dtaskoff in meantime can you use one of the work-around mentioned above?

Sure, it's just that I changed a type from data to a newtype, and spent some time wondering why it didn't work. It's even written in the documentation already (to use deriving newtype (FromJSON, ToJSON), and deriving anyclass Elm), so it's only a matter of convenience.

cool. There is another related but slightly different issue which is that elm-street likes to generate record aliases for new types on elm side. But semantically it would be better to better to generate regular type since elm compiler can unbox these in compile time much like Haskell does for new types. It can't do it for records though because records are real records in elm, not just field accessors like in Haskell.

I'm not sure that it's a good idea to translate Haskell's newtypes into Elm's type aliases, since in Elm they are really just aliases, so type alias X = Int, and type alias Y = Int are indistinguishable.

it should not be alias. elm equivalent of newtype Foo a = Foo { unFoo :: a } is type Foo a = Foo a. There are 2 differences in type systems in play here:

  1. haskell requires newtype for runtime unboxed values. Elm implicitly unboxes all values it can for type.
  2. haskell records are just types with field accessors. Elm records are aliases to record type which has row polymorphism (can't be unboxed).

so generating types elm can unbox would not be less type safe on elm side. It would be more type safe because it would generate distinct type rather than alias so newtype Foo = Foo { fooVal :: Int } and newtype Bar = Bar { barVal :: Int } would not be the same alias on elm side as they are now.

My bad, I misread type for type alias in your previous comment.

no worries. this terminology is super confusing between Elm and Haskell..

  • data vs type
  • type vs type alias

that's a recipe for misunderstanding.


I will try to bring this issue up internally at Holmusk but expect this issue will take some time to resolve. There is a lot of code in the wild which need to keep supporting without introducing major breakages.