Elm Code Generation in Protoc-Gen-Elm

Hi :slight_smile:

I’m currently maintaining the protoc-gen-elm protoc plugin and stumbled across an issue/limitation in the elm module system. Because all solutions that I can come up with kind of suck in one way or another, I thought I’ll make a post here to get some ideas and feedback.

What does protoc-gen-elm do?

It generates .elm files from .proto files, which contain decoders and encoders to convert Elm datatypes to the protocol buffers wire format.

Given a proto file like this:

syntax = "proto3";

message Test {
  string fieldOne = 1;
  bool fieldTwo = 2;
}

something like this is generated:

import Protobuf.Encode (Encoder)
import Protobuf.Decode (Decoder)

type alias Test {
  fieldOne : String,
  fieldTwo : Bool
}

encodeTest : Test -> Encoder
decodeTest : Decoder Test

Nested messages

In .proto files, messages are also a namespace where we can group other declarations.
For example:

enum OuterEnum {
  A = 0;
  B = 1;
}

message Outer {
  message Inner {}
  enum InnerEnum {
    One = 0;
    Two = 1;
  }
  oneof msg {
    Inner inner = 1;
    InnerEnum innerEnum = 2;
    OuterEnum outerEnum = 3;
  }
}

Now the question becomes: What elm code do we generate from this?
The following is what I want to be able to write:

encodedOuter = encodeOuter { msg = Just <| Outer.inner {}}
encodedOuter2 = encodeOuter { msg = Just <| Outer.innerEnum Outer.One }
encodedOuter3 = encodeOuter { msg = Just <| Outer.outerEnum B }

or something similar.

The issue with this code is that to handle the scopes, we used two modules:
The Outer module which defines all the things inside the scope of the Outer message and the
other, in this example implicitely global, scope, which defines the Outer type, the encodeOuter function and the OuterEnum union type.

In order to define a type for Outer.outerEnum we need to be able to refer to the OuterEnum type from the Outer module. But in order to define the type Outer, we need to be able to refer to the types inside of the Outer module. We have just ran into a cyclic dependency!

Solutions

The current solution implemented in protoc-gen-elm is to scrap the proto namespacing stuff and just

  1. hope for no collisions
  2. Try to do manual namespacing by adding prefixes like Outer_Msg_InnerEnum

The general solution for cyclic dependencies is to put the cyclic stuff into its own module and then import from there. Unfortunately, this comes at a great disadvantage:

While we can re-export functions and type aliases with wrapper functions and more type aliases,
the fun stops at union types. I am not aware of any way to re-export the constructor function Outer_Msg_InnerEnum as something more readable in another module. Obviously I can create a wrapper function but I lose the ability to pattern match.

The most recent idea that I’ve had is to generate the “ugly” type in a big module with all the stuff in it in order to have no cyclic dependency issue and to generate a nice seperate type in another module together with conversion functions between the two. That would make the generated code kind of confusing, but at least the code on the user side would look pretty good.

Other ideas?

What do you think? Have I missed something obvious? Are there other ways of solving the issue?
Is the original way of namespacing with “_” the more understandable solution?

For some context, here is the issue where my exploration started: Name clashes · Issue #151 · andreasewering/protoc-gen-elm · GitHub

Thanks!

I decided for generating mapping methods for union types and using aliases for product types.

The changes are released in a beta version (3.0.0-beta.0)

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