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

ocaml-decoders conversion experiment #1280

Merged
merged 9 commits into from Feb 3, 2020

Conversation

glennsl
Copy link
Member

@glennsl glennsl commented Jan 31, 2020

This converts most notably ExtensionManifest, ExtensionContributions and ExtHostInitData, but also Configuration and LocalizedToken, to use ocaml-decoders instead of ppx_derigin_yojson for encoding and decoding of json.

Currently this adds quite a bit of extra code. That's partly because of the infrastructure additions in Core.Json which required copying parts of ocaml-decoders to get access to some internals. That shouldn't really be necessary, and is a one-time cost either way. But more on that below.

A lot of the remaining weight is because this just mechanically reimplements the decoders and encoders generated by ppx_deriving_yojson. But a lot of this, and existing code, is unnecessary because, with custom decoders, intermediate data structured are no longer necessary just to generate the decoders. That is the point of this after all, to decode and encode directly to and from application data structures and save the intermediate data structure and extra conversions.

Unfortunately these intermediate data structures reach far into the model and beyond, and will take some work to untangle. But it does also serve to illustrate the point, that when intermediate data structures are used for the purpose of "convenient" generation fo json encoders and decoders, they tend to end up being used where they shouldn't, in place of application-specific data structures serving application needs, and making a mess of dependencies.

As for using the decoders themselves, I recommend stepping through the commits to see the progression. Decoding objects with the field decoder and monadic bind operator was a pretty messy affair, and helped by refmt still not having a clue about the monadic bind operator and aiming for the pyramid of doom instead. This could be improved with the monadic let bindings in 4.08, but I've also added a custom obj decoder that I think provides a significantly better experience that that would anyway. It's not without shortcomings though - in particular because the provided decoders can't be composed as normal decoders - and the API should still be considered unfinished. It also required copying parts of the ocaml-decoders implementation to get at the internals. We should try to get this upstreamed in some way to avoid that.

Caveats aside, I hope this is sufficient to get a good idea of how ocaml0decoders work and how the two approaches compare. Where ocaml-decoders replace custom yojson encoders and decoders, the former is certainly much nicer to use. I also like to have the decoding and encoding separate from the data structure. But most important I think is that it removes the need for intermediate data structures, and the temptation to use them where they shouldn't, because if you don't you often end up with just as much conversion code, but now have the overhead of conversion in addition to the hidden complexity of decoding and encoding.

@glennsl glennsl requested a review from bryphe January 31, 2020 08:29
Comment on lines 20 to 26
let decode =
Json.Decode.(
field("command", string) >>= command =>
field("title", LocalizedToken.decode) >>= title =>
field_opt("category", string) >>= category =>
succeed({command, title, category})
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of decoder using field and monadic bind (>>=). Not too bad for small decoders.

Comment on lines 55 to 80
let manifest =
field("name", string) >>= name =>
field("version", string) >>= version =>
one_of([
("author", field("author", author)),
("publisher", field("publisher", string)),
("default", succeed("Unknown Author")),
]) >>= author =>
field_opt("displayName", LocalizedToken.decode) >>= displayName =>
field_opt("description", string) >>= description =>
field_opt("main", string) >>= main =>
field_opt("icon", string) >>= icon =>
field_opt("categories", list(string)) |> default([]) >>= categories =>
field_opt("keywords", list(string)) |> default([]) >>= keywords =>
field("engines", engine) >>= engines =>
field_opt("activationEvents", list(string)) |> default([]) >>= activationEvents =>
field_opt("extensionDependencies", list(string)) |> default([]) >>= extensionDependencies =>
field_opt("extensionPack", list(string)) |> default([]) >>= extensionPack =>
field_opt("extensionKind", kind) |> default(Ui) >>= extensionKind =>
field("contributes", ExtensionContributions.decode) >>= contributes =>
field_opt("enableProposedApi", bool) |> default(false) >>= enableProposedApi =>
succeed({
name, version, author, displayName, description, main, icon,
categories, keywords, engines, activationEvents, extensionDependencies,
extensionPack, extensionKind, contributes, enableProposedApi
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Significantly less readable for large decoders.

Note that there's also a "pipeline" API which is more readable, and can reduce the boilerplate a bit with generated record constructors, but it depends on the position of arguments and is therefore more error-prone, and also yields harder-to understand errors messages.

Comment on lines 53 to 166
field_opt("displayName", LocalizedToken.decode)
>>= (
displayName =>
field_opt("description", string)
>>= (
description =>
field_opt("main", string)
>>= (
main =>
field_opt("icon", string)
>>= (
icon =>
field_opt("categories", list(string))
|> default([])
>>= (
categories =>
field_opt("keywords", list(string))
|> default([])
>>= (
keywords =>
field("engines", engine)
>>= (
engines =>
field_opt(
"activationEvents",
list(string),
)
|> default([])
>>= (
activationEvents =>
field_opt(
"extensionDependencies",
list(string),
)
|> default([])
>>= (
extensionDependencies =>
field_opt(
"extensionPack",
list(string),
)
|> default([])
>>= (
extensionPack =>
field_opt(
"extensionKind",
kind,
)
|> default(Ui)
>>= (
extensionKind =>
field(
"contributes",
ExtensionContributions.decode,
)
>>= (
contributes =>
field_opt(
"enableProposedApi",
bool,
)
|> default(
false,
)
>>= (
enableProposedApi =>
succeed({
name,
version,
author,
displayName,
description,
main,
icon,
categories,
keywords,
engines,
activationEvents,
extensionDependencies,
extensionPack,
extensionKind,
contributes,
enableProposedApi,
})
)
)
)
)
)
)
)
)
)
)
)
)
)
)
)
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh dear god refmt. Wat u doin?

Comment on lines 20 to 29
let decode =
Json.Decode.(
field("command", string)
>>= (
command =>
field("title", LocalizedToken.decode)
>>= (
title =>
field_opt("category", string)
>>= (category => succeed({command, title, category}))
)
obj(({field, _}) =>
{
command: field.required("command", string),
title: field.required("title", LocalizedToken.decode),
category: field.optional("category", string),
}
)
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example of a small decoder using obj

("publisher", field.monadic("publisher", string)),
("default", succeed("Unknown Author")),
]),
),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the shortcomings of the obj decoder. Because the field.* decoders aren't "normal" monadic decoders, they can't be composed. So we need an escape hatch. This is it, called whatever while I think of a better name for it.

enableProposedApi:
field.withDefault("enableProposedApi", false, bool),
}
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise I think this looks significantly better than the alternatives.

@@ -32,6 +32,7 @@ This project incorporates components from the projects listed below. The origina
25. fmt (https://github.com/dbuenzli/fmt)
26. LaserWave (https://github.com/Jaredk3nt/laserwave)
27. camlp5 (https://github.com/camlp5/camlp5)
28. ocaml-decoders (https://github.com/mattjbray/ocaml-decoders)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ocaml-decoders, but the license is just ISC. Literally, that is the entire license text. So should I just add that to the end here, find the text of the ISC license from somewhere else, or just leave it as-is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This in particular ought to be addressed before merging @bryphe

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thanks for the ping @glennsl ! Definitely - sorry I missed this.

Let's just add the ISC license text (from here: https://opensource.org/licenses/ISC)

Copyright 2020 mattjbray

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Thanks for checking in on this!

@glennsl glennsl merged commit f8a8be1 into onivim:master Feb 3, 2020
@glennsl glennsl deleted the refactor/decoders branch February 3, 2020 19:37
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

Successfully merging this pull request may close these issues.

None yet

2 participants