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

Full Example for type alias custom type #124

Open
bitc opened this issue Mar 26, 2021 · 14 comments
Open

Full Example for type alias custom type #124

bitc opened this issue Mar 26, 2021 · 14 comments

Comments

@bitc
Copy link

bitc commented Mar 26, 2021

The documentation mentions that we can have the following ADL type:

newtype Date = String;

be converted to the following Haskell type:

type Date = Day

Can you please provide a full example?

I've tried this:

  @HaskellCustomType {
    "haskellname" : "Data.Time.Calendar.Day",
    "haskellimports" : ["qualified Data.Time.Calendar"],
    "insertCode" : ["type Date = Data.Time.Calendar.Day"],
    "generateOrigADLType" : "",
    "structConstructor" : "Dummy",
    "unionConstructors" : []
  }
  newtype Date = String;

But I get the haskell error: No instance for (AdlValue Date)

I think I need to specify the functions for converting Day to/from a string. But I can't figure this out.

Thank you

@timbod7
Copy link
Collaborator

timbod7 commented Mar 26, 2021

You have to write the instance for AdlValue to implement the de/serialization to/from json.

I'll extend the test to a fully worked example.

@bitc
Copy link
Author

bitc commented Mar 26, 2021

Thank you. Do you recommend creating an orphan instance for Data.Time.Calendar.Day, or is there some way to add the instance into the AdlValue source file?

@timbod7
Copy link
Collaborator

timbod7 commented Mar 28, 2021

You either need to use a newtype or create an orphan instance.

I don't think it would be feasible to inject the value into the Value.hs source file.

@bitc
Copy link
Author

bitc commented Apr 1, 2021

Thank you. The "custom types" feature of ADL is amazing and what I believe sets it apart from protocol-buffers and the others (as well as other great ADL features like generics).

But I have 2 suggestions that I think would improve things for custom types:

  1. Instead of requiring a type-class for the custom type, in the annotation specify two functions:

     toType = "Data.Time.Calendar.Day.fromString"
     fromType = "Data.Time.Calendar.Day.toString"
    

    The code generator would then directly call those functions inside the generated serialization code. This way, there is no need to deal with newtypes or orphan instances. This is a simple approach that I believe would work great for all languages.

  2. The above two functions should have a type CustomType -> T and T -> CustomType, where T is the backing ADL type. In the current implementation, custom types serialize to/from JSON. This is unnecessarily tightly coupled to the current serialization ADL JSON implementation. But in the future ADL will (hopefully) develop a binary serialization (and other formats). If custom types serialize to/from the backing ADL type instead of JSON, then they will transparently work with any serialization format.

Thank you

@timbod7
Copy link
Collaborator

timbod7 commented Apr 2, 2021

The issue with newtypes and/or orphan instances is annoying - it applies both in the haskell and the rust language backends.

However, the implementation of serialization for generics current relies on type classes/traits so that a single generic serializer works with all instances (including custom types). I think this precludes your suggested approach of

code generator would then directly call those functions inside the generated serialization code

FWIW, in the java and typescript backends we rely on reified instances of interfaces equivalent to the type classes (eg JsonBinding and JsonBinding in json.ts). This approach lacks some of the convenience of using type classes, but avoids the need for newtypes or orphans.

Perhaps the haskell backend could also use a similar approach of reified serializers, but I sure people with then start asking "why aren't you using typeclasses?" .

@timbod7
Copy link
Collaborator

timbod7 commented Apr 2, 2021

But in the future ADL will (hopefully) develop a binary serialization

See #75

I'm not against the idea, just haven't had a driving need. The json serialization has been surprisingly versatile.

@bitc
Copy link
Author

bitc commented Apr 8, 2021

