Introducing elm-review-derive

For quite a while me and @MartinS have been working on a little project called elm-review-derive. I think it’s at a stage where it can be released at a sort of beta level of expectations. The hope here is that it can be useful for a number of people, but also get more people interested in this kind of thing and provide some quality feedback about the tool.

What is it?

It’s a tool that provides ad-hoc code generation inside your project based on type signatures. It searches for top-level functions whose body is a Debug.todo and that have a type signature the tool recognizes, such as:

myDecoder : Decode SomeType
myDecoder =
      Debug.todo ""

It will mark any such rule as an error (this is an elm-review rule after all), and attempt to provide a fix. It does that by recursively walking down the definition of the target type (SomeType in this example) and generating definitions to match the type.

So if SomeType looks like this:

type alias SomeType =
     { name : String 
     , age : Int
     , status : Status
     }

type Status = 
    Approved | Rejected

then the first fix provided will change the code to:

myDecoder : Decoder SomeType
myDecoder =
    Decode.map3
        SomeType
        (Decode.field "name" Decode.string)
        (Decode.field "age" Decode.int)
        (Decode.field "status" decodeStatus)

decodeStatus : Decoder Status
decodeStatus =
    Debug.todo ""

However, elm-review keeps running rules until they stop finding issues, so the rule will continue running and fills out decodeStatus like so:

myDecoder : Decoder Status
myDecoder =
    Decode.field "tag" Decode.string
        |> Decode.andThen
            (\ctor ->
                case ctor of
                    "Approved" ->
                        Decode.succeed Approved

                    "Rejected" ->
                        Decode.succeed Rejected

                    _ ->
                        Decode.fail "Unrecognized constructor"
            )

This step by step generation allows for interactive use (intended to be used with --fix rather than --fix-all), where you can pause generation at any point and adjust what’s going on before you keep going.

For instance perhaps the generated decoders don’t quite suite your preference. But the advantage of this tool is that it works in place in your editor and it’s often easier to quickly edit the generated code rather than type it out from scratch.

What’s it like to code with this?

To give you an idea what a workflow of using this tool can look like, I’ve recorded a video of me adding a few features to an existing application using this tool:

(Sorry for the occasional mumbling, I’ve gained some respect for streamers as talking and programming at the same time turns out to be harder than expected).

What kind of things can it generate?

At the moment, it can help with the following sorts of type signatures:

Serialize.Codec e MyType
Codec.Codec e MyType
Csv.Decode.Decoder MyType
String -> Maybe MyType
Fuzz.Fuzzer MyType
Json.Decode.Decoder MyType
MyType -> Json.Encode.Value
List MyType
Random.Generator MyType
MyType -> String

See the README for more details.

Can it be extended?

Yes, it is possible and not too difficult to write additional generators that can generate additional kinds of structures. The documentation should be reasonably helpful, or you can browse the existing code generators as examples.

Future plans

First, we’d like to shake out any existing bugs and improve the error reporting capability (sometimes it can be kind of confusing why code generation doesn’t work or how to fix it).

Second, I’d like to expand the kinds of type signatures we can reasonably operate on. For instance, I’d like to be able to generate meaningful map functions, or generate functions that can transform one type into another type.

Finally, at the moment the only input the code generation context can take is type information. I think it would be interesting to also allow other forms of input from the surrounding code. For one the string from the Debug.todo could be used in interesting ways. Html could be transformed into elm/html or graphql queries could be matched to the desired types in the type signatures to automatically generate elm-graphql code??? Who knows what else can be done?

Anyway, I hope you can give this tool a try and I’ll be happy to hear any feedback you may have.

30 Likes

Yes!!! This is wonderful and such a powerful validation of the superiority of Elm and elm-review over the competition.

4 Likes

@gampleman I think you are being too generous with crediting me. Most of the work was done by you :sweat_smile:. Good job on the release!

I think this should be Codec.Codec MyType assuming this is miniBill/elm-codec?

2 Likes

Hey @gampleman and @MartinS, congratulations on the release!

One question: say I change the types. Is there some easy way to regenerate encoders and decoders (and other things), other than me manually nuking the definition and replacing it with Debug.todo ""?

No, not really. I think the intent of the tool is for code generation as part of potentially manually adjusting the generated code, not just treating it as a static target. That sort of means that we don’t want to touch code other than where explicitly indicated.

Seems like removing the definitions shouldn’t be too onerous, no?

1 Like

This is wonderful, and I will definitely be using this a lot!

1 Like

Could this generate:

eq : MyType -> Bool

or even:

compare : MyType -> Order

At least in the case of enums:

type MyType = One | Two | Three

Should be easy.

Constructors with args could be done recursively, but perhaps less of a need for that, than for the simple enum case and constructors with 0 args.


Previously I created this enum package: elm-enum 1.0.1

I think I could combine that with List MyType and MyType -> String with Enum.make : List a -> (a -> String) -> Enum a to generate enums in that style.

In principle yes.

Although I think you are asking actually about functions of two arguments, right?

eq : MyType -> MyType -> Bool and compare : MyType -> MyType -> Order?

At the moment we don’t support signatures and patterns with more than one target, but in principle it shouldn’t be difficult to add support for it. I think the main things to do would be to a) test all the weird corner cases and b) refine the APIs in CodeGenerator to support such a thing.

I think the infrastructure that exists today has a lot of exciting ways to expand to future use cases, but I wanted to release something than keep it under wraps and endlessly extend it. This way we can explore where to go next together!

1 Like

Although I think you are asking actually about functions of two arguments, right?

Yes, don’t know how I got that wrong but two args…

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.