Thank you for the response. I will take a look at the Java and TypeScript implementations (But the TypeScript implementation doesn't support custom types yet, right?).

In the meantime I would like to respond to:

Perhaps the haskell backend could also use a similar approach of reified serializers, but I sure people with then start asking "why aren't you using typeclasses?" .

I am not certain that people would ask that. A large portion of the Haskell community is a await of the argument against using typeclasses for serialization. For example, the tomland TOML serialization library doesn't use typeclasses. They say:

A function-based approach for the description of how to convert Haskell data types to/from TOML instead of a typeclasses-based approach. This approach has a lot of pleasant advantages for decoding libraries: no orphan instances and no ambiguity when there are multiple options for conversion. Here tomland goes along with libraries like hedgehog, sv and waargonaut.

https://kowainik.github.io/posts/2019-01-14-tomland#key-concepts

As mentioned, warrgonaut and sv also are typeclassless and have good explanations about the advantages of a typeclassless approach.

I'm not against the idea, just haven't had a driving need [for binary serialization]. The json serialization has been surprisingly versatile.

I agree that the json serialization works well. But even if we end up staying with JSON, I still believe that the approach of custom types serializing to/from ADL types (instead of JSON) is conceptually cleaner and technically better. For example, ADL has a "ByteVector" type. If I make a custom type "Image" that is backed by an ADL "ByteVector", then in the JSON approach I now need to serialize/deserialize to JSON, but JSON doesn't have builtin support for bytes. So I have to look at the implementation of ADL and see how it encodes this in JSON (I'm guessing it uses something like base64 encoded strings?). So now I have to write my own base64 encoder/decoder (and hope that I don't make a bug and that I implement it exactly the same as ADL). And also I am tightly coupled with the ADL implementation (maybe ADL would like to switch to base122 encoding in the future?). With my approach, to implement my custom type I convert to/from a Haskell "ByteString" (or the equivalent in other languages), which is much simpler and more intuitive in my opinion.

@timbod7
Copy link
Collaborator

timbod7 commented Apr 11, 2021

A large portion of the Haskell community is a await of the argument against using typeclasses for serialization.
...
As mentioned, warrgonaut and sv also are typeclassless and have good explanations about the advantages of a typeclassless approach

You've convinced me. The current serialization approach in the generated haskell dates back to 2014 (!). I'm not so actively following the haskell ecosystem these days, and I wasn't aware of the typeclassless approach taken by other libraries.

I think it would be better indeed to modify the haskell and rust code generators to use this approach, making them consistent with the other language backends.

But even if we end up staying with JSON, I still believe that the approach of custom types serializing to/from ADL types (instead of JSON) is conceptually cleaner and technically better.

Note that this approach is already supported. As part of the custom type annotation (see generateOrigAdlType), you can request that the original serialization code still be generated. That way you can implement your de/serialization logic for your custom type as a conversion to the generated type, followed by a call to the generated de/serializer.

There is value in having explicit control over the serialization for custom types. We often use this as a means of getting deserialization backwards compatibility. If I have a existing type X for which we have persisted serialized data, and I want to extend or modify this type in a way that "breaks" the serialization. I can use a custom type definition to provide a deserializer that understand both the old and the new json, and converts either to the new X type.

@bitc
Copy link
Author

bitc commented Apr 20, 2021

I'm not sure if this is related. But I think this would also make it possible to have the builtin ADL types map directly to the Haskell types. For example:

  • ADL Nullable maps to Haskell Nullable type, which is a newtype around Maybe.
  • ADL StringMap maps to Haskell StringMap type which is a newtype around Map.

I'm not sure I see the value in these wrappers, they just create extra friction when using them in my code. Would be much nicer if the newtypes were eliminated and I would use Maybe and Map directly.

Thank you

@timbod7
Copy link
Collaborator

timbod7 commented Apr 20, 2021

I'm not sure if this is related.

It is related. These newtype wrappers existing because

I would use Maybe and Map directly

Note that you already do use Maybe or Map directly when you write ADL sys.types.Maybe<T> and sys.types.Map<K,V>. The wrappers are only required for the Nullable<T> and StringMap<V> primitives.

@timbod7
Copy link
Collaborator

timbod7 commented Apr 20, 2021

I think it would be better indeed to modify the haskell and rust code generators to use this approach, making them consistent with the other language backends.

I started think about what this would look like, and started prototyping. Let me know what you think on

#128

@bitc
Copy link
Author

bitc commented May 23, 2021

Thank you, I will try this out soon 👍

@bitc
Copy link
Author

bitc commented May 25, 2021

@timbod7 I looked at #128
This is still incomplete, right? But the JsonBinding looks good to me

@timbod7
Copy link
Collaborator

timbod7 commented May 26, 2021

It's not even close to being complete.

This was just an initial hand written attempt to reproduce what the generated code and runtime would look like if the code generator were updated to work this way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants